Mark write references in the editor view too

Kinds now live on the display ranges rather than in a side map, so the
virtual document can paint reads and writes differently.

- ResultLine.symbolRanges becomes DisplayRange[] carrying start, end, the
  source column and the kind. displayLine() classifies syntactically as it
  renders, so the write markers are correct on the very first paint and the
  document never waits on a language server. refineKinds() then applies
  documentHighlight results and re-decorates; only decorations change, never
  the text, and a superseded tab is left alone.
- Writes get the theme's editor.wordHighlightStrong colours plus bold, which
  is what the editor itself uses for a write occurrence, so the two kinds are
  distinguishable in any theme. Reads keep the find-match highlight.
- DisplayRange.sourceCol replaces the arithmetic that recovered a source
  column by shifting relative to the first reference on the line. Covered by
  a new test that walks every occurrence on a line holding two references to
  the same symbol and checks each one navigates to its own column.

Also drops a stale assertion: an empty kind map used to mean "everything
reads", and now means "use the syntactic classification", so buildRows()
without a map is expected to report writes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
max
2026-09-07 16:58:25 +02:00
co-authored by Claude Opus 5
parent 9b2dbc4522
commit 864f4c2c5f
6 changed files with 199 additions and 30 deletions
+55 -4
View File
@@ -3,7 +3,7 @@ import * as path from 'path';
import * as vscode from 'vscode';
import { buildRows } from '../../panel';
import { gather, refKey, referenceKinds, syntacticKind } from '../../references';
import { gather, refKey, referenceKinds, renderDocument, syntacticKind } from '../../references';
const KIND_FILE = path.join('Nerfed.Editor', 'Systems', 'EditorProfilerWindow.cs');
const KIND_SYMBOL = 'selectedFrame';
@@ -120,7 +120,54 @@ suite('Read / write kind from the language server', () => {
assert.ok(writes >= 2 && reads >= 3, `expected reads and writes, got ${reads}/${writes}`);
});
test('rows carry the kind, defaulting to read when unknown', async () => {
test('the editor view marks writes on the right display ranges', async () => {
const folder = vscode.workspace.workspaceFolders?.[0];
assert.ok(folder);
const uri = vscode.Uri.joinPath(folder.uri, ...KIND_FILE.split(path.sep));
const doc = await vscode.workspace.openTextDocument(uri);
let position = new vscode.Position(0, 0);
for (let i = 0; i < doc.lineCount; i++) {
const at = doc.lineAt(i).text.indexOf(KIND_DECL);
if (at >= 0) {
position = new vscode.Position(i, doc.lineAt(i).text.indexOf(KIND_SYMBOL, at) + 1);
break;
}
}
const locations = await vscode.commands.executeCommand<vscode.Location[]>(
'vscode.executeReferenceProvider', uri, position);
const results = await gather(KIND_SYMBOL, 'csharp', { uri, position }, locations ?? [], true);
// Rendering with no kind map still classifies syntactically, so the markers are
// correct on the first paint; the server pass only refines them.
const bare = renderDocument(results);
const refined = renderDocument(results, await referenceKinds(results));
for (const rendered of [bare, refined]) {
const writes = new Set<number>();
for (const line of rendered.lines) {
if (line.kind !== 'code' || line.sourceLine === undefined) {
continue;
}
const sourceText: string = doc.lineAt(line.sourceLine).text;
const displayed: string = rendered.content.split('\n')[rendered.lines.indexOf(line)];
for (const range of line.symbolRanges ?? []) {
// The recorded source column must actually be the symbol.
assert.ok(sourceText.startsWith(KIND_SYMBOL, range.sourceCol),
`line ${line.sourceLine + 1} col ${range.sourceCol} is not ${KIND_SYMBOL}`);
// And the display range must cover the symbol in the rendered text.
assert.strictEqual(displayed.slice(range.start, range.end), KIND_SYMBOL);
if (range.kind === 'write') {
writes.add(line.sourceLine + 1);
}
}
}
assert.deepStrictEqual([...writes].sort((a, b) => a - b), [41, 43],
'expected the assignment and the ref argument to be the only writes');
}
});
test('rows carry the kind, falling back to the syntactic one', async () => {
const folder = vscode.workspace.workspaceFolders?.[0];
assert.ok(folder);
const uri = vscode.Uri.joinPath(folder.uri, ...KIND_FILE.split(path.sep));
@@ -145,8 +192,12 @@ suite('Read / write kind from the language server', () => {
'every row needs a read/write kind');
assert.ok(rows.some(r => r.kind === 'write'), 'no write rows');
// With no kind map at all, rows still render as reads rather than blank.
// With no kind map at all — a language server that serves no document
// highlights — rows still carry the syntactic classification, not a blank
// column or a uniform "read".
const { rows: bare } = buildRows(results, new Map());
assert.ok(bare.every(r => r.kind === 'read'));
assert.deepStrictEqual(bare.map(r => r.kind), rows.map(r => r.kind),
'the syntactic fallback should already agree with the server here');
assert.ok(bare.some(r => r.kind === 'write'), 'no write rows without a kind map');
});
});
+33
View File
@@ -209,6 +209,39 @@ suite('Colored References - real C# solution', () => {
assert.deepStrictEqual(failures, [], failures.join('\n'));
});
test('each occurrence on a shared line navigates to its own column', async () => {
const shared: number[] = [];
for (const line of codeLineNumbers(doc)) {
const text = doc.lineAt(line).text;
if (text.split(TARGET_SYMBOL).length - 1 > 1) {
shared.push(line);
}
}
assert.ok(shared.length > 0,
`no result line holds two references to ${TARGET_SYMBOL}; this test needs one`);
const failures: string[] = [];
for (const line of shared) {
const text = doc.lineAt(line).text;
for (let at = text.indexOf(TARGET_SYMBOL); at >= 0;
at = text.indexOf(TARGET_SYMBOL, at + 1)) {
const target = await definitionAt(doc, line, at + 1);
if (!target) {
failures.push(`line ${line} col ${at}: no target`);
continue;
}
const source = await vscode.workspace.openTextDocument(target.uri);
const landed = source.lineAt(target.range.start.line).text
.slice(target.range.start.character);
if (!landed.startsWith(TARGET_SYMBOL)) {
failures.push(`line ${line} col ${at} landed on ` +
`${JSON.stringify(landed.slice(0, 20))}`);
}
}
}
assert.deepStrictEqual(failures, [], failures.join('\n'));
});
test('file headers navigate to the top of the file and show the project', async () => {
const headers = headerLineNumbers(doc);
assert.ok(headers.length > 0);