Add a JSON based language service (#175)

* Work an a LSIF language service module

* Add to @vscode namespace

* Add tests

* Move to language server types

* Add some basic tests

* Fix eslint errors

* More eslint fixes

* Add language service to all script
This commit is contained in:
Dirk Bäumer 2024-06-26 14:26:43 +02:00 коммит произвёл GitHub
Родитель da622ed284
Коммит ac447f8b20
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
19 изменённых файлов: 33405 добавлений и 5 удалений

3
.gitignore поставляемый
Просмотреть файл

@ -7,4 +7,5 @@ npm-debug.log
*.db
tsc-lsif/src/shared
npm-lsif/src/shared
util/junit.xml
util/junit.xml
!language-service/src/tests/dump.lsif

3
.vscode/settings.json поставляемый
Просмотреть файл

@ -15,7 +15,8 @@
"./npm",
"./sqlite",
"./tooling",
"./lsif"
"./lsif",
"./language-service"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"

Просмотреть файл

@ -20,7 +20,7 @@ if (args[0] === 'install' && process.env['npm_config_root_only'] === 'true') {
}
const folders = ['protocol', 'tsc', 'npm', 'sqlite', 'tooling', 'tsc-tests', 'lsif'];
const folders = ['protocol', 'tsc', 'npm', 'sqlite', 'tooling', 'tsc-tests', 'lsif', 'language-service'];
for (const folder of folders) {
console.log(`Running npm ${args.join(' ')} in ${folder}`);

Просмотреть файл

@ -67,6 +67,14 @@ try {
mkLink(path.join('..', '..', 'protocol'), 'lsif-protocol');
}
// Setup links for language-service
{
const sqlite = path.join(root, 'language-service', 'node_modules');
fs.mkdirSync(sqlite, { recursive: true });
process.chdir(sqlite);
mkLink(path.join('..', '..', 'protocol'), 'lsif-protocol');
}
// Setup symlinks for lsif commands
{
const lsif = path.join(root, 'lsif', 'node_modules')

Просмотреть файл

@ -0,0 +1,2 @@
lib
node_modules

Просмотреть файл

@ -0,0 +1,8 @@
{
"extends": "../.eslintrc.base.json",
"parserOptions": {
"project": ["./tsconfig.json"]
},
"rules": {
}
}

Просмотреть файл

@ -0,0 +1,6 @@
{
"spec": ["lib/tests"],
"extension": ["js"],
"recursive": true,
"ui": "tdd"
}

89
language-service/package-lock.json сгенерированный Normal file
Просмотреть файл

@ -0,0 +1,89 @@
{
"name": "@vscode/lsif-language-service",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@vscode/lsif-language-service",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"lsif-protocol": "0.6.0-next.7",
"semver": "^7.6.2",
"vscode-languageserver-types": "^3.17.5",
"vscode-uri": "^3.0.8"
},
"devDependencies": {
"@types/mocha": "^10.0.7",
"@types/semver": "^7.5.7"
}
},
"node_modules/@types/mocha": {
"version": "10.0.7",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.7.tgz",
"integrity": "sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true,
"license": "MIT"
},
"node_modules/lsif-protocol": {
"version": "0.6.0-next.7",
"resolved": "https://registry.npmjs.org/lsif-protocol/-/lsif-protocol-0.6.0-next.7.tgz",
"integrity": "sha512-P67czn1sCa4EhEFlRJWabieHB2poIuRHRO06mR5lLsTk0HqMrbM9McJaXlAJwOMylaxZ0HPpHrT9ij9BA4tzyw==",
"license": "MIT",
"dependencies": {
"vscode-languageserver-protocol": "^3.16.0"
}
},
"node_modules/semver": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/vscode-jsonrpc": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vscode-languageserver-protocol": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
"license": "MIT",
"dependencies": {
"vscode-jsonrpc": "8.2.0",
"vscode-languageserver-types": "3.17.5"
}
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
"license": "MIT"
},
"node_modules/vscode-uri": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
"license": "MIT"
}
}
}

Просмотреть файл

@ -0,0 +1,39 @@
{
"name": "@vscode/lsif-language-service",
"description": "LSIF based language services",
"private": true,
"version": "0.1.0",
"author": "Microsoft Corporation",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/Microsoft/lsif-node.git"
},
"bugs": {
"url": "https://github.com/Microsoft/lsif-node/issues"
},
"main": "lib/main.js",
"typings": "lib/main.d.ts",
"dependencies": {
"vscode-uri": "^3.0.8",
"semver": "^7.6.2",
"lsif-protocol": "0.6.0-next.7",
"vscode-languageserver-types": "^3.17.5"
},
"devDependencies": {
"@types/semver": "^7.5.7",
"@types/mocha": "^10.0.7"
},
"scripts": {
"compile": "tsc -b ./tsconfig.json",
"compile:publish": "tsc -p./tsconfig.publish.json",
"watch": "tsc -b ./tsconfig.watch.json -w",
"clean": "rimraf lib",
"clean:all": "tsc -b ./tsconfig.json --clean",
"lint": "eslint ./src/*.ts",
"test": "mocha",
"prepublishOnly": "git clean -xfd . && npm install && npm run clean && npm run compile:publish && npm run lint",
"postpublish": "node ../build/bin/post-publish.js",
"postinstall": ""
}
}

Просмотреть файл

@ -0,0 +1,123 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import { URI } from 'vscode-uri';
import * as lsp from 'vscode-languageserver-types';
import { Range, Id } from 'lsif-protocol';
import { FileType, FileSystem, DocumentInfo, FileStat } from './files';
export interface UriTransformer {
toDatabase(uri: string): string;
fromDatabase(uri: string): string;
}
export const noopTransformer: UriTransformer = {
toDatabase: uri => uri,
fromDatabase: uri => uri
};
export abstract class Database {
private fileSystem!: FileSystem;
private uriTransformer!: UriTransformer;
protected constructor() {
}
protected initialize(transformerFactory?: (workspaceRoot: string) => UriTransformer): void {
const workspaceRoot = this.getWorkspaceRoot().toString(true);
this.uriTransformer = transformerFactory ? transformerFactory(workspaceRoot) : noopTransformer;
this.fileSystem = new FileSystem(workspaceRoot, this.getDocumentInfos());
}
public abstract load(file: string, transformerFactory: (workspaceRoot: string) => UriTransformer): Promise<void>;
public abstract close(): void;
public abstract getWorkspaceRoot(): URI;
protected abstract getDocumentInfos(): DocumentInfo[];
public stat(uri: string): FileStat | null {
const transformed = this.uriTransformer.toDatabase(uri);
const result = this.fileSystem.stat(transformed);
if (result !== null) {
return result;
}
const id = this.findFile(transformed);
if (id === undefined) {
return null;
}
return FileStat.createFile();
}
public readDirectory(uri: string): [string, FileType][] {
return this.fileSystem.readDirectory(this.uriTransformer.toDatabase(uri));
}
public readFileContent(uri: string): string | null {
const transformed = this.uriTransformer.toDatabase(uri);
let info = this.fileSystem.getFileInfo(transformed);
if (info === undefined) {
info = this.findFile(transformed);
}
if (info === undefined) {
return null;
}
const result = this.fileContent(info);
if (result === undefined) {
return null;
}
return result;
}
protected abstract findFile(uri: string):{ id: Id; hash: string | undefined } | undefined;
protected abstract fileContent( info: { id: Id; hash: string | undefined } ) : string | undefined;
public abstract foldingRanges(uri: string): lsp.FoldingRange[] | undefined;
public abstract documentSymbols(uri: string): lsp.DocumentSymbol[] | undefined;
public abstract hover(uri: string, position: lsp.Position): lsp.Hover | undefined;
public abstract declarations(uri: string, position: lsp.Position): lsp.Location | lsp.Location[] | undefined;
public abstract definitions(uri: string, position: lsp.Position): lsp.Location | lsp.Location[] | undefined;
public abstract references(uri: string, position: lsp.Position, context: lsp.ReferenceContext): lsp.Location[] | undefined;
protected asDocumentSymbol(range: Range): lsp.DocumentSymbol | undefined {
const tag = range.tag;
if (tag === undefined || !(tag.type === 'declaration' || tag.type === 'definition')) {
return undefined;
}
return lsp.DocumentSymbol.create(
tag.text, tag.detail || '', tag.kind,
tag.fullRange, this.asRange(range)
);
}
protected asRange(value: Range): lsp.Range {
return {
start: {
line: value.start.line,
character: value.start.character
},
end: {
line: value.end.line,
character: value.end.character
}
};
}
protected toDatabase(uri: string): string {
return this.uriTransformer.toDatabase(uri);
}
protected fromDatabase(uri: string): string {
return this.uriTransformer.fromDatabase(uri);
}
}

Просмотреть файл

@ -0,0 +1,165 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import * as path from 'path';
import { Id } from 'lsif-protocol';
const ctime = Date.now();
const mtime = Date.now();
export namespace FileType {
export const Unknown: 0 = 0;
export const File: 1 = 1;
export const Directory: 2 = 2;
export const SymbolicLink: 64 = 64;
}
export type FileType = 0 | 1 | 2 | 64;
export interface FileStat {
type: FileType;
ctime: number;
mtime: number;
size: number;
}
export namespace FileStat {
export function createFile(): FileStat {
return { type: FileType.File, ctime: ctime, mtime: mtime, size: 0 };
}
}
export interface DocumentInfo {
id: Id;
uri: string;
hash: string;
}
interface File extends FileStat {
type: 1;
name: string;
id: Id;
hash: string;
}
namespace File {
export function create(name: string, id: Id, hash: string): File {
return { type: FileType.File, ctime: ctime, mtime: mtime, size: 0, name, id, hash };
}
}
interface Directory extends FileStat {
type: 2;
name: string;
children: Map<string, Entry>;
}
namespace Directory {
export function create(name: string): Directory {
return { type: FileType.Directory, ctime: Date.now(), mtime: Date.now(), size: 0, name, children: new Map() };
}
}
export type Entry = File | Directory;
export class FileSystem {
private workspaceRoot: string;
private workspaceRootWithSlash: string;
private filesOutsideWorkspaceRoot: Map<string, { id: Id; hash: string | undefined }>;
private root: Directory;
constructor(workspaceRoot: string, documents: DocumentInfo[]) {
if (workspaceRoot.charAt(workspaceRoot.length - 1) === '/') {
this.workspaceRoot = workspaceRoot.substr(0, workspaceRoot.length - 1);
this.workspaceRootWithSlash = workspaceRoot;
} else {
this.workspaceRoot = workspaceRoot;
this.workspaceRootWithSlash = workspaceRoot + '/';
}
this.root = Directory.create('');
this.filesOutsideWorkspaceRoot = new Map();
for (const info of documents) {
// Do not show file outside the workspaceRoot.
if (!info.uri.startsWith(this.workspaceRootWithSlash)) {
this.filesOutsideWorkspaceRoot.set(info.uri, info);
continue;
}
const p = info.uri.substring(workspaceRoot.length);
const dirname = path.posix.dirname(p);
const basename = path.posix.basename(p);
const entry = this.lookup(dirname, true);
if (entry && entry.type === FileType.Directory) {
entry.children.set(basename, File.create(basename, info.id, info.hash));
}
}
}
public stat(uri: string): FileStat | null {
if (this.filesOutsideWorkspaceRoot.has(uri)) {
return { type: FileType.File, ctime, mtime, size: 0 };
}
const isRoot = this.workspaceRoot === uri;
if (!uri.startsWith(this.workspaceRootWithSlash) && !isRoot) {
return null;
}
const p = isRoot ? '' : uri.substring(this.workspaceRootWithSlash.length);
const entry = this.lookup(p, false);
return entry ? entry : null;
}
public readDirectory(uri: string): [string, FileType][] {
const isRoot = this.workspaceRoot === uri;
if (!uri.startsWith(this.workspaceRootWithSlash) && !isRoot) {
return [];
}
const p = isRoot ? '' : uri.substring(this.workspaceRootWithSlash.length);
const entry = this.lookup(p, false);
if (entry === undefined || entry.type !== FileType.Directory) {
return [];
}
const result: [string, FileType][] = [];
for (const child of entry.children.values()) {
result.push([child.name, child.type]);
}
return result;
}
public getFileInfo(uri: string): { id: Id; hash: string | undefined } | undefined {
const result = this.filesOutsideWorkspaceRoot.get(uri);
if (result !== undefined) {
return result;
}
const isRoot = this.workspaceRoot === uri;
if (!uri.startsWith(this.workspaceRootWithSlash) && !isRoot) {
return undefined;
}
const entry = this.lookup(isRoot ? '' : uri.substring(this.workspaceRootWithSlash.length));
return entry && entry.type === FileType.File ? entry : undefined;
}
private lookup(uri: string, create: boolean = false): Entry | undefined {
const parts = uri.split('/');
let entry: Entry = this.root;
for (const part of parts) {
if (!part || part === '.') {
continue;
}
let child: Entry | undefined;
if (entry.type === FileType.Directory) {
child = entry.children.get(part);
if (child === undefined && create) {
child = Directory.create(part);
entry.children.set(part, child);
}
}
if (!child) {
return undefined;
}
entry = child;
}
return entry;
}
}

Просмотреть файл

@ -0,0 +1,732 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import * as fs from 'fs';
import * as crypto from 'crypto';
import * as readline from 'readline';
import { URI } from 'vscode-uri';
import * as SemVer from 'semver';
import * as lsp from 'vscode-languageserver-types';
import {
Id, Vertex, Project, Document, Range, DiagnosticResult, DocumentSymbolResult, FoldingRangeResult, DocumentLinkResult, DefinitionResult,
TypeDefinitionResult, HoverResult, ReferenceResult, ImplementationResult, Edge, RangeBasedDocumentSymbol, DeclarationResult,
ElementTypes, VertexLabels, EdgeLabels, ItemEdgeProperties, EventScope, EventKind, ProjectEvent, Moniker as PMoniker, MonikerKind
} from 'lsif-protocol';
import { DocumentInfo } from './files';
import { Database, UriTransformer } from './database';
interface Moniker extends PMoniker {
key: string;
}
interface Vertices {
all: Map<Id, Vertex>;
projects: Map<Id, Project>;
documents: Map<Id, Document>;
ranges: Map<Id, Range>;
}
type ItemTarget =
Range |
{ type: ItemEdgeProperties.declarations; range: Range } |
{ type: ItemEdgeProperties.definitions; range: Range } |
{ type: ItemEdgeProperties.references; range: Range } |
{ type: ItemEdgeProperties.referenceResults; result: ReferenceResult } |
{ type: ItemEdgeProperties.referenceLinks; result: Moniker };
interface Out {
contains: Map<Id, Document[] | Range[]>;
item: Map<Id, ItemTarget[]>;
next: Map<Id, Vertex>;
moniker: Map<Id, Moniker>;
documentSymbol: Map<Id, DocumentSymbolResult>;
foldingRange: Map<Id, FoldingRangeResult>;
documentLink: Map<Id, DocumentLinkResult>;
diagnostic: Map<Id, DiagnosticResult>;
declaration: Map<Id, DeclarationResult>;
definition: Map<Id, DefinitionResult>;
typeDefinition: Map<Id, TypeDefinitionResult>;
hover: Map<Id, HoverResult>;
references: Map<Id, ReferenceResult>;
implementation: Map<Id, ImplementationResult>;
}
interface In {
contains: Map<Id, Project | Document>;
moniker: Map<Id, Vertex[]>;
}
interface Indices {
monikers: Map<string, Moniker[]>;
contents: Map<string, string>;
documents: Map<string, { hash: string; documents: Document[] }>;
}
interface ResultPath<T> {
path: { vertex: Id; moniker: Moniker | undefined }[];
result: { value: T; moniker: Moniker | undefined } | undefined;
}
namespace Locations {
export function makeKey(location: lsp.Location): string {
const range = location.range;
return crypto.createHash('md5').update(JSON.stringify({ d: location.uri, sl: range.start.line, sc: range.start.character, el: range.end.line, ec: range.end.character }, undefined, 0)).digest('base64');
}
}
export class JsonStore extends Database {
private version: string | undefined;
private workspaceRoot!: URI;
protected activeProject: Id | undefined;
private vertices: Vertices;
private indices: Indices;
private out: Out;
private in: In;
constructor() {
super();
this.vertices = {
all: new Map(),
projects: new Map(),
documents: new Map(),
ranges: new Map()
};
this.indices = {
contents: new Map(),
documents: new Map(),
monikers: new Map(),
};
this.out = {
contains: new Map(),
item: new Map(),
next: new Map(),
moniker: new Map(),
documentSymbol: new Map(),
foldingRange: new Map(),
documentLink: new Map(),
diagnostic: new Map(),
declaration: new Map(),
definition: new Map(),
typeDefinition: new Map(),
hover: new Map(),
references: new Map(),
implementation: new Map()
};
this.in = {
contains: new Map(),
moniker: new Map()
};
}
public load(file: string, transformerFactory?: (workspaceRoot: string) => UriTransformer): Promise<void> {
return new Promise<void>((resolve, reject) => {
const input: fs.ReadStream = fs.createReadStream(file, { encoding: 'utf8'});
input.on('error', reject);
const rd = readline.createInterface(input);
rd.on('line', (line: string) => {
if (!line || line.length === 0) {
return;
}
try {
const element: Edge | Vertex = JSON.parse(line);
switch (element.type) {
case ElementTypes.vertex:
this.processVertex(element);
break;
case ElementTypes.edge:
this.processEdge(element);
break;
}
} catch (error) {
input.destroy();
reject(error);
}
});
rd.on('close', () => {
if (this.workspaceRoot === undefined) {
reject(new Error('No project root provided.'));
return;
}
if (this.version === undefined) {
reject(new Error('No version found.'));
return;
} else {
const semVer = SemVer.parse(this.version);
if (!semVer) {
reject(new Error(`No valid semantic version string. The version is: ${this.version}`));
return;
}
const range: SemVer.Range = new SemVer.Range('>0.5.99 <=0.6.0-next.7');
range.includePrerelease = true;
if (!SemVer.satisfies(semVer, range)) {
reject(new Error(`Requires version range >0.5.99 <=0.6.0-next.7 but received: ${this.version}`));
return;
}
}
resolve();
});
}).then(() => {
this.initialize(transformerFactory);
});
}
public getWorkspaceRoot(): URI {
return this.workspaceRoot;
}
public close(): void {
}
private processVertex(vertex: Vertex): void {
this.vertices.all.set(vertex.id, vertex);
switch(vertex.label) {
case VertexLabels.metaData:
this.version = vertex.version;
break;
case VertexLabels.source:
this.workspaceRoot = URI.parse(vertex.workspaceRoot);
break;
case VertexLabels.project:
this.vertices.projects.set(vertex.id, vertex);
break;
case VertexLabels.event:
if (vertex.kind === EventKind.begin) {
switch (vertex.scope) {
case EventScope.project:
this.activeProject = (vertex as ProjectEvent).data;
break;
}
}
break;
case VertexLabels.document:
this.doProcessDocument(vertex);
break;
case VertexLabels.moniker:
if (vertex.kind !== MonikerKind.local) {
const key = crypto.createHash('md5').update(JSON.stringify({ s: vertex.scheme, i: vertex.identifier }, undefined, 0)).digest('base64');
(vertex as Moniker).key = key;
let values = this.indices.monikers.get(key);
if (values === undefined) {
values = [];
this.indices.monikers.set(key, values);
}
values.push(vertex as Moniker);
}
break;
case VertexLabels.range:
this.vertices.ranges.set(vertex.id, vertex);
break;
}
}
private doProcessDocument(document: Document): void {
const contents = document.contents !== undefined ? document.contents : 'No content provided.';
this.vertices.documents.set(document.id, document);
const hash = crypto.createHash('md5').update(contents).digest('base64');
this.indices.contents.set(hash, contents);
let value = this.indices.documents.get(document.uri);
if (value === undefined) {
value = { hash, documents: [] };
this.indices.documents.set(document.uri, value);
}
if (hash !== value.hash) {
console.error(`Document ${document.uri} has different content.`);
}
value.documents.push(document);
}
private processEdge(edge: Edge): void {
let property: ItemEdgeProperties | undefined;
if (edge.label === 'item') {
property = edge.property;
}
if (Edge.is11(edge)) {
this.doProcessEdge(edge.label, edge.outV, edge.inV, property);
} else if (Edge.is1N(edge)) {
for (const inV of edge.inVs) {
this.doProcessEdge(edge.label, edge.outV, inV, property);
}
}
}
private doProcessEdge(label: EdgeLabels, outV: Id, inV: Id, property?: ItemEdgeProperties): void {
const from: Vertex | undefined = this.vertices.all.get(outV);
const to: Vertex | undefined = this.vertices.all.get(inV);
if (from === undefined) {
throw new Error(`No vertex found for Id ${outV}`);
}
if (to === undefined) {
throw new Error(`No vertex found for Id ${inV}`);
}
let values: any[] | undefined;
let itemTarget: ItemTarget | undefined;
switch (label) {
case EdgeLabels.contains:
values = this.out.contains.get(from.id);
if (values === undefined) {
values = [ to as any ];
this.out.contains.set(from.id, values);
} else {
values.push(to);
}
this.in.contains.set(to.id, from as any);
break;
case EdgeLabels.item:
values = this.out.item.get(from.id);
if (property !== undefined) {
switch (property) {
case ItemEdgeProperties.references:
itemTarget = { type: property, range: to as Range };
break;
case ItemEdgeProperties.declarations:
itemTarget = { type: property, range: to as Range };
break;
case ItemEdgeProperties.definitions:
itemTarget = { type: property, range: to as Range };
break;
case ItemEdgeProperties.referenceResults:
itemTarget = { type: property, result: to as ReferenceResult };
break;
case ItemEdgeProperties.referenceLinks:
itemTarget = { type: property, result: to as Moniker };
}
} else {
itemTarget = to as Range;
}
if (itemTarget !== undefined) {
if (values === undefined) {
values = [ itemTarget ];
this.out.item.set(from.id, values);
} else {
values.push(itemTarget);
}
}
break;
case EdgeLabels.next:
this.out.next.set(from.id, to);
break;
case EdgeLabels.moniker:
this.out.moniker.set(from.id, to as Moniker);
values = this.in.moniker.get(to.id);
if (values === undefined) {
values = [];
this.in.moniker.set(to.id, values);
}
values.push(from);
break;
case EdgeLabels.textDocument_documentSymbol:
this.out.documentSymbol.set(from.id, to as DocumentSymbolResult);
break;
case EdgeLabels.textDocument_foldingRange:
this.out.foldingRange.set(from.id, to as FoldingRangeResult);
break;
case EdgeLabels.textDocument_documentLink:
this.out.documentLink.set(from.id, to as DocumentLinkResult);
break;
case EdgeLabels.textDocument_diagnostic:
this.out.diagnostic.set(from.id, to as DiagnosticResult);
break;
case EdgeLabels.textDocument_definition:
this.out.definition.set(from.id, to as DefinitionResult);
break;
case EdgeLabels.textDocument_typeDefinition:
this.out.typeDefinition.set(from.id, to as TypeDefinitionResult);
break;
case EdgeLabels.textDocument_hover:
this.out.hover.set(from.id, to as HoverResult);
break;
case EdgeLabels.textDocument_references:
this.out.references.set(from.id, to as ReferenceResult);
break;
}
}
public getDocumentInfos(): DocumentInfo[] {
const result: DocumentInfo[] = [];
this.indices.documents.forEach((value, key) => {
// We take the id of the first document.
result.push({ uri: key, id: value.documents[0].id, hash: value.hash });
});
return result;
}
protected findFile(uri: string): { id: Id; hash: string } | undefined {
const result = this.indices.documents.get(uri);
if (result === undefined) {
return undefined;
}
return { id: result.documents[0].id, hash: result.hash };
}
protected fileContent(info: { id: Id; hash: string }): string | undefined {
return this.indices.contents.get(info.hash);
}
public foldingRanges(uri: string): lsp.FoldingRange[] | undefined {
const value = this.indices.documents.get(this.toDatabase(uri));
if (value === undefined) {
return undefined;
}
// Take the id of the first document with that content. We assume that
// all documents with the same content have the same folding ranges.
const id = value.documents[0].id;
const foldingRangeResult = this.out.foldingRange.get(id);
if (foldingRangeResult === undefined) {
return undefined;
}
const result: lsp.FoldingRange[] = [];
for (const item of foldingRangeResult.result) {
result.push(Object.assign(Object.create(null), item));
}
return result;
}
public documentSymbols(uri: string): lsp.DocumentSymbol[] | undefined {
const value = this.indices.documents.get(this.toDatabase(uri));
if (value === undefined) {
return undefined;
}
// Take the id of the first document with that content. We assume that
// all documents with the same content have the same document symbols.
const id = value.documents[0].id;
const documentSymbolResult = this.out.documentSymbol.get(id);
if (documentSymbolResult === undefined || documentSymbolResult.result.length === 0) {
return undefined;
}
const first = documentSymbolResult.result[0];
const result: lsp.DocumentSymbol[] = [];
if (lsp.DocumentSymbol.is(first)) {
for (const item of documentSymbolResult.result) {
result.push(Object.assign(Object.create(null), item));
}
} else {
for (const item of (documentSymbolResult.result as RangeBasedDocumentSymbol[])) {
const converted = this.toDocumentSymbol(item);
if (converted !== undefined) {
result.push(converted);
}
}
}
return result;
}
private toDocumentSymbol(value: RangeBasedDocumentSymbol): lsp.DocumentSymbol | undefined {
const range = this.vertices.ranges.get(value.id)!;
const tag = range.tag;
if (tag === undefined || !(tag.type === 'declaration' || tag.type === 'definition')) {
return undefined;
}
const result: lsp.DocumentSymbol = lsp.DocumentSymbol.create(
tag.text, tag.detail || '', tag.kind,
tag.fullRange, this.asRange(range)
);
if (value.children && value.children.length > 0) {
result.children = [];
for (const child of value.children) {
const converted = this.toDocumentSymbol(child);
if (converted !== undefined) {
result.children.push(converted);
}
}
}
return result;
}
public hover(uri: string, position: lsp.Position): lsp.Hover | undefined {
const ranges = this.findRangesFromPosition(this.toDatabase(uri), position);
if (ranges === undefined) {
return undefined;
}
// We assume that for the same document URI the same position results in the same
// hover. So we take the first range.
const range = ranges[0];
const hoverResult = this.getResultPath(range.id, this.out.hover).result?.value;
if (hoverResult === undefined) {
return undefined;
}
const hoverRange = hoverResult.result.range !== undefined ? hoverResult.result.range : range;
return {
contents: hoverResult.result.contents,
range: hoverRange
};
}
public declarations(uri: string, position: lsp.Position): lsp.Location | lsp.Location[] | undefined {
return this.findTargets(uri, position, this.out.declaration);
}
public definitions(uri: string, position: lsp.Position): lsp.Location | lsp.Location[] | undefined {
return this.findTargets(uri, position, this.out.definition);
}
private findTargets<T extends (DefinitionResult | DeclarationResult)>(uri: string, position: lsp.Position, edges: Map<Id, T>): lsp.Location | lsp.Location[] | undefined {
const ranges = this.findRangesFromPosition(this.toDatabase(uri), position);
if (ranges === undefined) {
return undefined;
}
const resolveTargets = (result: lsp.Location[], dedupLocations: Set<string>, targetResult: T): void => {
const ranges = this.item(targetResult);
if (ranges === undefined) {
return undefined;
}
for (const element of ranges) {
this.addLocation(result, element, dedupLocations);
}
};
const _findTargets = (result: lsp.Location[], dedupLocations: Set<string>, dedupMonikers: Set<string>, range: Range): void => {
const resultPath = this.getResultPath(range.id, edges);
if (resultPath.result === undefined) {
return undefined;
}
const mostSpecificMoniker = this.getMostSpecificMoniker(resultPath);
const monikers: Moniker[] = mostSpecificMoniker !== undefined ? [mostSpecificMoniker] : [];
resolveTargets(result, dedupLocations, resultPath.result.value);
for (const moniker of monikers) {
if (dedupMonikers.has(moniker.key)) {
continue;
}
dedupMonikers.add(moniker.key);
const matchingMonikers = this.indices.monikers.get(moniker.key);
if (matchingMonikers !== undefined) {
for (const matchingMoniker of matchingMonikers) {
const vertices = this.findVerticesForMoniker(matchingMoniker);
if (vertices !== undefined) {
for (const vertex of vertices) {
const resultPath = this.getResultPath(vertex.id, edges);
if (resultPath.result === undefined) {
continue;
}
resolveTargets(result, dedupLocations, resultPath.result.value);
}
}
}
}
}
};
const result: lsp.Location[] = [];
const dedupLocations: Set<string> = new Set();
const dedupMonikers: Set<string> = new Set();
for (const range of ranges) {
_findTargets(result, dedupLocations, dedupMonikers, range);
}
return result;
}
public references(uri: string, position: lsp.Position, context: lsp.ReferenceContext): lsp.Location[] | undefined {
const ranges = this.findRangesFromPosition(this.toDatabase(uri), position);
if (ranges === undefined) {
return undefined;
}
const findReferences = (result: lsp.Location[], dedupLocations: Set<string>, dedupMonikers: Set<string>, range: Range): void => {
const resultPath = this.getResultPath(range.id, this.out.references);
if (resultPath.result === undefined) {
return;
}
const mostSpecificMoniker = this.getMostSpecificMoniker(resultPath);
const monikers: Moniker[] = mostSpecificMoniker !== undefined ? [mostSpecificMoniker] : [];
this.resolveReferenceResult(result, dedupLocations, monikers, resultPath.result.value, context);
for (const moniker of monikers) {
if (dedupMonikers.has(moniker.key)) {
continue;
}
dedupMonikers.add(moniker.key);
const matchingMonikers = this.indices.monikers.get(moniker.key);
if (matchingMonikers !== undefined) {
for (const matchingMoniker of matchingMonikers) {
if (moniker.id === matchingMoniker.id) {
continue;
}
const vertices = this.findVerticesForMoniker(matchingMoniker);
if (vertices !== undefined) {
for (const vertex of vertices) {
const resultPath = this.getResultPath(vertex.id, this.out.references);
if (resultPath.result === undefined) {
continue;
}
this.resolveReferenceResult(result, dedupLocations, monikers, resultPath.result.value, context);
}
}
}
}
}
};
const result: lsp.Location[] = [];
const dedupLocations: Set<string> = new Set();
const dedupMonikers: Set<string> = new Set();
for (const range of ranges) {
findReferences(result, dedupLocations, dedupMonikers, range);
}
return result;
}
private getResultPath<T>(start: Id, edges: Map<Id, T>): ResultPath<T> {
let currentId = start;
const result: ResultPath<T> = { path: [], result: undefined };
do {
const value: T | undefined = edges.get(currentId);
const moniker: Moniker | undefined = this.out.moniker.get(currentId);
if (value !== undefined) {
result.result = { value, moniker };
return result;
}
result.path.push({ vertex: currentId, moniker });
const next = this.out.next.get(currentId);
if (next === undefined) {
return result;
}
currentId = next.id;
} while (true);
}
private getMostSpecificMoniker<T>(result: ResultPath<T>): Moniker | undefined {
if (result.result?.moniker !== undefined) {
return result.result.moniker;
}
for (let i = result.path.length - 1; i >= 0; i--) {
if (result.path[i].moniker !== undefined) {
return result.path[i].moniker;
}
}
return undefined;
}
private findVerticesForMoniker(moniker: Moniker): Vertex[] | undefined {
return this.in.moniker.get(moniker.id);
}
private resolveReferenceResult(locations: lsp.Location[], dedupLocations: Set<string>, monikers: Moniker[], referenceResult: ReferenceResult, context: lsp.ReferenceContext): void {
const targets = this.item(referenceResult);
if (targets === undefined) {
return undefined;
}
for (const target of targets) {
if (target.type === ItemEdgeProperties.declarations && context.includeDeclaration) {
this.addLocation(locations, target.range, dedupLocations);
} else if (target.type === ItemEdgeProperties.definitions && context.includeDeclaration) {
this.addLocation(locations, target.range, dedupLocations);
} else if (target.type === ItemEdgeProperties.references) {
this.addLocation(locations, target.range, dedupLocations);
} else if (target.type === ItemEdgeProperties.referenceResults) {
this.resolveReferenceResult(locations, dedupLocations, monikers, target.result, context);
} else if (target.type === ItemEdgeProperties.referenceLinks) {
monikers.push(target.result);
}
}
}
private item(value: DefinitionResult | DeclarationResult): Range[];
private item(value: ReferenceResult): ItemTarget[];
private item(value: DeclarationResult | DefinitionResult | ReferenceResult): Range[] | ItemTarget[] | undefined {
if (value.label === 'declarationResult') {
return this.out.item.get(value.id) as Range[];
} else if (value.label === 'definitionResult') {
return this.out.item.get(value.id) as Range[];
} else if (value.label === 'referenceResult') {
return this.out.item.get(value.id) as ItemTarget[];
} else {
return undefined;
}
}
private addLocation(result: lsp.Location[], value: Range | lsp.Location, dedup: Set<string>): void {
let location: lsp.Location;
if (lsp.Location.is(value)) {
location = value;
} else {
const document = this.in.contains.get(value.id)!;
location = lsp.Location.create(this.fromDatabase((document as Document).uri), this.asRange(value));
}
const key = Locations.makeKey(location);
if (!dedup.has(key)) {
dedup.add(key);
result.push(location);
}
}
private findRangesFromPosition(file: string, position: lsp.Position): Range[] | undefined {
const value = this.indices.documents.get(file);
if (value === undefined) {
return undefined;
}
const result: Range[] = [];
for (const document of value.documents) {
const id = document.id;
const contains = this.out.contains.get(id);
if (contains === undefined || contains.length === 0) {
return undefined;
}
let candidate: Range | undefined;
for (const item of contains) {
if (item.label !== VertexLabels.range) {
continue;
}
if (JsonStore.containsPosition(item, position)) {
if (!candidate) {
candidate = item;
} else {
if (JsonStore.containsRange(candidate, item)) {
candidate = item;
}
}
}
}
if (candidate !== undefined) {
result.push(candidate);
}
}
return result.length > 0 ? result : undefined;
}
private static containsPosition(range: lsp.Range, position: lsp.Position): boolean {
if (position.line < range.start.line || position.line > range.end.line) {
return false;
}
if (position.line === range.start.line && position.character < range.start.character) {
return false;
}
if (position.line === range.end.line && position.character > range.end.character) {
return false;
}
return true;
}
/**
* Test if `otherRange` is in `range`. If the ranges are equal, will return true.
*/
public static containsRange(range: lsp.Range, otherRange: lsp.Range): boolean {
if (otherRange.start.line < range.start.line || otherRange.end.line < range.start.line) {
return false;
}
if (otherRange.start.line > range.end.line || otherRange.end.line > range.end.line) {
return false;
}
if (otherRange.start.line === range.start.line && otherRange.start.character < range.start.character) {
return false;
}
if (otherRange.end.line === range.end.line && otherRange.end.character > range.end.character) {
return false;
}
return true;
}
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -0,0 +1,30 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import assert from 'assert';
import { JsonStore } from '../jsonStore';
import path from 'path';
suite('JSON Dump', async () => {
let store: JsonStore;
setup(async () => {
store = new JsonStore();
await store.load(path.join(__dirname, '..', '..', 'src', 'tests', 'dump.lsif'));
});
test('document symbols', async () => {
const symbols = store.documentSymbols('file:///lsif-node/protocol/src/protocol.ts');
assert.ok(symbols !== undefined);
assert.strictEqual(symbols!.length, 128);
});
test('references', async () => {
const references = store.references('file:///lsif-node/protocol/src/protocol.ts', { line: 7, character: 11 }, { includeDeclaration: true });
assert.ok(references !== undefined);
assert.strictEqual(references!.length, 10);
});
});

Просмотреть файл

@ -0,0 +1,19 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true,
"sourceMap": true,
"declaration": true,
"composite": true,
"rootDir": "./src",
"outDir": "./lib",
"tsBuildInfoFile":"./lib/compile.tsbuildInfo"
},
"include": [
"src/**/*.ts"
],
"references": [
{ "path": "../protocol/tsconfig.json"}
]
}

Просмотреть файл

@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"references": [
]
}

Просмотреть файл

@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noUnusedLocals": false,
"noUnusedParameters": false,
"assumeChangesOnlyAffectDirectDependencies": true,
"tsBuildInfoFile":"./lib/watch.tsbuildInfo"
},
"references": [
{ "path": "../protocol/tsconfig.watch.json"}
]
}

Просмотреть файл

@ -11,6 +11,7 @@
{ "path": "./tsc-tests/tsconfig.json" },
{ "path": "./npm/tsconfig.json" },
{ "path": "./sqlite/tsconfig.json" },
{ "path": "./lsif/tsconfig.json" }
{ "path": "./lsif/tsconfig.json" },
{ "path": "./language-service/tsconfig.json"}
]
}

Просмотреть файл

@ -11,6 +11,7 @@
{ "path": "./tsc-tests/tsconfig.watch.json" },
{ "path": "./npm/tsconfig.watch.json" },
{ "path": "./sqlite/tsconfig.watch.json" },
{ "path": "./lsif/tsconfig.watch.json" }
{ "path": "./lsif/tsconfig.watch.json" },
{ "path": "./language-service/tsconfig.json"}
]
}