Path: blob/main/extensions/copilot/test/simulation/language/lsifLanguageFeatureService.ts
13394 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import * as scip from '@c4312/scip';5import * as LSIF from '@vscode/lsif-language-service';6import * as fs from 'fs/promises';7import type * as vscode from 'vscode';8import { ILanguageFeaturesService } from '../../../src/platform/languages/common/languageFeaturesService';9import { SimulationWorkspace } from '../../../src/platform/test/node/simulationWorkspace';10import { escapeRegExpCharacters } from '../../../src/util/vs/base/common/strings';11import { URI } from '../../../src/util/vs/base/common/uri';12import { Location, Range } from '../../../src/vscodeTypes';1314const REPO_NAME = 'vscode-copilot';15const liftLSIFRange = (range: LSIF.types.Range): Range => new Range(range.start.line, range.start.character, range.end.line, range.end.character);16const liftLSIFLocations = (locations: undefined | LSIF.types.Location | LSIF.types.Location[]): vscode.Location[] => {17if (!locations) {18return [];19}20const arr = locations instanceof Array ? locations : [locations];21return arr.map(l => new Location(URI.parse(l.uri), liftLSIFRange(l.range)));22};2324type IGraph = Pick<LSIF.JsonStore, 'declarations' | 'definitions' | 'references'>;2526/** Gets whether the SCIP occurence happens at the given posiiton */27const occursAt = (o: scip.Occurrence, position: LSIF.types.Position) => {28const range = occurenceToPosition(o);29if (position.line < range.start.line || (position.line === range.start.line && position.character < range.start.character)) {30return false;31}3233if (position.line > range.end.line || (position.line === range.end.line && position.character >= range.end.character)) {34return false;35}3637return true;38};3940/** Converts an SCIP occurence to an LSIF range */41const occurenceToPosition = (o: scip.Occurrence): LSIF.types.Range => {42const [startLine, startChar] = o.range;43let endLine: number;44let endChar: number;45if (o.range.length >= 4) {46[, , endLine, endChar] = o.range;47} else {48endLine = startLine;49endChar = o.range[2];50}5152return { start: { line: startLine, character: startChar }, end: { line: endLine, character: endChar } };53};5455class SCIPGraph implements IGraph {56private readonly workspaceUriLower: string;57private readonly workspaceUriOriginal: URI;5859constructor(60private readonly index: scip.Index,61workspace: SimulationWorkspace,62) {63this.workspaceUriOriginal = workspace.workspaceFolders[0];64this.workspaceUriLower = workspace.workspaceFolders[0].toString(true).toLowerCase();65}6667declarations(uri: string, position: LSIF.types.Position): LSIF.types.Location | LSIF.types.Location[] | undefined {68// https://github.com/sourcegraph/scip/blob/0504a347d36dbff48b21f53ccfedb46f3803855e/scip.proto#L50169return this.findOccurencesOfSymbolAt(uri, position, o => !!(o.symbolRoles & 0x1));70}7172definitions(uri: string, position: LSIF.types.Position): LSIF.types.Location | LSIF.types.Location[] | undefined {73return this.declarations(uri, position); // SCIP doesn't really differentiate I think...74}7576references(uri: string, position: LSIF.types.Position, context: LSIF.types.ReferenceContext): LSIF.types.Location[] | undefined {77return this.findOccurencesOfSymbolAt(uri, position, () => true);78}7980private findOccurencesOfSymbolAt(uri: string, position: LSIF.types.Position, filter: (o: scip.Occurrence) => boolean): LSIF.types.Location[] {81const toFind = this.getSymbolsAt(uri, position);82const locations: LSIF.types.Location[] = [];83for (const doc of this.index.documents) {84for (const occurence of doc.occurrences) {85if (occurence.symbolRoles & 0x1 && toFind.has(occurence.symbol)) {// definition86toFind.delete(occurence.symbol);87locations.push({88uri: URI.joinPath(this.workspaceUriOriginal, doc.relativePath.replaceAll('\\', '/')).toString(true),89range: occurenceToPosition(occurence),90});91}92}93}9495return locations;96}9798private getSymbolsAt(uri: string, position: LSIF.types.Position): Set<string> {99const doc = this.getDoc(uri);100if (!doc) { return new Set(); }101102const toFind = new Set<string>();103for (const occurence of doc.occurrences) {104if (occursAt(occurence, position)) {105toFind.add(occurence.symbol);106}107}108109return toFind;110}111112private getDoc(uriInWorkspace: string) {113uriInWorkspace = uriInWorkspace.toLowerCase().replaceAll('\\', '/');114if (!uriInWorkspace.startsWith(this.workspaceUriLower)) {115return undefined;116}117118uriInWorkspace = uriInWorkspace.slice(this.workspaceUriLower.length);119if (uriInWorkspace.startsWith('/')) {120uriInWorkspace = uriInWorkspace.slice(1);121}122123return this.index.documents.find(d => d.relativePath.replaceAll('\\', '/').toLowerCase() === uriInWorkspace);124}125}126127const makeTranslator = (workspace: SimulationWorkspace, indexRoot: string) => {128let simulationRootUri = workspace.workspaceFolders[0].toString(true);129if (simulationRootUri.endsWith('/')) {130simulationRootUri = simulationRootUri.slice(0, -1);131}132133const indexPath = URI.parse(indexRoot).path;134const lastIndex = indexPath.lastIndexOf(REPO_NAME);135if (lastIndex === -1) {136throw new Error(`Index path ${indexPath} does not contain 'vscode-copilot', please ensure the index is generated in the correct workspace`);137}138139const subdir = indexPath.slice(lastIndex + REPO_NAME.length + 1);140const localRootRe = new RegExp(`^file:\\/\\/.*?${escapeRegExpCharacters(subdir.replaceAll('\\', '/'))}`, 'i');141142return {143fromDatabase: (uri: string) => uri.replace(localRootRe, simulationRootUri),144toDatabase: (uri: string) => uri.startsWith(simulationRootUri) ? uri.replace(simulationRootUri, indexRoot) : uri,145};146};147148/**149* A language features service powered by an LSIF index. To use this, you need150* to have generated an LSIF index for your workspace. This can be done in151* several ways:152*153* - Rust: ensure rust-analyzer is installed (rustup component add rust-analyzer)154* and run `rust-analyzer lsif ./ > lsif.json` in the workspace root.155*156* If you index a new language, please add instructions above.157*/158export class LSIFLanguageFeaturesService implements ILanguageFeaturesService {159_serviceBrand: undefined;160161private _graph: Promise<IGraph> | undefined;162163/**164* @param workspace The simulation workspace165* @param indexFilePath Path to an LSIF index file166*/167constructor(168private readonly workspace: SimulationWorkspace,169private readonly indexFilePath: string,170) { }171172private _getGraph(): Promise<IGraph> {173if (!this._graph) {174this._graph = this._load();175}176return this._graph;177}178179private async _load(): Promise<IGraph> {180if (this.indexFilePath.endsWith('.scip')) {181const contents = await fs.readFile(this.indexFilePath);182const index = scip.deserializeSCIP(contents);183return new SCIPGraph(index, this.workspace);184}185186const graph = new LSIF.JsonStore();187try {188await graph.load(this.indexFilePath, r => makeTranslator(this.workspace, r));189} catch (e) {190throw new Error(`Failed to parse LSIF index from ${this.indexFilePath}: ${e}`);191}192return graph;193}194195async getDocumentSymbols(uri: vscode.Uri): Promise<vscode.DocumentSymbol[]> {196throw new Error('Unimplemented: excercise for the reader');197}198199async getDefinitions(uri: vscode.Uri, position: vscode.Position): Promise<(vscode.LocationLink | vscode.Location)[]> {200const graph = await this._getGraph();201return liftLSIFLocations(graph.definitions(uri.toString(true), position));202}203204async getImplementations(uri: vscode.Uri, position: vscode.Position): Promise<(vscode.LocationLink | vscode.Location)[]> {205const graph = await this._getGraph();206return liftLSIFLocations(graph.declarations(uri.toString(true), position));207}208209async getReferences(uri: vscode.Uri, position: vscode.Position): Promise<vscode.Location[]> {210const graph = await this._getGraph();211return liftLSIFLocations(graph.references(uri.toString(true), position, { includeDeclaration: true }));212}213214getDiagnostics(_uri: vscode.Uri): vscode.Diagnostic[] {215return []; // not part of LSIF216}217218async getWorkspaceSymbols(query: string): Promise<vscode.SymbolInformation[]> {219throw new Error('Unimplemented: excercise for the reader');220// would have to iterate through all documents, get all symbols that match221}222}223224225