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:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user