Skip to content

feat: go-to definition (#9619)#9700

Merged
ematipico merged 25 commits intonextfrom
feat/go-to-definition
May 8, 2026
Merged

feat: go-to definition (#9619)#9700
ematipico merged 25 commits intonextfrom
feat/go-to-definition

Conversation

@ematipico
Copy link
Copy Markdown
Member

@ematipico ematipico commented Mar 29, 2026

Note

I used AI mostly for tests and bootstrapped the initial code, but rewrote most of it manually as I added more features.

Summary

Part of #9618

This is a follow-up of #9619 to complete the feature.

The majority of the architectural work is described in the PR. This PR implements the remaining features that were on the list.

I had to create a new function called get_real_capabilities because the old one wouldn't work for this feature, and changing it would break existing functionality.

I also refactored how the SettingsHandle is computed. It now accepts both inline config, and the new EditorFeatures. Cleaner and more extensible, and we don't need to use two handles.

I want to keep the issue open so that users can leave feedback there.

Test Plan

Added other tests.

Docs

biomejs/website#4197

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 29, 2026

🦋 Changeset detected

Latest commit: 875123c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@biomejs/biome Minor
@biomejs/cli-win32-x64 Minor
@biomejs/cli-win32-arm64 Minor
@biomejs/cli-darwin-x64 Minor
@biomejs/cli-darwin-arm64 Minor
@biomejs/cli-linux-x64 Minor
@biomejs/cli-linux-arm64 Minor
@biomejs/cli-linux-x64-musl Minor
@biomejs/cli-linux-arm64-musl Minor
@biomejs/wasm-web Minor
@biomejs/wasm-bundler Minor
@biomejs/wasm-nodejs Minor
@biomejs/backend-jsonrpc Patch
@biomejs/js-api Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added A-CLI Area: CLI A-Project Area: project A-Linter Area: linter A-LSP Area: language server protocol L-JavaScript Language: JavaScript and super languages L-HTML Language: HTML and super languages labels Mar 29, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Mar 29, 2026

Merging this PR will not alter performance

✅ 249 untouched benchmarks


Comparing feat/go-to-definition (875123c) with next (ebbf0bd)

Open in CodSpeed

@ematipico ematipico force-pushed the feat/go-to-definition branch from baba547 to 5fe1c4e Compare April 24, 2026 09:44
@github-actions github-actions Bot added A-Formatter Area: formatter L-CSS Language: CSS and super languages L-JSON Language: JSON and super languages L-Grit Language: GritQL labels Apr 24, 2026
@ematipico ematipico force-pushed the feat/go-to-definition branch from 064c8a0 to 3086d1a Compare April 28, 2026 07:12
@ematipico ematipico marked this pull request as ready for review April 29, 2026 15:51
@ematipico ematipico requested review from a team April 29, 2026 15:51
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Implements end-to-end LSP “go to definition”: new workspace types and server method, LSP capability advertisement and handler, editor-feature gating propagated on open/change, file-handler resolvers for JS/HTML/CSS, module-graph tracking of CSS selector ranges and inline offsets, richer embedded-binding metadata, many tests and test utilities, and updates to Open/Change file params to accept optional editor_features.

Possibly related PRs

Suggested reviewers

  • dyc3
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: go-to definition (#9619)' clearly and concisely describes the primary feature being added, and aligns with the changeset and code modifications across LSP handlers, workspace APIs, and file handlers.
Description check ✅ Passed The description provides context about the feature being a follow-up to PR #9619 for issue #9618, mentions AI assistance disclosure, outlines architectural decisions (new get_real_capabilities function), and references related documentation and test additions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/go-to-definition

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/biome_service/src/workspace/document/services/embedded_bindings.rs (1)

471-509: ⚠️ Potential issue | 🟠 Major

Finish the import-source wiring for all import forms.

default_specifiers() already registers the default binding, so the new default-clause branch adds a second copy. Meanwhile named imports still go through register_binding(...), which means import { Button } from "./Button.astro" never records a source at all. That breaks the new source-based component lookup for the most common import shape and can also duplicate entries once bindings_without_source() is used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_service/src/workspace/document/services/embedded_bindings.rs`
around lines 471 - 509, The code currently registers default imports twice and
omits source info for named imports; to fix, change the default-specifier branch
(clause.default_specifiers()) to call register_binding_with_source(...) using
import.source_text().ok()? instead of register_binding(...), and remove or
collapse the separate as_js_import_default_clause() branch to avoid duplicate
registration; also update the named-import handling logic (where individual
named specifiers are processed) to call register_binding_with_source(...) with
import.source_text().ok()? so every import form (default_specifiers,
as_js_import_default_clause/as_js_import_namespace_clause, and named specifiers)
records the source consistently.
🧹 Nitpick comments (1)
crates/biome_module_graph/src/module_graph.rs (1)

528-556: Avoid traversing static_import_paths twice here.

Lines 528-539 already add CSS imports from embedded <script> blocks, then Lines 543-556 add the same static imports again via static_import_paths.chain(...). That means duplicate CssClassSteps for the same stylesheet, which is wasted work and can leak duplicate path entries into diagnostics/traversal output.

♻️ One way to de-duplicate it
-            // 2b. CSS files imported from embedded <script> blocks
-            // (e.g., `import "./index.css"` in Astro frontmatter).
-            for import_path in html_info.static_import_paths.values() {
-                if let Some(path) = import_path.as_path()
-                    && let Some(css_info) = self.css_module_info_for_path(path)
-                {
-                    linked_steps.push(CssClassStep {
-                        css_path: path.to_path_buf(),
-                        css_classes: css_info.classes.clone(),
-                    });
-                }
-            }
-
-            // 3. CSS files imported via static imports from embedded scripts
-            //    (e.g., Astro frontmatter `import "./styles.css"`).
+            // 3. CSS files imported via embedded scripts
+            //    (e.g., Astro frontmatter `import "./styles.css"`).
             for import_path in html_info
                 .static_import_paths
                 .values()
                 .chain(html_info.dynamic_import_paths.values())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_module_graph/src/module_graph.rs` around lines 528 - 556,
Duplicate traversal: html_info.static_import_paths is iterated in the first loop
and again in the chained loop, producing duplicate CssClassStep entries; change
the second loop (which currently iterates
html_info.static_import_paths.values().chain(html_info.dynamic_import_paths.values()))
to only iterate html_info.dynamic_import_paths.values() (so use
dynamic_import_paths alone) and keep using self.css_module_info_for_path,
linked_steps.push(CssClassStep { css_path: path.to_path_buf(), css_classes:
css_info.classes.clone(), }) unchanged to preserve behavior for dynamic imports.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/dirty-lions-talk.md:
- Around line 5-14: Tighten the release note copy by rephrasing the clunky
sentence "When the cursor of the mouse is hovering an entity (variable, CSS
class, type, etc.), and the command CTRL + click is triggered" to something more
natural (e.g., "Ctrl+click an entity (variable, CSS class, type, etc.) to jump
to its definition") and finish/clarify the truncated bullet "Variables used in
HTML-ish files and defined in the same file or imported from another module
(JavaScript or HTML-ish)" so it reads as a complete, parallel item (e.g.,
"Variables used in HTML-ish files, defined in the same file or imported from
JavaScript or other HTML-ish files"). Make these edits in
.changeset/dirty-lions-talk.md replacing the exact phrases above to preserve
intent and parallel structure.

In `@crates/biome_lsp/src/handlers/navigation.rs`:
- Around line 60-69: The code currently fabricates a bogus Range::default() when
session.workspace.get_file_content(GetFileContentParams { ... }) fails, causing
a misleading jump; instead, on Err(_) do not produce a default range — propagate
the error or return None (so no Location is returned) and/or log the failure
with context; replace the Err(_) => Range::default() branch so it either returns
an Err with a descriptive message (bubbling the failure out of the surrounding
function) or yields no definition entry, rather than calling Range::default();
keep references to session.workspace.get_file_content, GetFileContentParams,
LineIndex::new, and to_proto::range when implementing the fix.
- Around line 8-15: The issue is a name collision between
biome_service::workspace::GotoDefinitionParams and the LSP GotoDefinitionParams
imported via tower_lsp_server::ls_types::*, causing
params.text_document_position_params (an LSP field) to be unavailable; fix by
aliasing the workspace types (e.g., import
biome_service::workspace::{GetFileContentParams as WsGetFileContentParams,
GotoDefinitionParams as WsGotoDefinitionParams}) and update usages in
goto_definition to refer to the LSP GotoDefinitionParams for the function
parameter and to WsGetFileContentParams/WsGotoDefinitionParams where you need
workspace types (also update the other occurrences that referenced the ambiguous
types around the later block that uses workspace types).

In `@crates/biome_lsp/src/server_goto.tests.rs`:
- Around line 263-515: Add tests covering the class/className → CSS definition
path: create new test functions in this file using the existing helpers
(goto_definition_single_file and goto_definition_cross_file) and assertions
(assert_definition, test_uri, file_uri, range, pos) that exercise (1) inline
<style> resolution in the same .astro/.html file (e.g. a component with <div
class="foo"> and a <style> block containing .foo { ... } and asserting the
definition range inside the style block) and (2) cross-file CSS resolution via
imports (e.g. an .astro/.jsx file with className="bar" importing a .css file
that defines .bar, or an imported .astro component with an inline style import)
making sure to set the same config used elsewhere ("html": {
"experimentalFullSupportEnabled": true }) where needed; name tests clearly like
goto_definition_css_inline_style and goto_definition_css_cross_file and use the
same pattern (tokio::test async fn, call helpers, then assert_definition) so
offsets and import-tree traversal for CSS are exercised.

In `@crates/biome_lsp/src/session.rs`:
- Around line 646-662: The current logic in can_register_goto_definition lets
any client with dynamic_registration enable go-to-definition regardless of the
feature toggle; change the boolean logic so the extension toggle gates
registration: read goto_definition_enabled() from extension_settings and only
allow registration when that is true and the client advertises dynamic
registration (i.e., compute the dynamic flag from
initialize_params.client_capabilities.text_document.definition.dynamic_registration
and return enabled && dynamic). Reference: can_register_goto_definition,
initialize_params, extension_settings, goto_definition_enabled.

In `@crates/biome_module_graph/src/css_module_info/mod.rs`:
- Around line 149-157: The classes map currently uses IndexMap<TokenText,
TextRange> which collapses repeated class selectors; change it to store all
ranges per class (e.g., IndexMap<TokenText, Vec<TextRange>> or another
collection) and update the code that populates it (the walker that records
CssClassSelector nodes) to push ranges into the vector instead of overwriting,
and update any call sites of the classes field (e.g., go-to-definition code) to
iterate/choose from the Vec<TextRange> rather than assuming a single TextRange
so callers can decide whether to return one or many locations.

In `@crates/biome_service/src/file_handlers/html/go_to.rs`:
- Around line 56-63: The hit-test for class names is inclusive of the end byte
so a caret on the following space still matches the previous class; in the loop
in go_to.rs that computes pos, start, end and checks relative_offset, change the
upper bound from inclusive to exclusive by replacing the condition
relative_offset >= start && relative_offset <= end with relative_offset >= start
&& relative_offset < end so DefinitionReference::CssClass only matches bytes
inside the class name.

In `@crates/biome_service/src/file_handlers/javascript.rs`:
- Around line 1670-1686: The cursor-in-class detection treats the class end as
inclusive, causing matches when the cursor is on the separating space or closing
quote; in the loop that iterates over text.split_ascii_whitespace() (using
variables relative_offset, pos, start, end, and class_name) change the
membership check to treat end as exclusive (i.e., require relative_offset >=
start && relative_offset < end) and keep end computed as start +
class_name.len() so the boundary at the character immediately after the class
does not count as inside the token.

In `@crates/biome_service/src/file_handlers/javascript/go_to.rs`:
- Around line 158-160: The HtmlComponent branch is passing the import specifier
`source` into `resolve_import_definition`, but that helper expects a local
binding name and looks up `static_imports`, so specifier paths (e.g.
"./Button.astro") never resolve; replace or augment the call in the
DefinitionReference::HtmlComponent arm to perform a specifier-based lookup
instead of the local-name lookup: consult the module graph/requests to map the
specifier `source` -> resolved module (or implement a new helper like
`resolve_definition_by_specifier` that queries the same index used for specifier
resolution), then return the appropriate DefinitionReference result; keep
`resolve_import_definition` for local-binding use and add/route HtmlComponent to
the new specifier resolver so tags backed by "./Button.astro" resolve correctly.

In `@crates/biome_service/src/workspace/server.rs`:
- Around line 2756-2777: The embedded-definition resolution currently calls
resolve_definition with each snippet's content_offset but doesn't record which
snippet produced the reference, so the first snippet that returns a location may
be unrelated; update the LocalEmbedded definition handling to carry the
originating snippet identity (e.g., file_source_index or content_offset) onto
the DefinitionRef when it's created and then use that stored origin when
building ResolveDefinitionParams in the block handling
definition_ref.is_embedded() (symbols: definition_ref, LocalEmbedded,
embedded_snippets, snippet.file_source_index(), snippet.content_offset(),
ResolveDefinitionParams, resolve_definition); ensure all places that construct
DefinitionRef for embedded bindings populate the origin field so
resolve_definition receives the correct offset from the actual originating
snippet.
- Around line 514-517: The code builds document services in open_file_internal()
when settings.is_linter_enabled() || settings.is_assist_enabled() ||
needs_document_services is true, but change_file() only recreates services based
on settings.is_linter_enabled() || settings.is_assist_enabled(), causing files
opened with only definition/navigation (needs_document_services) to lose their
semantic model after edits; fix by persisting the needs_document_services
decision into the file/workspace state when open_file_internal() runs (e.g., add
a flag like file.needs_document_services or store in the WorkspaceFile entry)
and update change_file() to consult that persisted flag (or the same composite
predicate) when deciding to rebuild document services so semantic models remain
available after edits.
- Around line 841-849: The current logic calls the host-language resolve_binding
and returns immediately even when it yields None, preventing the subsequent
embedded-snippet loop from running; change the block around
resolve_binding(ResolveBindingParams { parse, cursor_offset, services }) so you
only compute resolve_capabilities(&result, capabilities) and return Ok((result,
path.clone(), capabilities)) when result is Some(...) (i.e., a successful
binding), otherwise do not return and allow the snippet loop below to run and
attempt resolution inside embedded <script>/<style> regions; keep the same
variables (resolve_binding, ResolveBindingParams, resolve_capabilities, parse,
cursor_offset, services, path) to locate and adjust the logic.

---

Outside diff comments:
In `@crates/biome_service/src/workspace/document/services/embedded_bindings.rs`:
- Around line 471-509: The code currently registers default imports twice and
omits source info for named imports; to fix, change the default-specifier branch
(clause.default_specifiers()) to call register_binding_with_source(...) using
import.source_text().ok()? instead of register_binding(...), and remove or
collapse the separate as_js_import_default_clause() branch to avoid duplicate
registration; also update the named-import handling logic (where individual
named specifiers are processed) to call register_binding_with_source(...) with
import.source_text().ok()? so every import form (default_specifiers,
as_js_import_default_clause/as_js_import_namespace_clause, and named specifiers)
records the source consistently.

---

Nitpick comments:
In `@crates/biome_module_graph/src/module_graph.rs`:
- Around line 528-556: Duplicate traversal: html_info.static_import_paths is
iterated in the first loop and again in the chained loop, producing duplicate
CssClassStep entries; change the second loop (which currently iterates
html_info.static_import_paths.values().chain(html_info.dynamic_import_paths.values()))
to only iterate html_info.dynamic_import_paths.values() (so use
dynamic_import_paths alone) and keep using self.css_module_info_for_path,
linked_steps.push(CssClassStep { css_path: path.to_path_buf(), css_classes:
css_info.classes.clone(), }) unchanged to preserve behavior for dynamic imports.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1ba7e0f3-79c6-490c-9ed4-42c8a126ca5b

📥 Commits

Reviewing files that changed from the base of the PR and between 9c7c6eb and 55370a0.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
📒 Files selected for processing (62)
  • .changeset/dirty-lions-talk.md
  • crates/biome_cli/src/execute/migrate.rs
  • crates/biome_cli/src/runner/impls/process_file/format.rs
  • crates/biome_cli/src/runner/impls/process_file/lint_and_assist.rs
  • crates/biome_cli/src/runner/process_file.rs
  • crates/biome_css_formatter/tests/quick_test.rs
  • crates/biome_formatter_test/src/spec.rs
  • crates/biome_graphql_formatter/tests/quick_test.rs
  • crates/biome_grit_formatter/tests/quick_test.rs
  • crates/biome_html_analyze/src/lint/nursery/no_undeclared_classes.rs
  • crates/biome_html_formatter/tests/quick_test.rs
  • crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs
  • crates/biome_js_formatter/tests/quick_test.rs
  • crates/biome_json_formatter/tests/quick_test.rs
  • crates/biome_lsp/Cargo.toml
  • crates/biome_lsp/src/capabilities.rs
  • crates/biome_lsp/src/diagnostics.rs
  • crates/biome_lsp/src/extension_settings.rs
  • crates/biome_lsp/src/handlers.rs
  • crates/biome_lsp/src/handlers/navigation.rs
  • crates/biome_lsp/src/handlers/text_document.rs
  • crates/biome_lsp/src/lib.rs
  • crates/biome_lsp/src/server.rs
  • crates/biome_lsp/src/server.tests.rs
  • crates/biome_lsp/src/server_goto.tests.rs
  • crates/biome_lsp/src/server_test_utils.rs
  • crates/biome_lsp/src/session.rs
  • crates/biome_module_graph/src/css_module_info/mod.rs
  • crates/biome_module_graph/src/css_module_info/traverse.rs
  • crates/biome_module_graph/src/css_module_info/visitor.rs
  • crates/biome_module_graph/src/format_module_graph.rs
  • crates/biome_module_graph/src/html_module_info/mod.rs
  • crates/biome_module_graph/src/html_module_info/visitor.rs
  • crates/biome_module_graph/src/js_module_info.rs
  • crates/biome_module_graph/src/module_graph.rs
  • crates/biome_module_graph/tests/spec_tests.rs
  • crates/biome_service/Cargo.toml
  • crates/biome_service/src/diagnostics.rs
  • crates/biome_service/src/file_handlers/astro.rs
  • crates/biome_service/src/file_handlers/css.rs
  • crates/biome_service/src/file_handlers/graphql.rs
  • crates/biome_service/src/file_handlers/grit.rs
  • crates/biome_service/src/file_handlers/html.rs
  • crates/biome_service/src/file_handlers/html/go_to.rs
  • crates/biome_service/src/file_handlers/html/parse_embedded_nodes.tests.rs
  • crates/biome_service/src/file_handlers/javascript.rs
  • crates/biome_service/src/file_handlers/javascript/go_to.rs
  • crates/biome_service/src/file_handlers/json.rs
  • crates/biome_service/src/file_handlers/md.rs
  • crates/biome_service/src/file_handlers/mod.rs
  • crates/biome_service/src/file_handlers/svelte.rs
  • crates/biome_service/src/file_handlers/vue.rs
  • crates/biome_service/src/scanner/watcher.rs
  • crates/biome_service/src/scanner/workspace_scanner_bridge.tests.rs
  • crates/biome_service/src/scanner/workspace_watcher_bridge.tests.rs
  • crates/biome_service/src/settings.rs
  • crates/biome_service/src/workspace.rs
  • crates/biome_service/src/workspace.tests.rs
  • crates/biome_service/src/workspace/client.rs
  • crates/biome_service/src/workspace/document/services/embedded_bindings.rs
  • crates/biome_service/src/workspace/server.rs
  • crates/biome_service/src/workspace/server.tests.rs

Comment on lines +5 to +14
The Biome Language server now supports the "go-to definition" feature.

When the cursor of the mouse is hovering an entity (variable, CSS class, type, etc.), and the command <kbd>CTRL</kbd> + click is triggered, the editor jumps to where this entity is defined, if the language server can find it.

Here's what Biome is able to resolve:
- Variables and types used in JavaScript modules, defined in the same file or imported from another module.
- JSX Components used in JavaScript modules, defined in the same file or imported from another module.
- CSS classes used in JSX and HTML-ish files (Vue, Svelte and Astro), and defined in CSS files.
- Components used in HTML-ish files and defined in other HTML-ish.
- Variables used in HTML-ish files and defined in the same file or imported from another module (JavaScript or HTML-ish).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tighten the release note copy.

Line 7 is a bit clunky, and line 13 ends mid-sentence. A small rewrite would read more naturally.

Suggested wording
-When the cursor of the mouse is hovering an entity (variable, CSS class, type, etc.), and the command <kbd>CTRL</kbd> + click is triggered, the editor jumps to where this entity is defined, if the language server can find it.
+When hovering an entity (variable, CSS class, type, etc.) and pressing <kbd>CTRL</kbd> + click, the editor jumps to its definition if the language server can find it.
@@
-- Components used in HTML-ish files and defined in other HTML-ish.
+- Components used in HTML-ish files and defined in other HTML-ish files.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The Biome Language server now supports the "go-to definition" feature.
When the cursor of the mouse is hovering an entity (variable, CSS class, type, etc.), and the command <kbd>CTRL</kbd> + click is triggered, the editor jumps to where this entity is defined, if the language server can find it.
Here's what Biome is able to resolve:
- Variables and types used in JavaScript modules, defined in the same file or imported from another module.
- JSX Components used in JavaScript modules, defined in the same file or imported from another module.
- CSS classes used in JSX and HTML-ish files (Vue, Svelte and Astro), and defined in CSS files.
- Components used in HTML-ish files and defined in other HTML-ish.
- Variables used in HTML-ish files and defined in the same file or imported from another module (JavaScript or HTML-ish).
The Biome Language server now supports the "go-to definition" feature.
When hovering an entity (variable, CSS class, type, etc.) and pressing <kbd>CTRL</kbd> + click, the editor jumps to its definition if the language server can find it.
Here's what Biome is able to resolve:
- Variables and types used in JavaScript modules, defined in the same file or imported from another module.
- JSX Components used in JavaScript modules, defined in the same file or imported from another module.
- CSS classes used in JSX and HTML-ish files (Vue, Svelte and Astro), and defined in CSS files.
- Components used in HTML-ish files and defined in other HTML-ish files.
- Variables used in HTML-ish files and defined in the same file or imported from another module (JavaScript or HTML-ish).
🧰 Tools
🪛 LanguageTool

[typographical] ~7-~7: Usually, there’s no comma before “if”.
Context: ...or jumps to where this entity is defined, if the language server can find it. Here'...

(IF_NO_COMMA)


[style] ~9-~9: As a shorter alternative for ‘able to’, consider using “can”.
Context: ... server can find it. Here's what Biome is able to resolve: - Variables and types used in ...

(BE_ABLE_TO)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/dirty-lions-talk.md around lines 5 - 14, Tighten the release note
copy by rephrasing the clunky sentence "When the cursor of the mouse is hovering
an entity (variable, CSS class, type, etc.), and the command CTRL + click is
triggered" to something more natural (e.g., "Ctrl+click an entity (variable, CSS
class, type, etc.) to jump to its definition") and finish/clarify the truncated
bullet "Variables used in HTML-ish files and defined in the same file or
imported from another module (JavaScript or HTML-ish)" so it reads as a
complete, parallel item (e.g., "Variables used in HTML-ish files, defined in the
same file or imported from JavaScript or other HTML-ish files"). Make these
edits in .changeset/dirty-lions-talk.md replacing the exact phrases above to
preserve intent and parallel structure.

Comment thread crates/biome_lsp/src/handlers/navigation.rs Outdated
Comment thread crates/biome_lsp/src/handlers/navigation.rs Outdated
Comment thread crates/biome_lsp/src/server_goto.tests.rs
Comment thread crates/biome_lsp/src/session.rs
Comment thread crates/biome_service/src/file_handlers/javascript.rs Outdated
Comment thread crates/biome_service/src/file_handlers/javascript/go_to.rs Outdated
Comment thread crates/biome_service/src/workspace/server.rs Outdated
Comment thread crates/biome_service/src/workspace/server.rs
Comment thread crates/biome_service/src/workspace/server.rs Outdated
@ematipico
Copy link
Copy Markdown
Member Author

Ok I'll need to send more changes. The bot review was really helpful. Getting there!

@ematipico ematipico force-pushed the feat/go-to-definition branch from 7c2b242 to b98fef0 Compare May 1, 2026 08:43
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/biome_module_graph/src/module_graph.rs (1)

532-560: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid pushing embedded-script CSS twice.

static_import_paths are appended in the new 2b loop and again in the loop below, so HTML-like files with import "./styles.css" can emit duplicate CSS steps and duplicate go-to-definition targets.

Suggested trim
-            // 3. CSS files imported via static imports from embedded scripts
-            //    (e.g., Astro frontmatter `import "./styles.css"`).
-            for import_path in html_info
-                .static_import_paths
-                .values()
-                .chain(html_info.dynamic_import_paths.values())
+            // 3. CSS files imported dynamically from embedded scripts.
+            for import_path in html_info.dynamic_import_paths.values()
             {
                 if let Some(path) = import_path.as_path()
                     && let Some(css_info) = self.css_module_info_for_path(path)
                 {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_module_graph/src/module_graph.rs` around lines 532 - 560, The
code currently iterates html_info.static_import_paths twice (once in the 2b loop
and again in the 3rd loop), causing duplicate CssClassStep entries; update the
3rd loop to only iterate html_info.dynamic_import_paths.values() (remove the
.values().chain(html_info.dynamic_import_paths.values()) usage) so that static
imports are only handled in the 2b branch; keep the existing guard using
css_module_info_for_path and the linked_steps.push(CssClassStep { css_path: ...,
css_classes: ... }) logic unchanged.
crates/biome_lsp/src/handlers/text_document.rs (1)

223-229: ⚠️ Potential issue | 🟠 Major

Confirm editor_features regression: go-to-definition will disappear after first edit/save.

did_open stores editor_features via change_file, but did_change and did_save pass None. The change_file method (workspace/server.rs:1917) uses editor_features.unwrap_or_default(), which replaces None with empty EditorFeatures. Since the Document struct stores no editor_features field, there's nothing to preserve—the feature is simply lost on the next file update.

Pass the stored editor_features from the document, or update did_change/did_save to send the existing features instead of None.

Also applies to: 255–261

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_lsp/src/handlers/text_document.rs` around lines 223 - 229, The
code is dropping editor_features by passing None to
session.workspace.change_file in handlers for did_change/did_save (and similarly
at lines 255–261); update those calls to pass the document's stored
editor_features instead of None (or retrieve existing features from the Document
before constructing ChangeFileParams) so ChangeFileParams.editor_features is
populated rather than defaulted via editor_features.unwrap_or_default();
reference session.workspace.change_file, ChangeFileParams, Document, and the
did_open/did_change/did_save handlers to locate and fix the calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_lsp/src/handlers/navigation.rs`:
- Around line 72-85: The multi-match branch currently uses flat_map over
definition.matches which swallows errors from to_location; change the iterator
to map each (definition_path, range) to the Result returned by to_location, then
collect that iterator into a Result<Vec<_>, E> (e.g.
.map(...).collect::<Result<Vec<_>, _>>() ?) and propagate the error with ? so
failures mirror the single-match branch behavior; after successful collection,
flatten or extend the resulting locations into the final locations Vec as
needed. Reference: to_location, definition.matches, locations.

In `@crates/biome_service/src/file_handlers/css/go_to.rs`:
- Around line 29-36: The selector matching currently uses
selector.resolved().normalize().contains(class_name) which does substring
matching; change it to iterate selector.resolved().tokens() and check for an
exact token pair where the kind is CssSyntaxKind::DOT and the token text equals
class_name (match TokenText or string value exactly) before computing range and
calling range.add(offset) and result.store(BiomePath::new(path), range); update
the matching logic inside the loop over model.rules()/rule.selectors() to use
tokens() rather than normalize().contains(...), and add a regression test that
verifies a lookup for ".foo" does not match a selector like ".foobar".

In `@crates/biome_service/src/file_handlers/javascript/go_to.rs`:
- Around line 26-35: The code currently bails out early by calling
params.services.as_js_services()?.semantic_model.as_ref()? which returns None
when the JS semantic model is missing, preventing the JSX className/class path
from running; instead keep the JSX class-name path syntax-only by making
semantic_model optional (do not use the ? that returns early) — e.g. obtain
semantic_model as an Option (use as_js_services().and_then(|s|
s.semantic_model.as_ref()) or delay fetching the model) and move or check
semantic_model only when needed for other branches, so
extract_css_class_at_offset(jsx_attribute, params.cursor_offset) and the
JsxAttribute -> DefinitionReference::CssClass branch run even if semantic_model
is None.

In `@crates/biome_service/src/workspace/server.rs`:
- Around line 867-895: The loop currently probes the first snippet with a
resolve_binding and returns immediately, even when the cursor isn't in that
snippet or no binding is found; update the logic in the embedded_snippets
iteration so you first check snippet bounds/ownership (use
snippet.content_offset() and snippet length/extent) to ensure cursor_offset lies
inside the snippet before computing local_cursor and calling resolve, call
resolve(ResolveBindingParams { ... }) only for the snippet that contains the
cursor, treat a None/empty resolve result as "not found" and continue to the
next snippet instead of returning, and only adjust the
DefinitionReference::Local range (range + snippet_offset) and return
Ok((adjusted, path.clone(), capabilities)) when a concrete binding/result was
actually found; otherwise let the loop continue until all embedded_snippets are
exhausted.

---

Outside diff comments:
In `@crates/biome_lsp/src/handlers/text_document.rs`:
- Around line 223-229: The code is dropping editor_features by passing None to
session.workspace.change_file in handlers for did_change/did_save (and similarly
at lines 255–261); update those calls to pass the document's stored
editor_features instead of None (or retrieve existing features from the Document
before constructing ChangeFileParams) so ChangeFileParams.editor_features is
populated rather than defaulted via editor_features.unwrap_or_default();
reference session.workspace.change_file, ChangeFileParams, Document, and the
did_open/did_change/did_save handlers to locate and fix the calls.

In `@crates/biome_module_graph/src/module_graph.rs`:
- Around line 532-560: The code currently iterates html_info.static_import_paths
twice (once in the 2b loop and again in the 3rd loop), causing duplicate
CssClassStep entries; update the 3rd loop to only iterate
html_info.dynamic_import_paths.values() (remove the
.values().chain(html_info.dynamic_import_paths.values()) usage) so that static
imports are only handled in the 2b branch; keep the existing guard using
css_module_info_for_path and the linked_steps.push(CssClassStep { css_path: ...,
css_classes: ... }) logic unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0cd14ea5-54c5-46fc-88ba-80ceb6f6e9be

📥 Commits

Reviewing files that changed from the base of the PR and between 789c291 and 7c2b242.

📒 Files selected for processing (36)
  • crates/biome_cli/src/execute/migrate.rs
  • crates/biome_cli/src/runner/impls/process_file/format.rs
  • crates/biome_cli/src/runner/impls/process_file/lint_and_assist.rs
  • crates/biome_cli/src/runner/process_file.rs
  • crates/biome_css_formatter/tests/quick_test.rs
  • crates/biome_formatter_test/src/spec.rs
  • crates/biome_graphql_formatter/tests/quick_test.rs
  • crates/biome_grit_formatter/tests/quick_test.rs
  • crates/biome_html_formatter/tests/quick_test.rs
  • crates/biome_js_formatter/tests/quick_test.rs
  • crates/biome_json_formatter/tests/quick_test.rs
  • crates/biome_lsp/src/extension_settings.rs
  • crates/biome_lsp/src/handlers/navigation.rs
  • crates/biome_lsp/src/handlers/text_document.rs
  • crates/biome_lsp/src/server.tests.rs
  • crates/biome_lsp/src/server_goto.tests.rs
  • crates/biome_lsp/src/session.rs
  • crates/biome_module_graph/src/module_graph.rs
  • crates/biome_service/Cargo.toml
  • crates/biome_service/src/file_handlers/css.rs
  • crates/biome_service/src/file_handlers/css/go_to.rs
  • crates/biome_service/src/file_handlers/html/go_to.rs
  • crates/biome_service/src/file_handlers/html/parse_embedded_nodes.rs
  • crates/biome_service/src/file_handlers/html/parse_embedded_nodes.tests.rs
  • crates/biome_service/src/file_handlers/javascript.rs
  • crates/biome_service/src/file_handlers/javascript/go_to.rs
  • crates/biome_service/src/file_handlers/mod.rs
  • crates/biome_service/src/scanner/workspace_scanner_bridge.tests.rs
  • crates/biome_service/src/scanner/workspace_watcher_bridge.tests.rs
  • crates/biome_service/src/settings.rs
  • crates/biome_service/src/workspace.rs
  • crates/biome_service/src/workspace.tests.rs
  • crates/biome_service/src/workspace/document/mod.rs
  • crates/biome_service/src/workspace/server.rs
  • crates/biome_service/src/workspace/server.tests.rs
  • crates/biome_test_utils/src/lib.rs
✅ Files skipped from review due to trivial changes (11)
  • crates/biome_service/src/file_handlers/html/parse_embedded_nodes.tests.rs
  • crates/biome_css_formatter/tests/quick_test.rs
  • crates/biome_cli/src/runner/impls/process_file/lint_and_assist.rs
  • crates/biome_json_formatter/tests/quick_test.rs
  • crates/biome_service/src/workspace/document/mod.rs
  • crates/biome_html_formatter/tests/quick_test.rs
  • crates/biome_graphql_formatter/tests/quick_test.rs
  • crates/biome_service/src/scanner/workspace_watcher_bridge.tests.rs
  • crates/biome_cli/src/runner/process_file.rs
  • crates/biome_js_formatter/tests/quick_test.rs
  • crates/biome_service/src/scanner/workspace_scanner_bridge.tests.rs
🚧 Files skipped from review as they are similar to previous changes (9)
  • crates/biome_cli/src/runner/impls/process_file/format.rs
  • crates/biome_formatter_test/src/spec.rs
  • crates/biome_grit_formatter/tests/quick_test.rs
  • crates/biome_cli/src/execute/migrate.rs
  • crates/biome_lsp/src/extension_settings.rs
  • crates/biome_lsp/src/session.rs
  • crates/biome_service/src/file_handlers/html/go_to.rs
  • crates/biome_lsp/src/server_goto.tests.rs
  • crates/biome_service/src/workspace/server.tests.rs

Comment thread crates/biome_lsp/src/handlers/navigation.rs Outdated
Comment thread crates/biome_service/src/file_handlers/css/go_to.rs Outdated
Comment thread crates/biome_service/src/file_handlers/javascript/go_to.rs Outdated
Comment thread crates/biome_service/src/workspace/server.rs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
.changeset/dirty-lions-talk.md (1)

5-14: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Please tighten and complete the release note copy.

Line 7 reads clunky, and Line 13 ends abruptly (“other HTML-ish”). The entry is also longer than the 1–3 sentence changeset guidance.

✍️ Suggested rewrite
-The Biome Language server now supports the "go-to definition" feature.
-
-When the cursor of the mouse is hovering an entity (variable, CSS class, type, etc.), and the command <kbd>CTRL</kbd> + click is triggered, the editor jumps to where this entity is defined, if the language server can find it.
-
-Here's what Biome is able to resolve:
-- Variables and types used in JavaScript modules, defined in the same file or imported from another module.
-- JSX Components used in JavaScript modules, defined in the same file or imported from another module.
-- CSS classes used in JSX and HTML-ish files (Vue, Svelte and Astro), and defined in CSS files.
-- Components used in HTML-ish files and defined in other HTML-ish.
-- Variables used in HTML-ish files and defined in the same file or imported from another module (JavaScript or HTML-ish).
+Biome Language Server gained support for `go-to definition`.
+When hovering an entity (variable, CSS class, type, etc.) and pressing <kbd>CTRL</kbd> + click, the editor now jumps to its definition when the language server can resolve it.
+Biome now resolves definitions for JavaScript and HTML-ish variables/types/components (including imports), and CSS classes used in JSX/HTML-ish files and defined in CSS files.

As per coding guidelines: “Write changeset descriptions concisely and clearly (1-3 sentences) … End every sentence in changeset descriptions with a full stop (period).”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/dirty-lions-talk.md around lines 5 - 14, Replace the long
bulleted explanation with a concise 1–3 sentence changeset that ends every
sentence with a period; rewrite the CTRL + click sentence (currently "When the
cursor of the mouse is hovering an entity...") to a shorter, clearer sentence
like "Use Ctrl+click to jump to an entity's definition when available." and fix
the incomplete phrase "other HTML-ish" to "other HTML-ish files." Ensure you
keep a brief list of supported resolutions (variables/types, JSX components, CSS
classes, and components/variables in HTML-ish files) compressed into one
sentence or two, and terminate all sentences with periods.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.changeset/dirty-lions-talk.md:
- Around line 5-14: Replace the long bulleted explanation with a concise 1–3
sentence changeset that ends every sentence with a period; rewrite the CTRL +
click sentence (currently "When the cursor of the mouse is hovering an
entity...") to a shorter, clearer sentence like "Use Ctrl+click to jump to an
entity's definition when available." and fix the incomplete phrase "other
HTML-ish" to "other HTML-ish files." Ensure you keep a brief list of supported
resolutions (variables/types, JSX components, CSS classes, and
components/variables in HTML-ish files) compressed into one sentence or two, and
terminate all sentences with periods.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 96d0ad24-6496-4cf8-8f97-01df365dd39e

📥 Commits

Reviewing files that changed from the base of the PR and between 7c2b242 and b98fef0.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock and included by **
📒 Files selected for processing (12)
  • .changeset/dirty-lions-talk.md
  • crates/biome_cli/src/execute/migrate.rs
  • crates/biome_cli/src/runner/impls/process_file/format.rs
  • crates/biome_cli/src/runner/impls/process_file/lint_and_assist.rs
  • crates/biome_cli/src/runner/process_file.rs
  • crates/biome_css_formatter/tests/quick_test.rs
  • crates/biome_formatter_test/src/spec.rs
  • crates/biome_graphql_formatter/tests/quick_test.rs
  • crates/biome_grit_formatter/tests/quick_test.rs
  • crates/biome_html_analyze/src/lint/nursery/no_undeclared_classes.rs
  • crates/biome_html_formatter/tests/quick_test.rs
  • crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs
✅ Files skipped from review due to trivial changes (7)
  • crates/biome_cli/src/runner/process_file.rs
  • crates/biome_cli/src/runner/impls/process_file/lint_and_assist.rs
  • crates/biome_html_formatter/tests/quick_test.rs
  • crates/biome_graphql_formatter/tests/quick_test.rs
  • crates/biome_html_analyze/src/lint/nursery/no_undeclared_classes.rs
  • crates/biome_css_formatter/tests/quick_test.rs
  • crates/biome_formatter_test/src/spec.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/biome_cli/src/runner/impls/process_file/format.rs
  • crates/biome_grit_formatter/tests/quick_test.rs
  • crates/biome_cli/src/execute/migrate.rs

@ematipico ematipico added this to the Biome v2.5 milestone May 1, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/biome_module_graph/src/module_graph.rs (1)

532-560: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid collecting embedded-script CSS imports twice.

The new static_import_paths pass is redundant with the loop just below, which already walks static_import_paths and dynamic_import_paths. Static CSS imports from embedded <script> blocks therefore land in linked_steps twice, which can bubble up as duplicate available classes and duplicate go-to targets. One pass is plenty.

✂️ Minimal tidy-up
-            // 2b. CSS files imported from embedded <script> blocks
-            // (e.g., `import "./index.css"` in Astro frontmatter).
-            for import_path in html_info.static_import_paths.values() {
-                if let Some(path) = import_path.as_path()
-                    && let Some(css_info) = self.css_module_info_for_path(path)
-                {
-                    linked_steps.push(CssClassStep {
-                        css_path: path.to_path_buf(),
-                        css_classes: css_info.classes.clone(),
-                    });
-                }
-            }
-
             // 3. CSS files imported via static imports from embedded scripts
             //    (e.g., Astro frontmatter `import "./styles.css"`).
             for import_path in html_info
                 .static_import_paths
                 .values()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_module_graph/src/module_graph.rs` around lines 532 - 560, The
first loop that iterates html_info.static_import_paths and pushes CssClassStep
entries duplicates work done by the subsequent loop that walks
html_info.static_import_paths.chain(html_info.dynamic_import_paths); remove the
earlier loop (the one labeled "2b. CSS files imported from embedded <script>
blocks") so css_module_info_for_path results are only collected once into
linked_steps (keep the combined loop that uses
static_import_paths.chain(dynamic_import_paths) and ensure it still pushes
CssClassStep with css_path and css_classes as before).
♻️ Duplicate comments (1)
crates/biome_service/src/workspace/server.rs (1)

867-874: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Check snippet ownership before offset arithmetic.

Line 869 subtracts snippet_offset before Line 872 proves the cursor is inside that snippet. In multi-embed files, a cursor before the current snippet can underflow here and send resolution off into the weeds. Tiny bug, surprisingly pointy.

Possible fix
         for snippet in embedded_snippets {
-            let snippet_offset = snippet.content_offset();
-            let local_cursor = cursor_offset - snippet_offset;
-
             let snippet_range = snippet.content_range();
             if cursor_offset < snippet_range.start() || cursor_offset >= snippet_range.end() {
                 continue;
             }
+
+            let snippet_offset = snippet.content_offset();
+            let local_cursor = cursor_offset - snippet_offset;
 
             let Some(file_source) = self.get_source(snippet.file_source_index()) else {
                 continue;
             };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_service/src/workspace/server.rs` around lines 867 - 874, The
code performs subtraction of snippet.content_offset() into local_cursor before
verifying the cursor lies within the snippet, which can underflow for earlier
snippets; fix by moving the calculation of local_cursor (or using checked_sub)
to after the range check that uses snippet.content_range(), or use
snippet.content_offset().checked_sub(...) /
cursor_offset.checked_sub(snippet_offset) and continue if None, ensuring you
reference embedded_snippets, snippet.content_offset(), snippet.content_range(),
local_cursor and cursor_offset when applying the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_lsp/src/server_goto.tests.rs`:
- Around line 61-85: The test currently calls .context("goto_definition returned
None")? after awaiting server.request which converts
Option<lsp::GotoDefinitionResponse> into a Result<lsp::GotoDefinitionResponse>
and makes res non-optional; remove the .context("goto_definition returned
None")? suffix and simply use .await? so that res retains the
Option<lsp::GotoDefinitionResponse> type expected by the function and tests
(locate the call to server.request(...) for "textDocument/definition" /
"goto_definition" and remove the .context(...) chain).

In `@crates/biome_module_graph/src/module_graph.rs`:
- Around line 315-344: find_css_class_definition currently only checks CSS files
directly reached from the JS/HTML traversals and ignores transitive
CssModuleInfo.imports, so classes in CSS files imported by other CSS files are
missed; update the lookup to recursively follow CssModuleInfo.imports (e.g.,
add/extend a helper such as traverse_css_imports or augment
traverse_import_tree_for_classes/for_html_classes) and maintain a visited
HashSet to prevent cycles, collecting matching class ranges from each transitive
css_path; also add a regression test that exercises a chain like App.tsx ->
app.css -> components.css and verifies go-to-definition finds classes in
components.css.

In `@crates/biome_service/src/file_handlers/javascript/go_to.rs`:
- Around line 143-186: resolve_definition currently always returns Some(result)
even when no definitions were stored by resolve_import_definition,
resolve_dynamic_import_definition, or the LocalEmbedded branch without an
offset; change it to return None when the resulting GoToDefinitionResult is
empty. After the match block in resolve_definition, check whether the
accumulated result is empty (e.g. result.is_empty() or
result.matches.is_empty()) and return None in that case, otherwise return
Some(result); keep all existing calls to result.store and the
DefinitionReference branches unchanged.

In `@crates/biome_service/src/workspace/server.rs`:
- Around line 2751-2763: The code currently adds GotoDefinition to the
capability and only uses params.enabled in the capability calculation and
needs_document_services() path, but callers can still run document services when
params.enabled is false if lint or assist are enabled; to fix, short-circuit
early by returning Ok(None) when params.enabled is false (i.e., add a guard
checking params.enabled before computing capability or evaluating
has_document_services), or incorporate params.enabled into the
has_document_services check; update the logic around params.enabled,
EditorFeatures, and the call to self.settings_handle_with_features(&settings,
None, capability) (and the subsequent has_document_services =
settings.needs_document_services() || settings.as_ref().is_linter_enabled() ||
settings.as_ref().is_assist_enabled()) so that when params.enabled == false the
function exits without running go-to-definition or other document-resolution
code.

---

Outside diff comments:
In `@crates/biome_module_graph/src/module_graph.rs`:
- Around line 532-560: The first loop that iterates
html_info.static_import_paths and pushes CssClassStep entries duplicates work
done by the subsequent loop that walks
html_info.static_import_paths.chain(html_info.dynamic_import_paths); remove the
earlier loop (the one labeled "2b. CSS files imported from embedded <script>
blocks") so css_module_info_for_path results are only collected once into
linked_steps (keep the combined loop that uses
static_import_paths.chain(dynamic_import_paths) and ensure it still pushes
CssClassStep with css_path and css_classes as before).

---

Duplicate comments:
In `@crates/biome_service/src/workspace/server.rs`:
- Around line 867-874: The code performs subtraction of snippet.content_offset()
into local_cursor before verifying the cursor lies within the snippet, which can
underflow for earlier snippets; fix by moving the calculation of local_cursor
(or using checked_sub) to after the range check that uses
snippet.content_range(), or use snippet.content_offset().checked_sub(...) /
cursor_offset.checked_sub(snippet_offset) and continue if None, ensuring you
reference embedded_snippets, snippet.content_offset(), snippet.content_range(),
local_cursor and cursor_offset when applying the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7c71ef3e-fdd4-4f7c-8b3e-29f03d457d99

📥 Commits

Reviewing files that changed from the base of the PR and between b98fef0 and 270b88c.

⛔ Files ignored due to path filters (1)
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
📒 Files selected for processing (16)
  • crates/biome_html_analyze/src/lint/nursery/no_undeclared_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs
  • crates/biome_lsp/src/handlers/navigation.rs
  • crates/biome_lsp/src/server_goto.tests.rs
  • crates/biome_module_graph/src/css_module_info/mod.rs
  • crates/biome_module_graph/src/css_module_info/traverse.rs
  • crates/biome_module_graph/src/css_module_info/visitor.rs
  • crates/biome_module_graph/src/format_module_graph.rs
  • crates/biome_module_graph/src/module_graph.rs
  • crates/biome_module_graph/tests/spec_tests.rs
  • crates/biome_service/src/file_handlers/css/go_to.rs
  • crates/biome_service/src/file_handlers/javascript/go_to.rs
  • crates/biome_service/src/settings.rs
  • crates/biome_service/src/workspace.rs
  • crates/biome_service/src/workspace/server.rs
  • crates/biome_service/src/workspace/server.tests.rs
✅ Files skipped from review due to trivial changes (2)
  • crates/biome_html_analyze/src/lint/nursery/no_undeclared_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_undeclared_classes.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_service/src/settings.rs

Comment thread crates/biome_lsp/src/server_goto.tests.rs
Comment thread crates/biome_module_graph/src/module_graph.rs
Comment thread crates/biome_service/src/file_handlers/javascript/go_to.rs
Comment thread crates/biome_service/src/workspace/server.rs
Copy link
Copy Markdown
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

I just tried this on my vue project.

It worked for:

  • Components used in the template section, where the component was imported with a relative import like ./Foo.vue
  • All cases where a js variable was declared in the same contiguous js snippet (so the <script> section in a vue component, or a whole .ts file)
  • CSS Classes used in the template section that are defined in the same file's <style> section.

It did not work for these cases:

  • All cases where the definition was in another package and the file was a .vue component.
  • Components used in the template section, where the component was imported with a path alias like @/components/Foo.vue (seems more like a bug in the module graph)
  • variables used in embedded expressions like v-if, v-else-if, or interpolation.

I'm also concerned that this will override or conflict with the functionality of existing tools that already implement this. Does the lsp spec say anything about it? We should probably provide a way to disable it.

@ematipico
Copy link
Copy Markdown
Member Author

I'm also concerned that this will override or conflict with the functionality of existing tools that already implement this. Does the lsp spec say anything about it?

The client is in charge of it, there's no override, the signals are joined together. The client shows multiple definitions and the user decides which one to follow.

We should probably provide a way to disable it.

It's already there and implemented

@dyc3
Copy link
Copy Markdown
Contributor

dyc3 commented May 4, 2026

Ah ok, cool. What about the rest of the stuff I mentioned? Or do we want to defer it to another PR

https://github.com/dyc3/opentogethertube is the repo i tested on.

@ematipico
Copy link
Copy Markdown
Member Author

I'll look into those. First should be easy, second might be a bug, second probably to defer to later

@ematipico ematipico force-pushed the feat/go-to-definition branch from 7f24c7f to 875123c Compare May 8, 2026 07:45
@ematipico
Copy link
Copy Markdown
Member Author

@dyc3 Addressed 1 and 2, which belonged to the same bug: when the extension setting was on, we didn't enable the module graph, hence 1 wasn't properly resolved (plus some other bug). Now that the module graph is enabled, 2 should work now.

@ematipico ematipico merged commit 894f3fb into next May 8, 2026
36 of 37 checks passed
@ematipico ematipico deleted the feat/go-to-definition branch May 8, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Formatter Area: formatter A-Linter Area: linter A-LSP Area: language server protocol A-Project Area: project L-CSS Language: CSS and super languages L-Grit Language: GritQL L-HTML Language: HTML and super languages L-JavaScript Language: JavaScript and super languages L-JSON Language: JSON and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants