Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")。空の場合は機能無効 |

---
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

---
Expand Down
161 changes: 150 additions & 11 deletions rust/crates/language-host/src/main.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
))),
}
}

Expand Down Expand Up @@ -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<tree_sitter::Tree, String> {
Expand Down Expand Up @@ -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<Option<DefinitionResponse>, String> {
) -> Result<Option<(tree_sitter::Tree, &'a str)>, 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)?;
Expand All @@ -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<Option<DefinitionResponse>, 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<Option<DefinitionResponse>, 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,
);

Expand All @@ -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.
Expand Down Expand Up @@ -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<Location>,
) {
// 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];
Expand Down
34 changes: 32 additions & 2 deletions src/vs/workbench/services/languageHost/common/languageFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
FoldingRangeProvider,
Hover,
HoverProvider,
ReferenceContext,
ReferenceProvider,
SymbolKind,
} from '../../../../editor/common/languages.js';
import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
Expand Down Expand Up @@ -129,14 +131,38 @@ class CodermDefinitionProvider implements DefinitionProvider {
position.lineNumber,
position.column
);
return parseDefinition(json);
return parseLocations(json);
} catch (err) {
console.error('[CodermDefinitionProvider] request failed', err);
return undefined;
}
}
}

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<Location[] | undefined> {
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<T>(json: string): T[] {
Expand Down Expand Up @@ -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<DefinitionResponse>(json);
if (!response) {
return undefined;
Expand All @@ -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.
Expand All @@ -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),
);
}
6 changes: 6 additions & 0 deletions src/vs/workbench/services/languageHost/common/languageHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;

/**
* 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<string>;
}

// --- Coderm end ---
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
async requestReferences(uri: string, line: number, column: number, includeDeclaration: boolean): Promise<string> {
return this.requestFeature({ type: 'references', uri, line, column, includeDeclaration });
}

private async requestFeature(message: { type: string; uri: string; line?: number; column?: number; includeDeclaration?: boolean }): Promise<string> {
if (!this.protocol) {
throw new Error('Language Host not ready');
}
Expand Down
Loading