Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/language/lsifLanguageFeatureService.ts
13394 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import * as scip from '@c4312/scip';
6
import * as LSIF from '@vscode/lsif-language-service';
7
import * as fs from 'fs/promises';
8
import type * as vscode from 'vscode';
9
import { ILanguageFeaturesService } from '../../../src/platform/languages/common/languageFeaturesService';
10
import { SimulationWorkspace } from '../../../src/platform/test/node/simulationWorkspace';
11
import { escapeRegExpCharacters } from '../../../src/util/vs/base/common/strings';
12
import { URI } from '../../../src/util/vs/base/common/uri';
13
import { Location, Range } from '../../../src/vscodeTypes';
14
15
const REPO_NAME = 'vscode-copilot';
16
const liftLSIFRange = (range: LSIF.types.Range): Range => new Range(range.start.line, range.start.character, range.end.line, range.end.character);
17
const liftLSIFLocations = (locations: undefined | LSIF.types.Location | LSIF.types.Location[]): vscode.Location[] => {
18
if (!locations) {
19
return [];
20
}
21
const arr = locations instanceof Array ? locations : [locations];
22
return arr.map(l => new Location(URI.parse(l.uri), liftLSIFRange(l.range)));
23
};
24
25
type IGraph = Pick<LSIF.JsonStore, 'declarations' | 'definitions' | 'references'>;
26
27
/** Gets whether the SCIP occurence happens at the given posiiton */
28
const occursAt = (o: scip.Occurrence, position: LSIF.types.Position) => {
29
const range = occurenceToPosition(o);
30
if (position.line < range.start.line || (position.line === range.start.line && position.character < range.start.character)) {
31
return false;
32
}
33
34
if (position.line > range.end.line || (position.line === range.end.line && position.character >= range.end.character)) {
35
return false;
36
}
37
38
return true;
39
};
40
41
/** Converts an SCIP occurence to an LSIF range */
42
const occurenceToPosition = (o: scip.Occurrence): LSIF.types.Range => {
43
const [startLine, startChar] = o.range;
44
let endLine: number;
45
let endChar: number;
46
if (o.range.length >= 4) {
47
[, , endLine, endChar] = o.range;
48
} else {
49
endLine = startLine;
50
endChar = o.range[2];
51
}
52
53
return { start: { line: startLine, character: startChar }, end: { line: endLine, character: endChar } };
54
};
55
56
class SCIPGraph implements IGraph {
57
private readonly workspaceUriLower: string;
58
private readonly workspaceUriOriginal: URI;
59
60
constructor(
61
private readonly index: scip.Index,
62
workspace: SimulationWorkspace,
63
) {
64
this.workspaceUriOriginal = workspace.workspaceFolders[0];
65
this.workspaceUriLower = workspace.workspaceFolders[0].toString(true).toLowerCase();
66
}
67
68
declarations(uri: string, position: LSIF.types.Position): LSIF.types.Location | LSIF.types.Location[] | undefined {
69
// https://github.com/sourcegraph/scip/blob/0504a347d36dbff48b21f53ccfedb46f3803855e/scip.proto#L501
70
return this.findOccurencesOfSymbolAt(uri, position, o => !!(o.symbolRoles & 0x1));
71
}
72
73
definitions(uri: string, position: LSIF.types.Position): LSIF.types.Location | LSIF.types.Location[] | undefined {
74
return this.declarations(uri, position); // SCIP doesn't really differentiate I think...
75
}
76
77
references(uri: string, position: LSIF.types.Position, context: LSIF.types.ReferenceContext): LSIF.types.Location[] | undefined {
78
return this.findOccurencesOfSymbolAt(uri, position, () => true);
79
}
80
81
private findOccurencesOfSymbolAt(uri: string, position: LSIF.types.Position, filter: (o: scip.Occurrence) => boolean): LSIF.types.Location[] {
82
const toFind = this.getSymbolsAt(uri, position);
83
const locations: LSIF.types.Location[] = [];
84
for (const doc of this.index.documents) {
85
for (const occurence of doc.occurrences) {
86
if (occurence.symbolRoles & 0x1 && toFind.has(occurence.symbol)) {// definition
87
toFind.delete(occurence.symbol);
88
locations.push({
89
uri: URI.joinPath(this.workspaceUriOriginal, doc.relativePath.replaceAll('\\', '/')).toString(true),
90
range: occurenceToPosition(occurence),
91
});
92
}
93
}
94
}
95
96
return locations;
97
}
98
99
private getSymbolsAt(uri: string, position: LSIF.types.Position): Set<string> {
100
const doc = this.getDoc(uri);
101
if (!doc) { return new Set(); }
102
103
const toFind = new Set<string>();
104
for (const occurence of doc.occurrences) {
105
if (occursAt(occurence, position)) {
106
toFind.add(occurence.symbol);
107
}
108
}
109
110
return toFind;
111
}
112
113
private getDoc(uriInWorkspace: string) {
114
uriInWorkspace = uriInWorkspace.toLowerCase().replaceAll('\\', '/');
115
if (!uriInWorkspace.startsWith(this.workspaceUriLower)) {
116
return undefined;
117
}
118
119
uriInWorkspace = uriInWorkspace.slice(this.workspaceUriLower.length);
120
if (uriInWorkspace.startsWith('/')) {
121
uriInWorkspace = uriInWorkspace.slice(1);
122
}
123
124
return this.index.documents.find(d => d.relativePath.replaceAll('\\', '/').toLowerCase() === uriInWorkspace);
125
}
126
}
127
128
const makeTranslator = (workspace: SimulationWorkspace, indexRoot: string) => {
129
let simulationRootUri = workspace.workspaceFolders[0].toString(true);
130
if (simulationRootUri.endsWith('/')) {
131
simulationRootUri = simulationRootUri.slice(0, -1);
132
}
133
134
const indexPath = URI.parse(indexRoot).path;
135
const lastIndex = indexPath.lastIndexOf(REPO_NAME);
136
if (lastIndex === -1) {
137
throw new Error(`Index path ${indexPath} does not contain 'vscode-copilot', please ensure the index is generated in the correct workspace`);
138
}
139
140
const subdir = indexPath.slice(lastIndex + REPO_NAME.length + 1);
141
const localRootRe = new RegExp(`^file:\\/\\/.*?${escapeRegExpCharacters(subdir.replaceAll('\\', '/'))}`, 'i');
142
143
return {
144
fromDatabase: (uri: string) => uri.replace(localRootRe, simulationRootUri),
145
toDatabase: (uri: string) => uri.startsWith(simulationRootUri) ? uri.replace(simulationRootUri, indexRoot) : uri,
146
};
147
};
148
149
/**
150
* A language features service powered by an LSIF index. To use this, you need
151
* to have generated an LSIF index for your workspace. This can be done in
152
* several ways:
153
*
154
* - Rust: ensure rust-analyzer is installed (rustup component add rust-analyzer)
155
* and run `rust-analyzer lsif ./ > lsif.json` in the workspace root.
156
*
157
* If you index a new language, please add instructions above.
158
*/
159
export class LSIFLanguageFeaturesService implements ILanguageFeaturesService {
160
_serviceBrand: undefined;
161
162
private _graph: Promise<IGraph> | undefined;
163
164
/**
165
* @param workspace The simulation workspace
166
* @param indexFilePath Path to an LSIF index file
167
*/
168
constructor(
169
private readonly workspace: SimulationWorkspace,
170
private readonly indexFilePath: string,
171
) { }
172
173
private _getGraph(): Promise<IGraph> {
174
if (!this._graph) {
175
this._graph = this._load();
176
}
177
return this._graph;
178
}
179
180
private async _load(): Promise<IGraph> {
181
if (this.indexFilePath.endsWith('.scip')) {
182
const contents = await fs.readFile(this.indexFilePath);
183
const index = scip.deserializeSCIP(contents);
184
return new SCIPGraph(index, this.workspace);
185
}
186
187
const graph = new LSIF.JsonStore();
188
try {
189
await graph.load(this.indexFilePath, r => makeTranslator(this.workspace, r));
190
} catch (e) {
191
throw new Error(`Failed to parse LSIF index from ${this.indexFilePath}: ${e}`);
192
}
193
return graph;
194
}
195
196
async getDocumentSymbols(uri: vscode.Uri): Promise<vscode.DocumentSymbol[]> {
197
throw new Error('Unimplemented: excercise for the reader');
198
}
199
200
async getDefinitions(uri: vscode.Uri, position: vscode.Position): Promise<(vscode.LocationLink | vscode.Location)[]> {
201
const graph = await this._getGraph();
202
return liftLSIFLocations(graph.definitions(uri.toString(true), position));
203
}
204
205
async getImplementations(uri: vscode.Uri, position: vscode.Position): Promise<(vscode.LocationLink | vscode.Location)[]> {
206
const graph = await this._getGraph();
207
return liftLSIFLocations(graph.declarations(uri.toString(true), position));
208
}
209
210
async getReferences(uri: vscode.Uri, position: vscode.Position): Promise<vscode.Location[]> {
211
const graph = await this._getGraph();
212
return liftLSIFLocations(graph.references(uri.toString(true), position, { includeDeclaration: true }));
213
}
214
215
getDiagnostics(_uri: vscode.Uri): vscode.Diagnostic[] {
216
return []; // not part of LSIF
217
}
218
219
async getWorkspaceSymbols(query: string): Promise<vscode.SymbolInformation[]> {
220
throw new Error('Unimplemented: excercise for the reader');
221
// would have to iterate through all documents, get all symbols that match
222
}
223
}
224
225