Skip to content

Commit 00d64ae

Browse files
committed
Fix code lens navigation for non-VS Code editors
1 parent 39f6257 commit 00d64ae

3 files changed

Lines changed: 49 additions & 28 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
### Fixed
1515

16+
- **Code lens navigation.** Code lenses now work in Zed, Neovim, Emacs, and other editors. Previously the click command used a VS Code-specific API that other editors ignored.
1617
- **`@mixin` with union types.** `@mixin Foo|Bar` now correctly exposes members from all classes in the union. Previously only single-class mixins were recognized.
1718
- **`throw new` completion no longer offers non-instantiable types.** Interfaces, abstract classes, traits, and enums are now filtered out, matching the behavior of `new` completion. The `throw new` path also now filters to Throwable descendants only.
1819
- **Unified class name completion architecture.** `throw new` and `catch()` completion now use the same `build_class_name_completions` pipeline as `new`, `extends`, `implements`, etc. `throw new` uses a `ThrowNew` context (instantiable + Throwable) and `catch()`/`@throws` uses a `Catch` context (class or interface + Throwable). This gives both contexts the same affinity scoring, FQN shortening via use-map, namespace segment drill-down, deprecation flags, and consistent filtering. The separate `build_catch_class_name_completions` function has been removed.

src/code_lens.rs

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -70,30 +70,16 @@ impl Backend {
7070
let icon = if proto.is_interface { "◆" } else { "↑" };
7171
let title = format!("{} {}::{}", icon, proto.ancestor_name, method.name);
7272

73-
// Build a URI with a fragment encoding the target
74-
// line and column so that `vscode.open` jumps to the
75-
// right position. This avoids the `instanceof`
76-
// constraint errors that `editor.action.goToLocations`
77-
// and `editor.action.showReferences` trigger when
78-
// called from an LSP server without a companion
79-
// extension to convert plain JSON into VS Code class
80-
// instances.
81-
let fragment = format!(
82-
"L{},{}",
83-
proto.position.line + 1,
84-
proto.position.character + 1
85-
);
86-
let mut target_uri: Url = match proto.file_uri.parse() {
73+
let target_uri: Url = match proto.file_uri.parse() {
8774
Ok(u) => u,
8875
Err(_) => continue,
8976
};
90-
target_uri.set_fragment(Some(&fragment));
9177

92-
let command = Command {
78+
let command = self.build_code_lens_command(
9379
title,
94-
command: "vscode.open".to_string(),
95-
arguments: Some(vec![serde_json::json!(target_uri)]),
96-
};
80+
target_uri,
81+
proto.position,
82+
);
9783

9884
lenses.push(CodeLens {
9985
range,
@@ -322,6 +308,45 @@ impl Backend {
322308
None
323309
}
324310

311+
/// Build the LSP `Command` for a code lens that navigates to a target
312+
/// location.
313+
///
314+
/// Uses `editor.action.showReferences` (widely supported) by default,
315+
/// and `vscode.open` when connected to a VS Code client.
316+
fn build_code_lens_command(&self, title: String, uri: Url, position: Position) -> Command {
317+
let client = self.client_name.lock();
318+
if client.contains("Visual Studio Code") {
319+
// VS Code: use vscode.open with a fragment for direct navigation.
320+
let fragment = format!("L{},{}", position.line + 1, position.character + 1);
321+
let mut target_uri = uri;
322+
target_uri.set_fragment(Some(&fragment));
323+
Command {
324+
title,
325+
command: "vscode.open".to_string(),
326+
arguments: Some(vec![serde_json::json!(target_uri)]),
327+
}
328+
} else {
329+
// All other editors: use editor.action.showReferences which is
330+
// handled by most LSP clients (Zed, Neovim, Emacs, etc.).
331+
let location = Location {
332+
uri: uri.clone(),
333+
range: Range {
334+
start: position,
335+
end: position,
336+
},
337+
};
338+
Command {
339+
title,
340+
command: "editor.action.showReferences".to_string(),
341+
arguments: Some(vec![
342+
serde_json::json!(uri),
343+
serde_json::json!(position),
344+
serde_json::json!([location]),
345+
]),
346+
}
347+
}
348+
}
349+
325350
/// Build a `Prototype` by locating the method's position in the
326351
/// ancestor's source file.
327352
fn build_prototype(

tests/integration/code_lens.rs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ class Handler extends Base {
301301
// ─── Code Lens Command ─────────────────────────────────────────────────────
302302

303303
#[test]
304-
fn lens_command_uses_vscode_open() {
304+
fn lens_command_uses_show_references_by_default() {
305305
let backend = create_test_backend();
306306
let content = r#"<?php
307307
class Parent_ {
@@ -317,16 +317,11 @@ class Child extends Parent_ {
317317

318318
assert_eq!(lenses.len(), 1);
319319
let cmd = lenses[0].command.as_ref().unwrap();
320-
assert_eq!(cmd.command, "vscode.open");
320+
assert_eq!(cmd.command, "editor.action.showReferences");
321321
assert!(cmd.arguments.is_some());
322322
let args = cmd.arguments.as_ref().unwrap();
323-
// Should have 1 argument: the URI with a fragment encoding the position
324-
assert_eq!(args.len(), 1);
325-
let uri_str = args[0].as_str().unwrap();
326-
assert!(
327-
uri_str.contains("#L"),
328-
"URI should contain a #L fragment for the target position, got: {uri_str}"
329-
);
323+
// Should have 3 arguments: uri, position, locations[]
324+
assert_eq!(args.len(), 3);
330325
}
331326

332327
// ─── Multiple Interfaces ────────────────────────────────────────────────────

0 commit comments

Comments
 (0)