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・references を提供(Phase 4) |
| `coderm.languageHost.enabled` | `boolean` | `false` | (実験的)ネイティブ(Rust)Language Host を有効化。設定した言語で tree-sitter ベースの documentSymbol・foldingRange・hover・definition・references・documentHighlights を提供(Phase 5) |
| `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, definition, and references for the configured languages (Phase 4) |
| `coderm.languageHost.enabled` | `boolean` | `false` | _(experimental)_ Enable the native (Rust) Language Host. Provides tree-sitter-backed documentSymbol, foldingRange, hover, definition, references, and document highlights for the configured languages (Phase 5) |
| `coderm.languageHost.languages` | `array` | `[]` | _(experimental)_ Language IDs handled by the native host (e.g. "typescript", "tsx"). Empty keeps the feature inert |

---
Expand Down
127 changes: 106 additions & 21 deletions rust/crates/language-host/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
// Coderm Language Host (Phase 4: references + Phase 3: definition + Phase 2 hover + Phase 1.5 robustness).
// Coderm Language Host (Phase 5: document highlights + 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.
// request_id > 0 → request (response expected). Used for language features.
// Payload is JSON tagged by "type":
// - document/open: {type:"document/open", uri, version, languageId, text}
// - document/change: {type:"document/change", uri, version, text} // full-text replace
// - document/close: {type:"document/close", uri}
// - documentSymbol: {type:"documentSymbol", uri} → [DocumentSymbol]
// - 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
// - document/open: {type:"document/open", uri, version, languageId, text}
// - document/change: {type:"document/change", uri, version, text} // full-text replace
// - document/close: {type:"document/close", uri}
// - documentSymbol: {type:"documentSymbol", uri} → [DocumentSymbol]
// - 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
// - documentHighlights: {type:"documentHighlights", uri, line, column} → DocumentHighlightsResponse | null
//
// Phase 5 additions:
// - documentHighlights: file-local identifier highlights (Write for declaration names, Read otherwise)
//
// Phase 4 additions:
// - references: file-local symbol references (find all references matching the identifier name)
Expand Down Expand Up @@ -74,6 +78,8 @@ enum Message {
column: u32,
include_declaration: bool,
},
#[serde(rename = "documentHighlights", rename_all = "camelCase")]
DocumentHighlights { uri: String, line: u32, column: u32 },
}

struct Document {
Expand Down Expand Up @@ -133,6 +139,21 @@ struct Location {
range: Range,
}

// Phase 5 document highlight response. `kind` mirrors VS Code's DocumentHighlightKind
// (Text=0, Read=1, Write=2); Text is unused in v1.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct DocumentHighlightsResponse {
highlights: Vec<DocumentHighlightResponse>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct DocumentHighlightResponse {
range: Range,
kind: u32,
}

struct LanguageHost {
documents: HashMap<String, Document>,
}
Expand Down Expand Up @@ -213,6 +234,9 @@ impl LanguageHost {
column,
include_declaration,
))),
Message::DocumentHighlights { uri, line, column } => {
Ok(Some(self.document_highlights_json(&uri, line, column)))
}
}
}

Expand Down Expand Up @@ -276,6 +300,12 @@ impl LanguageHost {
)
})
}

fn document_highlights_json(&self, uri: &str, line: u32, column: u32) -> String {
self.feature_json(uri, "document highlights", |doc| {
document_highlights_at_position(&doc.language_id, &doc.content, line, column)
})
}
}

fn parse_source(language_id: &str, content: &str) -> Result<tree_sitter::Tree, String> {
Expand Down Expand Up @@ -409,12 +439,7 @@ fn hover_at_position(
column_1: u32,
) -> Result<Option<HoverResponse>, 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)?;
let point = Point {
row: row_0,
column: column_0_utf8,
};
let point = point_for_position(content, line_1, column_1)?;

// Deepest named node at the point, then walk up to the enclosing declaration.
let leaf = match tree
Expand Down Expand Up @@ -478,6 +503,18 @@ fn utf16_column_to_utf8_byte(content: &str, row_0: usize, column_1: u32) -> Resu
Ok(byte_offset)
}

// Resolve a renderer (line_1, column_1) Position to a tree-sitter Point. Wraps the
// 1-indexed → 0-indexed row conversion and the UTF-16 → UTF-8 byte column reconciliation
// performed by utf16_column_to_utf8_byte (see its comment for the per-row walk rationale).
fn point_for_position(content: &str, line_1: u32, column_1: u32) -> Result<Point, String> {
let row_0 = line_1.saturating_sub(1) as usize;
let column_0_utf8 = utf16_column_to_utf8_byte(content, row_0, column_1)?;
Ok(Point {
row: row_0,
column: column_0_utf8,
})
}

fn walk_up_to_declaration<'a>(start: Node<'a>) -> Option<Node<'a>> {
let mut cursor = Some(start);
while let Some(n) = cursor {
Expand Down Expand Up @@ -588,12 +625,7 @@ fn find_identifier_at_position<'a>(
column_1: u32,
) -> 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)?;
let point = Point {
row: row_0,
column: column_0_utf8,
};
let point = point_for_position(content, line_1, column_1)?;

// Get the identifier at the cursor position.
let leaf = match tree
Expand Down Expand Up @@ -789,6 +821,59 @@ fn is_declaration_name(node: Node) -> bool {
false
}

// Phase 5: document highlights provider. Returns highlight ranges for all occurrences of the
// identifier at the given position. File-local only. kind heuristic: declaration name →
// Write(2), otherwise Read(1); Text(0) is unused in v1.
fn document_highlights_at_position(
language_id: &str,
content: &str,
line_1: u32,
column_1: u32,
) -> Result<Option<DocumentHighlightsResponse>, String> {
let (tree, identifier) =
match find_identifier_at_position(language_id, content, line_1, column_1)? {
Some(v) => v,
None => return Ok(None),
};

// Caution: name-based, not scope-aware — highlights every same-named identifier in the file
// (shadowed locals too), since Phase 5 is file-local only.
let mut highlights = Vec::new();
collect_document_highlights(tree.root_node(), content, identifier, &mut highlights);

if highlights.is_empty() {
Ok(None)
} else {
Ok(Some(DocumentHighlightsResponse { highlights }))
}
}

// Why recursive named-child walk: occurrences nest arbitrarily (methods inside classes,
// references inside expressions), so we descend into named children like the other collect_*
// helpers — a single goto_next() loop only visits one level of siblings.
fn collect_document_highlights(
node: Node,
source: &str,
identifier: &str,
out: &mut Vec<DocumentHighlightResponse>,
) {
if is_definition_trigger_kind(node.kind()) && node_text(node, source) == identifier {
// Declaration name (the identifier bound as a declaration's "name" field) is a Write
// site; every other occurrence is a Read.
let kind = if is_declaration_name(node) { 2 } else { 1 };
out.push(DocumentHighlightResponse {
range: range_from_points(node.start_position(), node.end_position()),
kind,
});
}
let count = node.named_child_count();
for i in 0..count {
if let Some(child) = node.named_child(i) {
collect_document_highlights(child, source, identifier, out);
}
}
}

// 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
55 changes: 51 additions & 4 deletions src/vs/workbench/services/languageHost/common/languageFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,24 @@ import {
HoverProvider,
ReferenceContext,
ReferenceProvider,
DocumentHighlight,
DocumentHighlightKind,
DocumentHighlightProvider,
SymbolKind,
} from '../../../../editor/common/languages.js';
import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { Position } from '../../../../editor/common/core/position.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { ILanguageHostService } from './languageHost.js';

// Shapes returned by the Rust host (camelCase, matching the renderer's DocumentSymbol/IRange).
// `range` fields reuse the editor's IRange since the host emits the identical shape.
interface DocumentSymbolResponse {
name: string;
kind: number;
range: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number };
selectionRange: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number };
range: IRange;
selectionRange: IRange;
children?: DocumentSymbolResponse[];
}

Expand All @@ -48,11 +53,15 @@ interface FoldingRangeResponse {
interface HoverResponse {
signature: string;
documentation: string;
range: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number };
range: IRange;
}

interface DefinitionResponse {
locations: Array<{ uri: string; range: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number } }>;
locations: Array<{ uri: string; range: IRange }>;
}

interface DocumentHighlightsResponse {
highlights: Array<{ range: IRange; kind: number }>;
}

class CodermDocumentSymbolProvider implements DocumentSymbolProvider {
Expand Down Expand Up @@ -163,6 +172,29 @@ class CodermReferenceProvider implements ReferenceProvider {
}
}

class CodermDocumentHighlightProvider implements DocumentHighlightProvider {
readonly displayName = 'Coderm Language Host';

constructor(private readonly languageHostService: ILanguageHostService) { }

async provideDocumentHighlights(model: ITextModel, position: Position, token: CancellationToken): Promise<DocumentHighlight[] | undefined> {
if (token.isCancellationRequested) {
return undefined;
}
try {
const json = await this.languageHostService.requestDocumentHighlights(
model.uri.toString(),
position.lineNumber,
position.column
);
return parseDocumentHighlights(json);
} catch (err) {
console.error('[CodermDocumentHighlightProvider] 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 @@ -252,6 +284,19 @@ function parseLocations(json: string): Location[] | undefined {
}));
}

// Note: kind mirrors VS Code's DocumentHighlightKind (Text=0, Read=1, Write=2). The host sends
// numbers directly (no enum on the wire), so a plain cast is sufficient.
function parseDocumentHighlights(json: string): DocumentHighlight[] | undefined {
const response = parseJsonObject<DocumentHighlightsResponse>(json);
if (!response) {
return undefined;
}
return response.highlights.map((h): DocumentHighlight => ({
range: h.range,
kind: h.kind as DocumentHighlightKind,
}));
}

export function registerLanguageFeatureProviders(
languageHostService: ILanguageHostService,
languages: string[],
Expand All @@ -262,6 +307,7 @@ export function registerLanguageFeatureProviders(
const hoverProvider = new CodermHoverProvider(languageHostService);
const definitionProvider = new CodermDefinitionProvider(languageHostService);
const referenceProvider = new CodermReferenceProvider(languageHostService);
const documentHighlightProvider = new CodermDocumentHighlightProvider(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 @@ -271,5 +317,6 @@ export function registerLanguageFeatureProviders(
languageFeaturesService.hoverProvider.register(languages, hoverProvider),
languageFeaturesService.definitionProvider.register(languages, definitionProvider),
languageFeaturesService.referenceProvider.register(languages, referenceProvider),
languageFeaturesService.documentHighlightProvider.register(languages, documentHighlightProvider),
);
}
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 @@ -61,6 +61,12 @@ export interface ILanguageHostService {
* Returns the host's JSON string (a DefinitionResponse shape or null).
*/
requestReferences(uri: string, line: number, column: number, includeDeclaration: boolean): Promise<string>;

/**
* Request document highlights for a position in a synced document.
* Returns the host's JSON string (a DocumentHighlightsResponse shape or null).
*/
requestDocumentHighlights(uri: string, line: number, column: number): Promise<string>;
}

// --- Coderm end ---
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ export class NativeLanguageHostService extends Disposable implements ILanguageHo
return this.requestFeature({ type: 'references', uri, line, column, includeDeclaration });
}

async requestDocumentHighlights(uri: string, line: number, column: number): Promise<string> {
return this.requestFeature({ type: 'documentHighlights', uri, line, column });
}

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