diff --git a/README.ja.md b/README.ja.md index 8270f561d3a..2825bd982eb 100644 --- a/README.ja.md +++ b/README.ja.md @@ -80,7 +80,7 @@ VS Codeのフォーク。本家にはないUI/UX改善を追加した(してい | `coderm.workbench.editor.separateTerminalEditors` | `boolean` | `true` | ターミナルエディタとテキストエディタを同じグループに混在させない。Quick Open等のデフォルトの開く操作で、既存の同種グループ(なければ新規)に振り分ける | | `coderm.workbench.editor.singleTerminalEditorPerGroup` | `boolean` | `true` | 1つのエディタグループにターミナルエディタは1つだけ。新規ターミナルは空きグループ(なければ新規)に開き、既存ターミナルグループにタブを追加しない | | `coderm.workbench.editor.disableGroupLock` | `boolean` | `true` | エディタグループのロック機能を完全に無効化。グループは自動・手動を問わずロックできず、常にロック解除状態で動作 | -| `coderm.languageHost.enabled` | `boolean` | `false` | (実験的)ネイティブ(Rust)Language Host を有効化。設定した言語で tree-sitter ベースの documentSymbol・foldingRange・hover・definition を提供(Phase 3) | +| `coderm.languageHost.enabled` | `boolean` | `false` | (実験的)ネイティブ(Rust)Language Host を有効化。設定した言語で tree-sitter ベースの documentSymbol・foldingRange・hover・definition・references を提供(Phase 4) | | `coderm.languageHost.languages` | `array` | `[]` | (実験的)ネイティブ Host が扱う言語 ID(例: "typescript", "tsx")。空の場合は機能無効 | --- diff --git a/README.md b/README.md index 0a923a61780..4c89888ac1f 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Settings unique to Coderm that are not available in upstream VS Code. | `coderm.workbench.editor.separateTerminalEditors` | `boolean` | `true` | Never mix terminal and text editors in one group; Quick Open and other default open paths route to an existing same-type group (or a new one) instead of mixing | | `coderm.workbench.editor.singleTerminalEditorPerGroup` | `boolean` | `true` | Limit each editor group to a single terminal editor; opening a new terminal routes to an empty group (or creates one) instead of adding a tab to an existing terminal group | | `coderm.workbench.editor.disableGroupLock` | `boolean` | `true` | Completely disable the editor group lock feature — groups can never be locked (automatically or manually) and always behave as unlocked | -| `coderm.languageHost.enabled` | `boolean` | `false` | _(experimental)_ Enable the native (Rust) Language Host. Provides tree-sitter-backed documentSymbol, foldingRange, hover, and definition for the configured languages (Phase 3) | +| `coderm.languageHost.enabled` | `boolean` | `false` | _(experimental)_ Enable the native (Rust) Language Host. Provides tree-sitter-backed documentSymbol, foldingRange, hover, definition, and references for the configured languages (Phase 4) | | `coderm.languageHost.languages` | `array` | `[]` | _(experimental)_ Language IDs handled by the native host (e.g. "typescript", "tsx"). Empty keeps the feature inert | --- diff --git a/rust/crates/language-host/src/main.rs b/rust/crates/language-host/src/main.rs index d702ba9b100..35e039c9fe2 100644 --- a/rust/crates/language-host/src/main.rs +++ b/rust/crates/language-host/src/main.rs @@ -1,4 +1,4 @@ -// Coderm Language Host (Phase 3: definition + Phase 2 hover + Phase 1.5 robustness). +// Coderm Language Host (Phase 4: references + Phase 3: definition + Phase 2 hover + Phase 1.5 robustness). // // Wire format: [4 bytes LE request_id][4 bytes LE length][payload(JSON)]. // request_id == 0 → notification (no response). Used for document sync. @@ -11,6 +11,10 @@ // - foldingRange: {type:"foldingRange", uri} → [FoldingRange] // - hover: {type:"hover", uri, line, column} → HoverResponse | null // - definition: {type:"definition", uri, line, column} → DefinitionResponse | null +// - references: {type:"references", uri, line, column, includeDeclaration} → DefinitionResponse | null +// +// Phase 4 additions: +// - references: file-local symbol references (find all references matching the identifier name) // // Phase 3 additions: // - definition: file-local symbol resolution (find all declarations matching the identifier name) @@ -63,6 +67,13 @@ enum Message { Hover { uri: String, line: u32, column: u32 }, #[serde(rename = "definition")] Definition { uri: String, line: u32, column: u32 }, + #[serde(rename = "references", rename_all = "camelCase")] + References { + uri: String, + line: u32, + column: u32, + include_declaration: bool, + }, } struct Document { @@ -191,6 +202,17 @@ impl LanguageHost { Message::Definition { uri, line, column } => { Ok(Some(self.definition_json(&uri, line, column))) } + Message::References { + uri, + line, + column, + include_declaration, + } => Ok(Some(self.references_json( + &uri, + line, + column, + include_declaration, + ))), } } @@ -235,6 +257,25 @@ impl LanguageHost { definition_at_position(&doc.language_id, &doc.content, uri, line, column) }) } + + fn references_json( + &self, + uri: &str, + line: u32, + column: u32, + include_declaration: bool, + ) -> String { + self.feature_json(uri, "references", |doc| { + references_at_position( + &doc.language_id, + &doc.content, + uri, + line, + column, + include_declaration, + ) + }) + } } fn parse_source(language_id: &str, content: &str) -> Result { @@ -533,15 +574,19 @@ fn strip_jsdoc(combined: &str) -> String { .join("\n") } -// Phase 3: definition provider. Returns all declaration locations matching the identifier -// at the given position. File-local only: all locations share the same uri. -fn definition_at_position( +// Resolves the renderer's (line_1, column_1) to the identifier text under the cursor and +// returns the parsed tree so the caller can walk it. Returns None when the point is not on +// an identifier-like node. Shared front-end for definition (Phase 3) and references (Phase 4). +// +// Why the Tree is returned (not re-parsed inside each caller): walking the tree for matches +// is the caller's job, and re-parsing would double the parse cost per request. The returned +// &str borrows from `content` (not from the Tree), so the Tree can be moved out freely. +fn find_identifier_at_position<'a>( language_id: &str, - content: &str, - uri: &str, + content: &'a str, line_1: u32, column_1: u32, -) -> Result, String> { +) -> Result, String> { let tree = parse_source(language_id, content)?; let row_0 = line_1.saturating_sub(1) as usize; let column_0_utf8 = utf16_column_to_utf8_byte(content, row_0, column_1)?; @@ -564,17 +609,66 @@ fn definition_at_position( return Ok(None); } - let identifier_text = node_text(leaf, content); + // Bind the identifier first so NLL can see `leaf`'s borrow of `tree` ends before `tree` + // is moved into the return tuple. The slice borrows from `content` (not the Tree). + let identifier = node_text(leaf, content); + Ok(Some((tree, identifier))) +} + +// Phase 3: definition provider. Returns all declaration locations matching the identifier +// at the given position. File-local only: all locations share the same uri. +fn definition_at_position( + language_id: &str, + content: &str, + uri: &str, + line_1: u32, + column_1: u32, +) -> Result, String> { + let (tree, identifier) = + match find_identifier_at_position(language_id, content, line_1, column_1)? { + Some(v) => v, + None => return Ok(None), + }; // Scan the entire document for declarations whose name token matches. // Caution: name-based, not scope-aware — returns every same-named declaration in the file // (no shadowing/overload filtering), since Phase 3 is file-local only. let mut locations = Vec::new(); - collect_definition_locations( + collect_definition_locations(tree.root_node(), content, uri, identifier, &mut locations); + + if locations.is_empty() { + Ok(None) + } else { + Ok(Some(DefinitionResponse { locations })) + } +} + +// Phase 4: references provider. Returns all reference locations matching the identifier +// at the given position. File-local only: all locations share the same uri. +fn references_at_position( + language_id: &str, + content: &str, + uri: &str, + line_1: u32, + column_1: u32, + include_declaration: bool, +) -> Result, String> { + let (tree, identifier) = + match find_identifier_at_position(language_id, content, line_1, column_1)? { + Some(v) => v, + None => return Ok(None), + }; + + // Collect references to the identifier throughout the entire document. + // Caution: name-based, not scope-aware — returns every same-named identifier in the file + // (no shadowing/overload filtering), since Phase 4 is file-local only. + let mut locations = Vec::new(); + collect_reference_locations( tree.root_node(), content, uri, - identifier_text, + identifier, + include_declaration, &mut locations, ); @@ -585,7 +679,7 @@ fn definition_at_position( } } -// Why recursive named-child walk (not TreeCursor siblings): declarations nest arbitrarily +// Why recursive named-child walk (not TreeCursor siblings): references nest arbitrarily // (methods inside classes, functions inside blocks), so we descend into named children the // same way collect_symbols does — a single TreeCursor.goto_next() loop only visits siblings // of one node and would miss nested declarations. @@ -650,6 +744,51 @@ fn is_definition_declaration_kind(kind: &str) -> bool { ) } +// Phase 4: collect all identifier references matching a given text. +// Why recursive named-child walk (not TreeCursor siblings): references nest arbitrarily +// (methods inside classes, functions inside blocks), so we descend into named children the +// same way collect_symbols does — a single TreeCursor.goto_next() loop only visits siblings +// of one node and would miss nested references. +fn collect_reference_locations( + node: Node, + source: &str, + uri: &str, + identifier: &str, + include_declaration: bool, + out: &mut Vec, +) { + // Collect identifier-like nodes whose text matches. When include_declaration is false, + // skip nodes that are the `name` field of a declaration (those are the declaration sites). + if is_definition_trigger_kind(node.kind()) + && node_text(node, source) == identifier + && (include_declaration || !is_declaration_name(node)) + { + out.push(Location { + uri: uri.to_string(), + range: range_from_points(node.start_position(), node.end_position()), + }); + } + let count = node.named_child_count(); + for i in 0..count { + if let Some(child) = node.named_child(i) { + collect_reference_locations(child, source, uri, identifier, include_declaration, out); + } + } +} + +// Check if the node is a declaration name (parent is a declaration kind and this node is its "name" field). +// Used to gate declaration inclusion in reference results when include_declaration is false. +fn is_declaration_name(node: Node) -> bool { + if let Some(parent) = node.parent() { + if is_definition_declaration_kind(parent.kind()) { + if let Some(name_node) = parent.child_by_field_name("name") { + return node == name_node; + } + } + } + false +} + // Emits one [reqId(4)][length(4)][payload] frame to stdout. fn write_frame(request_id: u32, payload: &[u8]) -> io::Result<()> { let mut header = [0u8; 8]; diff --git a/src/vs/workbench/services/languageHost/common/languageFeatures.ts b/src/vs/workbench/services/languageHost/common/languageFeatures.ts index fb829768f3f..7d8f2ee63a2 100644 --- a/src/vs/workbench/services/languageHost/common/languageFeatures.ts +++ b/src/vs/workbench/services/languageHost/common/languageFeatures.ts @@ -22,6 +22,8 @@ import { FoldingRangeProvider, Hover, HoverProvider, + ReferenceContext, + ReferenceProvider, SymbolKind, } from '../../../../editor/common/languages.js'; import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; @@ -129,7 +131,7 @@ class CodermDefinitionProvider implements DefinitionProvider { position.lineNumber, position.column ); - return parseDefinition(json); + return parseLocations(json); } catch (err) { console.error('[CodermDefinitionProvider] request failed', err); return undefined; @@ -137,6 +139,30 @@ class CodermDefinitionProvider implements DefinitionProvider { } } +class CodermReferenceProvider implements ReferenceProvider { + readonly displayName = 'Coderm Language Host'; + + constructor(private readonly languageHostService: ILanguageHostService) { } + + async provideReferences(model: ITextModel, position: Position, context: ReferenceContext, token: CancellationToken): Promise { + if (token.isCancellationRequested) { + return undefined; + } + try { + const json = await this.languageHostService.requestReferences( + model.uri.toString(), + position.lineNumber, + position.column, + context.includeDeclaration + ); + return parseLocations(json); + } catch (err) { + console.error('[CodermReferenceProvider] request failed', err); + return undefined; + } + } +} + // Parses a JSON array from the host's response. Returns [] on parse failure or a // non-array payload — a malformed host reply should not crash the provider. function parseJsonArray(json: string): T[] { @@ -213,7 +239,9 @@ function parseHover(json: string): Hover | undefined { return { contents, range: response.range }; } -function parseDefinition(json: string): Definition | undefined { +// Definition and references share the same wire shape (a DefinitionResponse with a +// `locations` array), so one parser covers both. Returns Location[] — also a valid Definition. +function parseLocations(json: string): Location[] | undefined { const response = parseJsonObject(json); if (!response) { return undefined; @@ -233,6 +261,7 @@ export function registerLanguageFeatureProviders( const foldingRangeProvider = new CodermFoldingRangeProvider(languageHostService); const hoverProvider = new CodermHoverProvider(languageHostService); const definitionProvider = new CodermDefinitionProvider(languageHostService); + const referenceProvider = new CodermReferenceProvider(languageHostService); // Why a flat string[] selector: each entry is a language id; VS Code invokes the provider // only for models whose language matches, so no extra in-provider filtering is needed. @@ -241,5 +270,6 @@ export function registerLanguageFeatureProviders( languageFeaturesService.foldingRangeProvider.register(languages, foldingRangeProvider), languageFeaturesService.hoverProvider.register(languages, hoverProvider), languageFeaturesService.definitionProvider.register(languages, definitionProvider), + languageFeaturesService.referenceProvider.register(languages, referenceProvider), ); } diff --git a/src/vs/workbench/services/languageHost/common/languageHost.ts b/src/vs/workbench/services/languageHost/common/languageHost.ts index ab1f22e6f78..fdb5fb79ace 100644 --- a/src/vs/workbench/services/languageHost/common/languageHost.ts +++ b/src/vs/workbench/services/languageHost/common/languageHost.ts @@ -55,6 +55,12 @@ export interface ILanguageHostService { * Returns the host's JSON string (a DefinitionResponse shape or null). */ requestDefinition(uri: string, line: number, column: number): Promise; + + /** + * Request references for a position in a synced document. + * Returns the host's JSON string (a DefinitionResponse shape or null). + */ + requestReferences(uri: string, line: number, column: number, includeDeclaration: boolean): Promise; } // --- Coderm end --- diff --git a/src/vs/workbench/services/languageHost/electron-browser/languageHostService.ts b/src/vs/workbench/services/languageHost/electron-browser/languageHostService.ts index c043447285b..e0dff75081a 100644 --- a/src/vs/workbench/services/languageHost/electron-browser/languageHostService.ts +++ b/src/vs/workbench/services/languageHost/electron-browser/languageHostService.ts @@ -111,7 +111,11 @@ export class NativeLanguageHostService extends Disposable implements ILanguageHo return this.requestFeature({ type: 'definition', uri, line, column }); } - private async requestFeature(message: { type: string; uri: string; line?: number; column?: number }): Promise { + async requestReferences(uri: string, line: number, column: number, includeDeclaration: boolean): Promise { + return this.requestFeature({ type: 'references', uri, line, column, includeDeclaration }); + } + + private async requestFeature(message: { type: string; uri: string; line?: number; column?: number; includeDeclaration?: boolean }): Promise { if (!this.protocol) { throw new Error('Language Host not ready'); }