added full Language server for URDF and xacro #9
Conversation
- Full Gazebo property schema: mu1/mu2/kp/kd/selfCollide etc. with type
validation (float, bool, int) inside <gazebo> blocks
- gazebo reference= validated against known links/joints
- Completions for reference= (links+joints) and element names inside <gazebo>
- Fix ${-var} and ${pi/2} falsely flagged as undefined xacro properties
- Eliminate double XML parse per keystroke (check_schema now reuses parsed doc)
- Deduplicate byte_range_to_lsp (pub(crate) in document.rs)
- Extract is_xacro_element() and resolve_effective() helpers
- HashSet for duplicate detection, remove unused imports and allow suppressor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three new checks in check(): - Isolated link (no joints at all): Error/Warning - Multiple roots (>1 link with no parent joint but with children): Error/Warning - Cycle: reports the joint that closes the loop, always Error Severity is WARNING for xacro files (tree may be completed by included files) and ERROR for plain URDF. Includes 4 unit tests covering isolated, multiple-roots, cycle, and valid chain. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
roxmltree reports XML parse errors at the position where the inconsistency was first detected — typically the closing tag. For a misspelled opening tag like <mateaaaarial> ... </material>, this meant the error appeared on the (correct) closing tag, hiding the actual typo. Add a small fallback tag-balance scanner in document::parse that runs only when roxmltree fails. It tracks opening/closing tag balance and reports: - Mismatched tag: opened <A> but closed with </B> — on the opening tag - Closing tag </X> has no matching opening tag — on the closing tag - Tag <X> is never closed — on the opening tag Honours quoted attribute values, self-closing tags, comments, CDATA, DOCTYPE, and processing instructions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New module xacro_eval.rs implements a small recursive-descent
expression evaluator: +, -, *, /, %, unary -, parentheses, the pi
constant, math functions (sin/cos/tan/abs/sqrt/radians/degrees), and
variable references with recursive resolution and cycle detection.
features::inlay_hints scans the requested range for ${...} substitutions,
evaluates each via xacro_eval, and emits an inlay hint right after the
closing '}' showing the computed value.
document::XacroProperty now also stores the raw value attribute so the
evaluator can resolve variable references without re-parsing the XML.
Includes 9 unit tests (8 for the evaluator, 1 end-to-end exercising the
inlay_hints function on a realistic URDF snippet).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The server was advertising CompletionOptions::default() with no trigger
characters, so VS Code only invoked completion when the user typed
identifier characters. Typing \${ or \" never popped up the dropdown.
Also fix an edge case in the xacro regex_match: when the prefix is
exactly \${ at column 0 (no closing brace anywhere), open > close
evaluated 0 > 0 = false, suppressing the completion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The xacro property scanner looked for the next '}' anywhere in the
file. When a ${... was left unclosed (e.g. radius=\"\${asaso\"), the
scanner happily consumed text past the closing quote and across element
boundaries until it found a stray '}'. The resulting \"varname\"
contained spaces/operators, so the existing operator-character guard
silently dropped it — no diagnostic at all.
Fix: while scanning for the closing brace, abort at attribute/element
boundaries ('\"', \"'\", '<', newline). On abort emit an
\"Unclosed xacro expression: missing '}'\" error on the ${... span.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VS Code defaults editor.quickSuggestions.strings to \"off\", which
suppresses completion when typing identifier characters inside a
\"...\" attribute value. Since virtually every useful URDF/xacro
completion (link names, joint names, xacro property names) appears
inside attribute strings, the default made the dropdown essentially
invisible — only the registered trigger characters ($, {, \", <)
fired it.
Override the default for files with the urdf language id via
contributes.configurationDefaults.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For each diagnostic whose message we recognise, offer a quick-fix:
- Undefined link / joint / mimic / gazebo reference → typo-correction
candidates from the appropriate symbol pool (Levenshtein distance,
up to 3 suggestions within edit distance ~len/3)
- Undefined xacro property → typo correction wrapped in \${...}
- Element missing required attribute → insert attr=\"\" right after
the element name
Wires up the LSP code_action capability and handler.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When roxmltree::Document::parse failed (e.g. a single malformed line),
document::parse returned an empty Document while check() and
check_schema() still ran. With empty xacro_properties, every
\${var} reference in the file was wrongly flagged as 'Undefined xacro
property' — drowning the real XML-parse error in cascade noise.
Add a parse_ok flag on Document; main::validate now runs the semantic
checks only when parse_ok is true. The XML-parse-error diagnostic
still reaches the user, so they can fix that first.
Regression test added.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every multi-line element (link, joint, visual, collision, inertial, gazebo, plugin, material with children, xacro:macro, etc.) becomes a collapsible region. The closing tag stays visible when collapsed. Walks the parsed roxmltree::Document; emits one FoldingRange per element where the closing tag is at least two lines below the opening. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
roxmltree 0.20 emits errors like \"expected '=' not 'e' at 26:19\" — the position lives at the END of the message after \" at \". Our old parser only matched a leading \"ROW:COL message\" prefix, failed silently, and fell back to (0, 0), which is why a typo on line 26 was reported at the top of the file. parse_xml_error_pos now first tries the tail-of-message form, then falls back to the leading form for compatibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LSP color provider: - document_color handler walks the XML for <color rgba=\"r g b a\"/> and returns a ColorInformation per match so VS Code can render an inline clickable swatch - color_presentation returns the space-separated literal that replaces the rgba value when the user picks a new colour - Wired through the LSP capabilities TextMate grammar additions: - New scopes: urdf-structure (parent/child/mimic), urdf-material (material/color/texture), urdf-inertial (mass/inertia) - New gazebo-properties scope for mu1/mu2/kp/kd/etc. — distinct color - New urdf-attributes scope so common attribute names (name=, link=, joint=, type=, rgba=, xyz=, rpy=, size=, radius=, length=, etc.) highlight as attribute names document::attr_value_range was made pub(crate) so features.rs can reuse it for the color range. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous scopes (entity.name.tag.material.urdf, entity.name.tag.inertial.urdf, entity.name.tag.structure.urdf) fell back to entity.name.tag in most themes — i.e. exactly the same color as a plain XML tag, so the new groupings produced no visible differentiation. Switched to scopes that themes commonly style distinctly: - parent/child/mimic → variable.parameter - material/color/texture → support.class - mass/inertia → support.function Other scopes already used well-themed roots (keyword.control, storage.type, support.type, support.constant, entity.other.attribute-name). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous grammar inherited text.xml and tried to override scopes
with top-level patterns using \`(?<=</?)(name)(?=...)\` lookbehind.
The problem: text.xml's tag pattern uses begin=\"(</?)([\\w:.-]+)\" which
consumes the entire \`<name\` as one match before any of our top-level
patterns get tried at the position of the name. So our overrides
never ran — every URDF/xacro file looked identical to plain XML.
Replace the grammar with our own \`tag\` begin/end repository entry that
captures the tag name via a nested patterns array, dispatching to
specific scope rules:
- robot/link/joint/transmission/gazebo → keyword.control
- visual/collision/inertial/geometry/origin/axis/limit/dynamics/etc → storage.type
- box/cylinder/sphere/mesh → support.type
- material/color/texture → support.class
- mass/inertia → support.function
- parent/child/mimic → variable.parameter
- mu1/mu2/kp/kd/etc. → support.constant
- xacro:* → entity.name.function
Also handles attribute names (entity.other.attribute-name), joint type
values inside strings (constant.language), \${...} interpolation, XML
prolog, and comments. Falls back to text.xml for anything we don't
specifically know about.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In TextMate's vscode-textmate engine, ^ and $ are line anchors — even when re-scanning a captured value. The capture is still part of the original line, so ^material never matches because the line starts with whitespace, not 'material'. As a result every URDF tag fell through to the .+ fallback, scoped as entity.name.tag.xml — exactly what the scope inspector confirmed. Switch to \A/\z (start/end of string, Oniguruma absolute anchors) which anchor to the start/end of the captured substring itself. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ures The previous design captured the tag name and used a nested patterns array to dispatch to specific scopes. Whether due to anchor handling or capture-pattern semantics in vscode-textmate, none of the specific patterns matched — every tag ended up with the .+ fallback scope (entity.name.tag.xml), as confirmed by the scope inspector showing joint, link, material all at the same color. Replace the single dispatching tag rule with one begin/end rule per category. The scope is assigned directly in beginCaptures[2] — no nested patterns, no anchor tricks, just a literal alternation in the begin regex that matches only the tag names for that category. Order in the top-level patterns list ensures the specific rules win before the generic-tag fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR upgrades the extension from primarily snippets/highlighting into a full URDF/xacro language extension by introducing a Rust-based LSP server (stdio) plus a thin TypeScript VS Code client, and wiring it into the extension manifest and build pipeline (Linux-only per README).
Changes:
- Added a Rust language server (
urdf-lsp) implementing diagnostics, completions, hover/definition/references/rename, inlay hints, code actions, folding, and color features. - Registered a dedicated
urdflanguage id with a custom TextMate grammar + language configuration for URDF/xacro editing. - Updated packaging/build workflow, docs, examples, and VS Code dev tasks/launch config.
Reviewed changes
Copilot reviewed 19 out of 23 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| syntaxes/urdf.tmLanguage.json | Adds a TextMate grammar for URDF/xacro highlighting. |
| language-configuration.json | Adds comment/bracket/autoclosing configuration for the urdf language. |
| package.json | Registers urdf language + grammar + snippets; points extension entry to client/out. |
| README.md | Documents new LSP-based feature set and build/run instructions. |
| CHANGELOG.md | Notes the 0.4.0-dev LSP rewrite and Linux-only status. |
| examples/sample.urdf | Adds a sample URDF for manual testing/demonstration. |
| client/src/extension.ts | Implements the VS Code client that spawns the Rust server over stdio. |
| client/tsconfig.json | Adds strict TS build config for the client. |
| client/package.json | Declares client dependency on vscode-languageclient. |
| client/package-lock.json | Locks client dependencies. |
| server/src/main.rs | Implements the LSP server entrypoint and registers capabilities/handlers. |
| server/src/document.rs | Adds XML parsing + symbol extraction + tag-balance fallback diagnostics helpers. |
| server/src/diagnostics.rs | Adds semantic + schema-like validation diagnostics and related tests. |
| server/src/features.rs | Adds IDE features (completion/hover/definition/rename/etc.) and quick-fix actions. |
| server/src/xacro_eval.rs | Adds ${...} expression evaluation + formatting and unit tests. |
| server/Cargo.toml | Declares the Rust server crate and dependencies. |
| server/Cargo.lock | Locks Rust dependencies for the server. |
| .vscodeignore | Updates packaging exclusions for client/server build artifacts. |
| .vscode/tasks.json | Replaces watch tasking with explicit build tasks for server/client. |
| .vscode/launch.json | Updates extension host launch config to use the new build task and output path. |
| .gitignore | Updates ignored build outputs for the new client/server layout. |
| .github/workflows/build.yml | Adds CI build + vsix packaging workflow (Linux). |
Files not reviewed (1)
- client/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| npm install -g @vscode/vsce | ||
| vsce package --no-dependencies --out urdf-$(node -p "require('./package.json').version")-linux-x64.vsix |
There was a problem hiding this comment.
Added esbuild to client/package.json — extension.ts gets bundled into a single out/extension.js with --bundle --external:vscode, which inlines vscode-languageclient and its deps into that one file. The .vsix ships only the bundle, so client/node_modules being excluded no longer matters.
| client/out/**/*.map | ||
| client/src/** | ||
| client/tsconfig.json | ||
| client/node_modules/** |
There was a problem hiding this comment.
Bundled the client with esbuild — client/package.json now has a bundle script (esbuild ... --bundle --platform=node --external:vscode) that produces a self-contained client/out/extension.js, so vscode-languageclient/node is inlined and .vscodeignore excluding client/node_modules/ is fine.
| // Walk backwards through all occurrences of the attribute name. | ||
| let mut search = prefix; | ||
| loop { | ||
| let Some(pos) = search.find(attr) else { | ||
| return false; | ||
| }; | ||
| let after_attr = &search[pos + attr.len()..]; | ||
| // Skip optional whitespace | ||
| let after_ws = after_attr.trim_start_matches(|c: char| c == ' ' || c == '\t'); | ||
| if let Some(rest) = after_ws.strip_prefix('=') { | ||
| let after_eq = rest.trim_start_matches(|c: char| c == ' ' || c == '\t'); | ||
| if let Some(after_quote) = after_eq.strip_prefix('"') { | ||
| // We are inside the quote only if there is no closing `"` after it. | ||
| if !after_quote.contains('"') { | ||
| return true; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
- Left-boundary check (features.rs:496-497) — the byte before the match must
be whitespace, <, or /. That rejects xlink, parent_link, _link, etc. - Right-boundary check (features.rs:499-508) — after the matched name there
must be optional whitespace, =, optional whitespace, ", and no closing " yet
(cursor still inside the value). - Loop, not single-shot — if the first hit fails boundary checks, it keeps
scanning further into the prefix instead of giving up.
And there are explicit tests at features.rs:846-858 covering the exact cases
from the review: xlink=, parent_link=, and the "already-closed quote" case
(cursor past the value).
| /// True when the cursor byte offset falls inside an open `<gazebo …>` block. | ||
| fn inside_gazebo_block(text: &str, offset: usize) -> bool { | ||
| let before = &text[..offset.min(text.len())]; | ||
| let last_open = before.rfind("<gazebo"); | ||
| let last_close = before.rfind("</gazebo"); | ||
| match (last_open, last_close) { | ||
| (Some(open), Some(close)) => open > close, | ||
| (Some(_), None) => true, | ||
| _ => false, |
There was a problem hiding this comment.
Fixed. Self-closing <gazebo …/> now returns false via scan_to_tag_close (the
canonical quote-aware scanner), with test coverage at features.rs:868
(inside_gazebo_block_after_self_closing) and features.rs:880 (quoted />
doesn't false-trigger).
| /// Parse a row:col prefix from roxmltree error messages (e.g. "9:5 unexpected close tag"). | ||
| /// Returns 0-indexed (line, character). Falls back to (0, 0) if the format doesn't match. | ||
| /// Walk the document tracking opening/closing tag balance. Returns a single | ||
| /// diagnostic positioned on the actual misspelled (or unclosed) opening tag, | ||
| /// rather than at the closing tag where the inconsistency was detected. | ||
| /// Used as a fallback when roxmltree::Document::parse fails. |
There was a problem hiding this comment.
Fixed. The doc comment on scan_tag_balance (lines 261–264) now only describes the tag-balance scanner. no leftover row/col parsing language. parse_xml_error_pos has its own separate comment at line 414.
| "gazebo-prop-tag": { | ||
| "begin": "(</?)(mu1|mu2|mu|kp|kd|maxVel|minDepth|maxContacts|selfCollide|turnGravityOff|gravity|implicitSpringDamper|dampingFactor|laserRetro|stopCfm|stopErp|fudgeFactor)(?=[\\s/>])", | ||
| "beginCaptures": { | ||
| "1": { "name": "punctuation.definition.tag.xml" }, | ||
| "2": { "name": "support.constant.gazebo" } | ||
| }, |
There was a problem hiding this comment.
Fixed. Added material, sensor, plugin to the regex, plus a doc comment pointing to GAZEBO_PROPS in diagnostics.rs as the single source of truth.
|
hey @OTL , I validated the code. |
|
@OTL are you there? |
|
Mr. @OTL I'm here waiting desperately for you to tell me what is wrong with my code |
|
Hello. I'm very busy, so please wait patiently.
/*************************
Takashi Ogura (小倉 崇)
***@***.***
*************************/
2026年5月28日(木) 19:03 pichifkes ***@***.***>:
… *pichifkes* left a comment (OTL/vscode-urdf#9)
<#9 (comment)>
Mr. @OTL <https://github.com/OTL> I'm here waiting desperately for you to
tell me what is wrong with my code
—
Reply to this email directly, view it on GitHub
<#9?email_source=notifications&email_token=AABSSJRBIPISXFG6PIAYAXD45AFHPA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTINJWGI4TAMBUG432M4TFMFZW63VHNVSW45DJN5XKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-4562900477>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABSSJRHU7L32O5MW27TGUD45AFHPAVCNFSM6AAAAACZHXRNAKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DKNRSHEYDANBXG4>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Read the readme and try it out.
its nice
error messages, dropdowns for variables, the whole shtick
This one is limited to Linux ONLY