Skip to content

added full Language server for URDF and xacro #9

Open
pichifkes wants to merge 33 commits into
OTL:masterfrom
pichifkes:master
Open

added full Language server for URDF and xacro #9
pichifkes wants to merge 33 commits into
OTL:masterfrom
pichifkes:master

Conversation

@pichifkes

Copy link
Copy Markdown

Read the readme and try it out.
its nice

error messages, dropdowns for variables, the whole shtick
This one is limited to Linux ONLY

pichifkes and others added 22 commits May 21, 2026 09:49
- 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 urdf language 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.

Comment on lines +47 to +48
npm install -g @vscode/vsce
vsce package --no-dependencies --out urdf-$(node -p "require('./package.json').version")-linux-x64.vsix

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread .vscodeignore
client/out/**/*.map
client/src/**
client/tsconfig.json
client/node_modules/**

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread server/src/features.rs Outdated
Comment on lines +493 to +510
// 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;
}
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Left-boundary check (features.rs:496-497) — the byte before the match must
    be whitespace, <, or /. That rejects xlink, parent_link, _link, etc.
  2. 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).
  3. 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).

Comment thread server/src/features.rs Outdated
Comment on lines +519 to +527
/// 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,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread server/src/document.rs Outdated
Comment on lines +206 to +211
/// 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +120 to +125
"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" }
},

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@pichifkes pichifkes closed this May 21, 2026
@pichifkes pichifkes reopened this May 21, 2026
@pichifkes pichifkes marked this pull request as draft May 22, 2026 08:02
@pichifkes pichifkes marked this pull request as ready for review May 22, 2026 18:18
@pichifkes pichifkes marked this pull request as draft May 22, 2026 18:20
@pichifkes pichifkes marked this pull request as ready for review May 24, 2026 07:26
@pichifkes

Copy link
Copy Markdown
Author

hey @OTL , I validated the code.

@pichifkes

Copy link
Copy Markdown
Author

@OTL are you there?

@pichifkes pichifkes closed this May 27, 2026
@pichifkes pichifkes reopened this May 27, 2026
@pichifkes

Copy link
Copy Markdown
Author

Mr. @OTL I'm here waiting desperately for you to tell me what is wrong with my code

@OTL

OTL commented May 28, 2026 via email

Copy link
Copy Markdown
Owner

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants