Language Server Index Format Spezifikation - 0.6.0
Die Version 0.6.0 von LSIF befindet sich derzeit im Aufbau.
Language Server Index Format
Der Zweck des Language Server Index Format (LSIF) ist es, ein Standardformat für Language Server oder andere Programmierwerkzeuge zu definieren, um deren Wissen über einen Arbeitsbereich zu exportieren. Dieser Export kann später verwendet werden, um Anfragen des Language Servers LSP für denselben Arbeitsbereich zu beantworten, ohne den Language Server selbst auszuführen. Da viele Informationen durch eine Änderung des Arbeitsbereichs ungültig würden, schließen die exportierten Informationen typischerweise Anfragen aus, die zum Modifizieren eines Dokuments verwendet werden. So ist beispielsweise das Ergebnis einer Code-Vervollständigungsanfrage typischerweise nicht Teil eines solchen Exports.
Changelog
Version 0.6.0
Feedback von Implementierern von Speichern zeigte, dass das Konzept der Gruppierung von Projekten in größere Speichereinheiten nichts ist, das in LSIF selbst definiert werden sollte. Dies sollte dem Speicher-Backend überlassen werden. Aus diesem Grund wurde der in 0.5.0 eingeführte Group Vertex wieder entfernt. Da einige Informationen, die im Group Vertex erfasst wurden, allgemein nützlich sind, wurde ein Source Vertex eingeführt, um diese Informationen zu speichern.
- Unterstützung für semantische Farbgebung von Dateien über LSIF. Zur Unterstützung dieser Funktion wurde die Anfrage 'textDocument/semanticTokens/full' hinzugefügt.
Version 0.5.0
In Version 0.4.0 wurde die Unterstützung hinzugefügt, um größere Systeme Projekt für Projekt (in umgekehrter Abhängigkeitsreihenfolge) zu exportieren und dann die Exporte in einer Datenbank zu kombinieren, indem Ergebnismengen über ihre entsprechenden Moniker verknüpft werden. Die Verwendung des Formats hat gezeigt, dass einige Funktionen fehlen, um dies gut zu ermöglichen
- Unterstützung für die logische Gruppierung von Projekten. Zur Unterstützung dieser Funktion wurde ein
GroupVertex hinzugefügt. - Wissen darüber, wie eindeutig ein Moniker ist. Zur Unterstützung dieser Funktion wurde der Eigenschaft
uniquedesMonikerhinzugefügt. - Der
nextMoniker-Edge wurde durch einen generischerenattach-Edge ersetzt. Dies war möglich, da Moniker nun eine Eigenschaftuniquetragen, die zuvor in der Richtung desnextMoniker-Edges kodiert war. - In Programmiersprachen, die Polymorphie unterstützen, können Aufrufe zur Laufzeit an einen anderen Typ gebunden werden, als statisch bekannt ist. Ein Beispiel sind überschriebene Methoden in objektorientierten Programmiersprachen. Da Exporte auf Pro-Projekt-Basis erfolgen können, müssen wir den Exporten zusätzliche Informationen hinzufügen, damit diese polymorphen Bindungen erfasst werden können. Das allgemeine Konzept von Referenzlinks wurde daher eingeführt (siehe Abschnitt Mehrere Projekte). Kurz gesagt, es ermöglicht einem Werkzeug, einen
item-Edge mit den EigenschaftswertenreferenceLinkszu annotieren. - Um die Ausgabe besser in Chunks aufzuteilen, trägt der
items-Edge eine zusätzliche Eigenschaftshard. Diese Eigenschaft hieß in einer frühen Version der 0.5-Spezifikationdocument.
Eine alte Version 0.4.0 der Spezifikation ist hier verfügbar
Version 0.4.0
Bis zur Version 0.4.0 lag der Fokus des LSIF-Formats darin, die Erstellung des Dumps für Language-Tool-Anbieter zu erleichtern. Dies machte es für Konsumenten des Dumps jedoch sehr schwierig, ihn effizient in eine Datenbank zu importieren, es sei denn, das Datenbankformat entsprach eins zu eins dem LSIF-Format. Diese Version der Spezifikation versucht, dies auszubalancieren, indem sie von Tool-Anbietern verlangt, zusätzliche Ereignisse auszugeben, wenn bestimmte Daten zum Konsum bereit sind. Sie fügt auch Unterstützung für die Partitionierung von Daten pro Dokument hinzu.
Da 0.4.0 einige LSIF-Aspekte tiefergreifend ändert, ist eine alte 0.3.x Version der Spezifikation hier verfügbar.
Motivation
Hauptentwicklungsziele
- Das Format sollte nicht die Verwendung einer bestimmten Persistenztechnologie implizieren.
- Die definierten Daten sollten so nah wie möglich am Language Server Protocol modelliert werden, um es zu ermöglichen, die Daten über LSP ohne weitere Transformation bereitzustellen.
- Die gespeicherten Daten sind Ergebnisdaten, die normalerweise von einer LSP-Anfrage zurückgegeben werden. Der Dump enthält keine Programmsymbolinformationen und LSIF definiert auch keine Symbolsemantik (z. B. wo ein Symbol definiert oder referenziert wird oder wann eine Methode eine andere überschreibt). LSIF definiert daher keine Symbol-Datenbank. Bitte beachten Sie, dass dies im Einklang mit LSP selbst steht, das ebenfalls keine Symbolsemantik definiert.
- Das Ausgabeformat basiert, wie bei LSP, auf JSON.
LSP-Anfragen, die sich gut für die Unterstützung in LSIF eignen, sind
textDocument/documentSymboltextDocument/foldingRangetextDocument/documentLinktextDocument/definitiontextDocument/declarationtextDocument/typeDefinitiontextDocument/hovertextDocument/referencestextDocument/implementation
Die entsprechenden LSP-Anfragen haben eine der folgenden beiden Formen
request(uri, method) -> result
request(uri, position, method) -> result
wobei `method` die JSON-RPC-Anfragemethode ist.
Konkrete Beispiele sind
request(
'file:///Users/dirkb/sample/test.ts',
'textDocument/foldingRange'
) -> FoldingRange[];
request(
'file:///Users/dirkb/sample/test.ts',
{ line: 10, character: 17 },
'textDocument/hover'
) -> Hover;
Das Eingabetupel für eine Anfrage ist entweder [uri, method] oder [uri, position, method] und die Ausgabe ist eine Form von Ergebnis. Für dasselbe uri und Tupel [uri, position] gibt es viele verschiedene Anfragen, die ausgeführt werden müssen.
Das Dump-Format sollte daher die folgenden Funktionen unterstützen
- Eingabedaten müssen einfach abfragbar sein (z. B. das Dokument und die Position).
- Jedes Element hat eine eindeutige ID (die eine Zeichenkette oder eine Zahl sein kann).
- Es sollte möglich sein, Daten auszugeben, sobald sie verfügbar sind, um Streaming statt großer Speicheranforderungen zu ermöglichen. Beispielsweise sollten Daten basierend auf der Dokumentensyntax für jede Datei ausgegeben werden, während die Analyse fortschreitet.
- Es sollte einfach sein, später zusätzliche Anfragen hinzuzufügen.
- Es sollte für ein Werkzeug einfach sein, einen Export zu verarbeiten und ihn zum Beispiel in eine Datenbank zu importieren, ohne den Export im Speicher zu halten.
Wir sind zu dem Schluss gekommen, dass der flexibelste Weg zur Ausgabe dies ein Graph ist, bei dem Kanten die Methode darstellen und Knoten [uri], [uri, position] oder ein Anfrageergebnis sind. Diese Daten könnten dann als JSON gespeichert oder in eine Datenbank geladen werden, die diese Knoten und Beziehungen darstellen kann.
Angenommen, es gibt eine Datei /Users/dirkb/sample.ts und wir möchten die Faltungsbereichsinformationen dazu speichern, dann gibt der Indexer zwei Knoten aus: einen, der das Dokument mit seiner URI file:///Users/dirkb/sample.ts repräsentiert, und einen anderen, der das Faltungsergebnis repräsentiert. Zusätzlich wird eine Kante ausgegeben, die die Anfrage textDocument/foldingRange repräsentiert.
{ id: 1, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
{ id: 2, type: "vertex", label: "foldingRangeResult",
result: [ { ... }, { ... }, ... ]
}
{ id: 3, type: "edge", label: "textDocument/foldingRange", outV: 1, inV: 2 }
Der entsprechende Graph sieht wie folgt aus

Bereiche (Ranges)
Für Anfragen, die eine Position als Eingabe nehmen, müssen wir auch die Position speichern. Normalerweise geben LSP-Anfragen dasselbe Ergebnis für Positionen zurück, die auf dasselbe Wort/denselben Namen in einem Dokument zeigen. Nehmen wir das folgende TypeScript-Beispiel
function bar() {
}
Eine Hover-Anfrage für eine Position, die das b in bar bezeichnet, gibt dasselbe Ergebnis zurück wie eine Position, die das a oder r bezeichnet. Um den Dump kompakter zu gestalten, werden stattdessen Bereiche (Ranges) verwendet, um dies zu erfassen, anstatt einzelne Positionen. In diesem Fall werden die folgenden Knoten ausgegeben. Beachten Sie, dass Zeile und Spalte nullbasiert sind, wie in LSP
{ id: 4, type: "vertex", label: "range",
start: { line: 0, character: 9}, end: { line: 0, character: 12 }
}
Um den Bereich an ein Dokument zu binden, verwenden wir eine spezielle Kante mit der Bezeichnung contains, die von einem Dokument zu einer Menge von Bereichen zeigt.
{ id: 5, type: "edge", label: "contains", outV: 1, inVs: [4] }
LSIF unterstützt 1:n-Kanten für die contains-Beziehung, die in einem Graphen leicht auf n 1:1-Kanten abgebildet werden kann. LSIF unterstützt dies aus zwei Gründen: (a) um die Ausgabe kompakter zu gestalten, da ein Dokument normalerweise Hunderte dieser Bereiche enthält, und (b) um den Import und die Stapelverarbeitung für Konsumenten eines LSIF-Dumps zu erleichtern.
Um das Hover-Ergebnis an den Bereich zu binden, verwenden wir dasselbe Muster, das wir für Faltungsbereiche verwendet haben. Wir geben einen Knoten aus, der das Hover-Ergebnis repräsentiert, und eine Kante, die die Anfrage textDocument/hover repräsentiert.
{
id: 6,
type: "vertex",
label: "hoverResult",
result: {
contents: [
{ language: "typescript", value: "function bar(): void" }
]
}
}
{ id: 7, type: "edge", label: "textDocument/hover", outV: 4, inV: 6 }
Der entsprechende Graph sieht wie folgt aus

Die für ein Dokument im `contains`-Verhältnis ausgegebenen Bereiche müssen diese Regeln befolgen
- eine gegebene Bereichs-ID kann nur in einem Dokument enthalten sein, d. h. Bereiche dürfen nicht über Dokumente hinweg geteilt werden, auch wenn sie denselben Start-/Endwert haben.
- Keine zwei Bereiche können gleich sein.
- Keine zwei Bereiche dürfen sich überschneiden und dieselbe Position in einem Dokument beanspruchen, es sei denn, ein Bereich ist vollständig in einem anderen enthalten.
Wenn eine Position in einem Dokument einem Bereich zugeordnet wird und mehr als ein Bereich die Position abdeckt, sollte der folgende Algorithmus verwendet werden
- sortiere die Bereiche nach Einbettungstiefe mit der innersten zuerst
- für jeden Bereich in den Bereichen do
- prüfe, ob der Bereich eine ausgehende Kante
textDocument/${method}hat - wenn ja, verwende sie
- prüfe, ob der Bereich eine ausgehende Kante
- end
- gib
nullzurück
Ergebnis-Menge (Result Set)
Normalerweise ist das Hover-Ergebnis dasselbe, ob man über eine Definition einer Funktion oder über eine Referenz dieser Funktion schwebt. Dasselbe gilt tatsächlich für viele LSP-Anfragen wie textDocument/definition, textDocument/references oder textDocument/typeDefinition. In einem naiven Modell hätte jeder Bereich ausgehende Kanten für all diese LSP-Anfragen und würde auf die entsprechenden Ergebnisse zeigen. Um dies zu optimieren und den Graphen leichter verständlich zu machen, wird das Konzept einer ResultSet eingeführt. Eine Ergebnis-Menge dient als Drehscheibe, um Informationen zu speichern, die für viele Bereiche gemeinsam sind. Die ResultSet selbst trägt keine Informationen. Sie sieht also so aus
export interface ResultSet {
}
Die entsprechende Ausgabe des obigen Beispiels mit einem Hover, der eine Ergebnis-Menge verwendet, sieht so aus
{ id: 1, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
{ id: 2, type: "vertex", label: "resultSet" }
{ id: 3, type: "vertex", label: "range",
start: { line: 0, character: 9}, end: { line: 0, character: 12 }
}
{ id: 4, type: "edge", label: "contains", outV: 1, inVs: [3] }
{ id: 5, type: "edge", label: "next", outV: 3, inV: 2 }
{ id: 6, type: "vertex", label: "hoverResult",
result: {
"contents":[ {
language: "typescript", value:"function bar(): void"
}]
}
}
{ id: 7, type: "edge", label: "textDocument/hover", outV: 2, inV: 6 }

Ergebnis-Mengen werden mit einer next-Kante mit Bereichen verknüpft. Eine Ergebnis-Menge kann auch Informationen an eine andere Ergebnis-Menge weiterleiten, indem sie mit einer next-Kante darauf verweist.
Das Muster zum Speichern des Ergebnisses mit der ResultSet wird auch für andere Anfragen verwendet. Der Suchalgorithmus für eine Anfrage [document, position, method] ist daher wie folgt
- finde alle Bereiche für [document, position]. Wenn keine existieren, gib
nullals Ergebnis zurück - sortiere die Bereiche nach Einbettungstiefe, die innerste zuerst
- für jeden Bereich in den Bereichen do
- weise den Bereich `out` zu
- while out !==
null- prüfe, ob `out` eine ausgehende Kante
textDocument/${method}hat. Wenn ja, verwende sie und gib das entsprechende Ergebnis zurück - prüfe, ob `out` eine ausgehende
next-Kante hat. Wenn ja, setze `out` auf den Zielknoten. Andernfalls setze `out` aufnull
- prüfe, ob `out` eine ausgehende Kante
- end
- end
- andernfalls gib
nullzurück
Sprachfunktionen
Anfrage: textDocument/definition
Dasselbe Muster, eine Range, eine Ergebnis-Menge oder ein Dokument mit einer Anfragekante zu einem Methoden-Ergebnis zu verbinden, wird auch für andere Anfragen verwendet. Betrachten wir als Nächstes die Anfrage textDocument/definition unter Verwendung des folgenden TypeScript-Beispiels
function bar() {
}
function foo() {
bar();
}
Dies gibt die folgenden Knoten und Kanten aus, um die Anfrage textDocument/definition zu modellieren
// The document
{ id: 4, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
// The result set
{ id: 6, type: "vertex", label: "resultSet" }
// The bar declaration
{ id: 9, type: "vertex", label: "range",
start: { line: 0, character: 9 }, end: { line: 0, character: 12 }
}
{ id: 10, type: "edge", label: "next", outV: 9, inV: 6 }
// The bar reference
{ id: 20, type: "vertex", label: "range",
start: { line: 4, character: 2 }, end: { line: 4, character: 5 }
}
{ id: 21, type: "edge", label: "next", outV: 20, inV: 6}
// The definition result linked to the bar result set
{ id: 22, type: "vertex", label: "definitionResult" }
{ id: 23, type: "edge", label: "textDocument/definition", outV: 6, inV: 22 }
{ id: 24, type: "edge", label: "item", outV: 22, inVs: [9], shard: 4 }

Das Definitions-Ergebnis oben hat nur einen Wert (die Range mit der ID ‘9’) und wir hätten es direkt ausgeben können. Wir haben jedoch den Knoten `definitionResult` aus zwei Gründen eingeführt
- Um Konsistenz mit allen anderen Anfragen zu gewährleisten, die auf ein Ergebnis verweisen.
- Um Unterstützung für Sprachen zu haben, bei denen eine Definition über mehrere Bereiche oder sogar mehrere Dokumente verteilt sein kann. Um mehrere Dokumente zu unterstützen, werden Bereiche zu einem Definitions-Ergebnis über eine 1:N
item-Kante hinzugefügt. Konzeptionell ist ein Definitions-Ergebnis ein Array, dem dieitem-Kante Elemente hinzufügt.
Betrachten Sie das folgende TypeScript-Beispiel
interface X {
foo();
}
interface X {
bar();
}
let x: X;
Wenn Sie in let x: X auf X klicken, wird ein Dialog angezeigt, der dem Benutzer die Auswahl zwischen den beiden Definitionen des interface X ermöglicht. Das ausgegebene JSON sieht in diesem Fall wie folgt aus
{ id : 38, type: "vertex", label: "definitionResult" }
{ id : 40, type: "edge", label: "item", outV: 38, inVs: [9, 13], shard: 4 }
Der item-Edge hat eine zusätzliche Eigenschaft shard, die auf den Vertex verweist, der die Quelle (z. B. ein Dokument oder ein Projekt) dieser Deklarationen ist. Wir haben diese Information hinzugefügt, um es immer noch einfach zu machen, die Daten auszugeben, aber auch einfach, die Daten zu verarbeiten und zu sharden, wenn sie in einer Datenbank gespeichert werden. Ohne diese Information müssten wir entweder eine Reihenfolge festlegen, in der Daten ausgegeben werden müssen (z. B. ein item-Edge, der sich nur auf einen Bereich bezieht, der bereits mit einem contains-Edge zu einem Dokument hinzugefügt wurde) oder wir zwingen Verarbeitungswerkzeuge, viele Vertices und Edges im Speicher zu behalten. Der Ansatz, diese shard-Eigenschaft zu haben, scheint ein fairer Ausgleich zu sein.
Anfrage: textDocument/declaration
Es gibt Programmiersprachen, die das Konzept von Deklarationen und Definitionen kennen (wie C/C++). In diesem Fall kann der Dump einen entsprechenden declarationResult-Knoten und eine textDocument/declaration-Kante enthalten, um die Informationen zu speichern. Sie werden analog zu den Entitäten behandelt, die für die Anfrage textDocument/definition ausgegeben werden.
Mehr zur Anfrage: textDocument/hover
In LSP wird der Hover wie folgt definiert
export interface Hover {
/**
* The hover's content
*/
contents: MarkupContent | MarkedString | MarkedString[];
/**
* An optional range
*/
range?: Range;
}
wobei der optionale Bereich der Namensbereich des Wortes ist, über das gehovert wird.
Seitennotiz: Dies ist ein Muster, das auch für andere LSP-Anfragen verwendet wird, bei denen das Ergebnis den Wortbereich des Wortes enthält, auf das der Positions-Parameter gezeigt hat.
Dies macht den Hover für jede Position unterschiedlich, daher können wir ihn nicht wirklich mit der Ergebnis-Menge speichern. Aber warten Sie, der Bereich ist der Bereich einer der bar-Referenzen, die wir bereits ausgegeben und verwendet haben, um mit der Berechnung des Ergebnisses zu beginnen. Um den Hover wiederverwendbar zu machen, bitten wir den Indexserver, den Startbereich zu füllen, wenn im Ergebnis kein Bereich definiert ist. Bei einer Hover-Anfrage, die auf dem Bereich { line: 4, character: 2 }, end: { line: 4, character: 5 } ausgeführt wird, ist das Hover-Ergebnis
{ id: 6, type: "vertex", label: "hoverResult",
result: {
contents: [ { language: "typescript", value: "function bar(): void" } ],
range: { line: 4, character: 2 }, end: { line: 4, character: 5 }
}
}
Anfrage: textDocument/references
Das Speichern von Referenzen erfolgt auf die gleiche Weise wie das Speichern von Hover- oder Go-to-Definition-Bereichen. Es verwendet einen `referenceResult`-Knoten und `item`-Kanten, um Bereiche zum Ergebnis hinzuzufügen.
Betrachten Sie das folgende Beispiel
function bar() {
}
function foo() {
bar();
}
Die relevante JSON-Ausgabe sieht so aus
// The document
{ id: 4, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
// The bar declaration
{ id: 6, type: "vertex", label: "resultSet" }
{ id: 9, type: "vertex", label: "range",
start: { line: 0, character: 9 }, end: { line: 0, character: 12 }
}
{ id: 10, type: "edge", label: "next", outV: 9, inV: 6 }
// The bar reference range
{ id: 20, type: "vertex", label: "range",
start: { line: 4, character: 2 }, end: { line: 4, character: 5 }
}
{ id: 21, type: "edge", label: "next", outV: 20, inV: 6 }
// The reference result
{ id : 25, type: "vertex", label: "referenceResult" }
// Link it to the result set
{ id : 26, type: "edge", label: "textDocument/references", outV: 6, inV: 25 }
// Add the bar definition as a reference to the reference result
{ id: 27, type: "edge", label: "item",
outV: 25, inVs: [9], shard: 4, property: "definitions"
}
// Add the bar reference as a reference to the reference result
{ id: 28, type: "edge", label: "item",
outV: 25, inVs: [20], shard: 4, property: "references"
}

Wir kennzeichnen die item-Kante mit der ID 27 als Definition, da das Referenzergebnis zwischen Definitionen, Deklarationen und Referenzen unterscheidet. Dies geschieht, da die Anfrage textDocument/references einen zusätzlichen Eingabeparameter includeDeclarations aufweist, der steuert, ob Deklarationen und Definitionen ebenfalls in das Ergebnis einbezogen werden. Drei separate Eigenschaften ermöglichen es dem Server, das Ergebnis entsprechend zu berechnen.
Die `item`-Kante unterstützt auch die Verknüpfung von Referenzergebnissen mit anderen Referenzergebnissen. Dies ist nützlich, wenn Referenzen auf Methoden berechnet werden, die in einer Typenhierarchie überschrieben werden.
Nehmen Sie das folgende Beispiel
interface I {
foo(): void;
}
class A implements I {
foo(): void {
}
}
class B implements I {
foo(): void {
}
}
let i: I;
i.foo();
let b: B;
b.foo();
Das Referenzergebnis für die Methode foo in TypeScript enthält alle drei Deklarationen und beide Referenzen. Während der Analyse des Dokuments wird ein Referenzergebnis erstellt und dann für alle Ergebnis-Mengen gemeinsam genutzt.
Die Ausgabe sieht so aus
// The document
{ id: 4, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
// The declaration of I#foo
{ id: 13, type: "vertex", label: "resultSet" }
{ id: 16, type: "vertex", label: "range",
start: { line: 1, character: 2 }, end: { line: 1, character: 5 }
}
{ id: 17, type: "edge", label: "next", outV: 16, inV: 13 }
// The reference result for I#foo
{ id: 30, type: "vertex", label: "referenceResult" }
{ id: 31, type: "edge", label: "textDocument/references", outV: 13, inV: 30 }
// The declaration of A#foo
{ id: 29, type: "vertex", label: "resultSet" }
{ id: 34, type: "vertex", label: "range",
start: { line: 5, character: 2 }, end: { line: 5, character: 5 }
}
{ id: 35, type: "edge", label: "next", outV: 34, inV: 29 }
// The declaration of B#foo
{ id: 47, type: "vertex", label: "resultSet" }
{ id: 50, type: "vertex", label: "range",
start: { line: 10, character: 2 }, end: { line: 10, character: 5 }
}
{ id: 51, type: "edge", label: "next", outV: 50, inV: 47 }
// The reference i.foo()
{ id: 65, type: "vertex", label: "range",
start: { line: 15, character: 2 }, end: { line: 15, character: 5 }
}
// The reference b.foo()
{ id: 78, type: "vertex", label: "range",
start: { line: 18, character: 2 }, end: { line: 18, character: 5 }
}
// The insertion of the ranges into the shared reference result
{ id: 90, type: "edge", label: "item",
outV: 30, inVs: [16,34,50], shard: 4, property: "definitions"
}
{ id: 91, type: "edge", label: "item",
outV: 30, inVs: [65,78], shard: 4, property: "references"
}
// Linking A#foo to I#foo
{ id: 101, type: "vertex", label: "referenceResult" }
{ id: 102, type: "edge", label: "textDocument/references", outV: 29, inV: 101 }
{ id: 103, type: "edge", label: "item",
outV: 101, inVs: [30], shard: 4, property: "referenceResults"
}
// Linking B#foo to I#foo
{ id: 114, type: "vertex", label: "referenceResult" }
{ id: 115, type: "edge", label: "textDocument/references", outV: 47, inV: 114 }
{ id: 116, type: "edge", label: "item",
outV: 114, inVs: [30], shard: 4, property: "referenceResults"
}
Ein Ziel des Language Server Index Format ist es, dass die Informationen so schnell wie möglich ausgegeben werden können, ohne zu viele Informationen im Speicher zu cachen. Bei Sprachen, die das Überschreiben von Methoden, die in mehr als einer Schnittstelle definiert sind, unterstützen, kann dies komplizierter sein, da der gesamte Vererbungsbaum möglicherweise erst nach der Analyse aller Dokumente bekannt ist.
Betrachten Sie das folgende TypeScript-Beispiel
interface I {
foo(): void;
}
interface II {
foo(): void;
}
class B implements I, II {
foo(): void {
}
}
let i: I;
i.foo();
let b: B;
b.foo();
Die Suche nach I#foo() liefert 4 Referenzen, die Suche nach II#foo() liefert 3 Referenzen und die Suche nach B#foo() liefert 5 Ergebnisse. Das Interessante hierbei ist, wenn die Deklaration von class B verarbeitet wird, die I und II implementiert, weder das Ergebnis der Referenz, das an I#foo() gebunden ist, noch das, das an II#foo() gebunden ist, wiederverwendet werden kann. Also müssen wir ein neues erstellen. Um immer noch von den für I#foo und II#foo generierten Ergebnissen zu profitieren, unterstützt LSIF verschachtelte Referenzergebnisse. Auf diese Weise wird das von B#foo referenzierte Ergebnis das von I#foo und II#foo wiederverwenden. Je nachdem, wie diese Deklarationen geparst werden, können die beiden Referenzergebnisse dieselben Referenzen enthalten. Wenn ein Language Server Referenzergebnisse interpretiert, die aus anderen Referenzergebnissen bestehen, ist der Server für die Deduplizierung der endgültigen Bereiche verantwortlich.
Im obigen Beispiel wird es drei Referenzergebnisse geben
// The document
{ id: 4, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
// Declaration of I#foo
{ id: 13, type: "vertex", label: "resultSet" }
{ id: 16, type: "vertex", label: "range",
start: { line: 1, character: 2 }, end: { line: 1, character: 5 }
}
{ id: 17, type: "edge", label: "next", outV: 16, inV: 13 }
// Declaration of II#foo
{ id: 27, type: "vertex", label: "resultSet" }
{ id: 30, type: "vertex", label: "range",
start: { line: 5, character: 2 }, end: { line: 5, character: 5 }
}
{ id: 31, type: "edge", label: "next", outV: 30, inV: 27 }
// Declaration of B#foo
{ id: 45, type: "vertex", label: "resultSet" }
{ id: 52, type: "vertex", label: "range",
start: { line: 9, character: 2 }, end: { line: 9, character: 5 }
}
{ id: 53, type: "edge", label: "next", outV: 52, inV: 45 }
// Reference result for I#foo
{ id: 46, type: "vertex", label: "referenceResult" }
{ id: 47, type: "edge", label: "textDocument/references", outV: 13, inV: 46 }
// Reference result for II#foo
{ id: 48, type: "vertex", label: "referenceResult" }
{ id: 49, type: "edge", label: "textDocument/references", outV: 27, inV: 48 }
// Reference result for B#foo
{ id: 116 "typ" :"vertex", label: "referenceResult" }
{ id: 117 "typ" :"edge", label: "textDocument/references", outV: 45, inV: 116 }
// Link B#foo reference result to I#foo and II#foo
{ id: 118 "typ" :"edge", label: "item",
outV: 116, inVs: [46,48], document: 4, property: "referenceResults"
}
Für TypeScript werden Methodenreferenzen bei ihrer abstraktesten Deklaration aufgezeichnet, und wenn Methoden zusammengeführt werden (B#foo), werden sie über ein Referenzergebnis zusammengeführt, das auf andere Ergebnisse verweist.
Anfrage: textDocument/implementation
Die Unterstützung einer textDocument/implementation-Anfrage erfolgt durch Wiederverwendung dessen, was wir für eine textDocument/references-Anfrage implementiert haben. In den meisten Fällen gibt die textDocument/implementation die Deklarationswerte des Referenzergebnisses zurück, auf das eine Symboldeklaration verweist. Für Fälle, in denen das Ergebnis abweicht, bietet LSIF ein ImplementationResult. Um Implementierungsergebnisse zu verschachteln, unterstützt die item-Kante einen property-Wert von "implementationResults".
Das entsprechende ImplementationResult sieht wie folgt aus
interface ImplementationResult {
label: `implementationResult`
}
Anfrage: textDocument/typeDefinition
Die Unterstützung von textDocument/typeDefinition ist unkompliziert. Die Kante wird entweder am Bereich oder an der ResultSet aufgezeichnet.
Das entsprechende TypeDefinitionResult sieht wie folgt aus
interface TypeDefinitionResult {
label: `typeDefinitionResult`
}
Für das folgende TypeScript-Beispiel
interface I {
foo(): void;
}
let i: I;
Die relevanten ausgegebenen Knoten und Kanten sehen wie folgt aus
// The document
{ id: 4, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
// The declaration of I
{ id: 6, type: "vertex", label: "resultSet" }
{ id: 9, type: "vertex", label: "range",
start: { line: 0, character: 10 }, end: { line: 0, character: 11 }
}
{ id: 10, type: "edge", label: "next", outV: 9, inV: 6 }
// The declaration of i
{ id: 26, type: "vertex", label: "resultSet" }
// The type definition result
{ id: 37, type: "vertex", label: "typeDefinitionResult" }
// Hook the result to the declaration
{ id: 38, type: "edge", label: "textDocument/typeDefinition", outV: 26, inV:37 }
// Add the declaration of I as a target range.
{ id: 51, type: "edge", label: "item", outV: 37, inVs: [9], shard: 4 }
Wie bei anderen Ergebnissen werden Bereiche über eine item-Kante hinzugefügt. In diesem Fall ohne property, da es nur eine Art von Bereich gibt.
Dokument-Anfragen
Das Language Server Protocol unterstützt auch Anfragen für nur Dokumente (ohne Positionsinformationen). Diese Anfragen sind textDocument/foldingRange, textDocument/documentLink, textDocument/documentSymbol und textDocument/semanticTokens/full. Wir folgen dem gleichen Muster wie zuvor, um diese zu modellieren, wobei der Unterschied darin besteht, dass das Ergebnis an das Dokument und nicht an einen Bereich gebunden ist.
Anfrage: textDocument/foldingRange
Für das Ergebnis der Faltungsbereiche sieht dies wie folgt aus
function hello() {
console.log('Hello');
}
function world() {
console.log('world');
}
function space() {
console.log(' ');
}
hello();space();world();
{ id: 2, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
{ id: 112, type: "vertex", label: "foldingRangeResult", result:[
{ startLine: 0, startCharacter: 16, endLine: 2, endCharacter: 1 },
{ startLine: 4, startCharacter: 16, endLine: 6, endCharacter: 1 },
{ startLine: 8, startCharacter: 16, endLine: 10, endCharacter: 1 }
]}
{ id: 113, type: "edge", label: "textDocument/foldingRange", outV: 2, inV: 112 }
Das entsprechende FoldingRangeResult ist wie folgt definiert
export interface FoldingRangeResult {
label: 'foldingRangeResult';
result: lsp.FoldingRange[];
}
Anfrage: textDocument/documentLink
Auch für Dokumentenlinks definieren wir einen Ergebnistyp und eine entsprechende Kante, um ihn mit einem Dokument zu verknüpfen. Da die Link-Positionen normalerweise in Kommentaren vorkommen, bezeichnen die Bereiche keine Symboldeklarationen oder -referenzen. Wir betten daher den Bereich in das Ergebnis ein, wie wir es bei Faltungsbereichen tun.
export interface DocumentLinkResult {
label: 'documentLinkResult';
result: lsp.DocumentLink[];
}
Anfrage: textDocument/documentSymbol
Als nächstes betrachten wir die Anfrage textDocument/documentSymbol. Diese Anfrage gibt normalerweise eine Gliederungsansicht des Dokuments in hierarchischer Form zurück. Allerdings sind nicht alle in einem Dokument deklarierten oder definierten Programmsymbole Teil des Ergebnisses (z. B. werden lokale Variablen normalerweise weggelassen). Darüber hinaus muss ein Gliederungselement zusätzliche Informationen wie den vollständigen Bereich und eine Symbolart bereitstellen. Es gibt zwei Möglichkeiten, dies zu modellieren: Entweder tun wir dasselbe wie bei Faltungsbereichen und Dokumentlinks und speichern die Informationen in einem Dokument-Symbol-Ergebnis als Literale, oder wir erweitern den Bereichs-Vertex um zusätzliche Informationen und verweisen auf diese Bereiche im Dokument-Symbol-Ergebnis. Da die zusätzlichen Informationen für Bereiche auch in anderen Szenarien hilfreich sein könnten, unterstützen wir das Hinzufügen zusätzlicher Tags zu diesen Bereichen, indem wir eine tag-Eigenschaft auf dem range-Vertex definieren.
Die folgenden Tags werden derzeit unterstützt
/**
* The range represents a declaration
*/
export interface DeclarationTag {
/**
* A type identifier for the declaration tag.
*/
type: 'declaration';
/**
* The text covered by the range
*/
text: string;
/**
* The kind of the declaration.
*/
kind: lsp.SymbolKind;
/**
* The full range of the declaration not including leading/trailing whitespace
* but everything else, e.g comments and code. The range must be included in
* fullRange.
*/
fullRange: lsp.Range;
/**
* Optional detail information for the declaration.
*/
detail?: string;
}
/**
* The range represents a definition
*/
export interface DefinitionTag {
/**
* A type identifier for the declaration tag.
*/
type: 'definition';
/**
* The text covered by the range
*/
text: string;
/**
* The symbol kind.
*/
kind: lsp.SymbolKind;
/**
* The full range of the definition not including leading/trailing whitespace
* but everything else, e.g comments and code. The range must be included in
* fullRange.
*/
fullRange: lsp.Range;
/**
* Optional detail information for the definition.
*/
detail?: string;
}
/**
* The range represents a reference
*/
export interface ReferenceTag {
/**
* A type identifier for the reference tag.
*/
type: 'reference';
/**
* The text covered by the range
*/
text: string;
}
/**
* The type of the range is unknown.
*/
export interface UnknownTag {
/**
* A type identifier for the unknown tag.
*/
type: 'unknown';
/**
* The text covered by the range
*/
text: string;
}
Ausgabe der Tags für das folgende TypeScript-Beispiel
function hello() {
}
hello();
Sieht so aus
{ id: 2, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
{ id: 4, type: "vertex", label: "resultSet" }
{ id: 7, type: "vertex", label: "range",
start: { line: 0, character: 9 }, end: { line: 0, character: 14 },
tag: {
type: "definition", text: "hello", kind: 12,
fullRange: {
start: { line: 0, character: 0 }, end: { line: 1, character: 1 }
}
}
}
Das `documentSymbolResult` wird dann wie folgt modelliert
export interface RangeBasedDocumentSymbol {
id: RangeId
children?: RangeBasedDocumentSymbol[];
}
export interface DocumentSymbolResult extends V {
label: 'documentSymbolResult';
result: lsp.DocumentSymbol[] | RangeBasedDocumentSymbol[];
}
Das gegebene TypeScript-Beispiel
namespace Main {
function hello() {
}
function world() {
let i: number = 10;
}
}
Erzeugt die folgende Ausgabe
// The document
{ id: 2 , type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
// The declaration of Main
{ id: 7 , type: "vertex", label: "range",
start: { line: 0, character: 10 }, end: { line: 0, character: 14 },
tag: {
type: "definition", text: "Main", kind: 7,
fullRange: {
start: { line: 0, character: 0 }, end: { line: 5, character: 1 }
}
}
}
// The declaration of hello
{ id: 18 , type: "vertex", label: "range",
start: { line: 1, character: 11 }, end: { line: 1, character: 16 },
tag: {
type: "definition", text: "hello", kind: 12,
fullRange: {
start: { line: 1, character: 2 }, end: { line: 2, character: 3 }
}
}
}
// The declaration of world
{ id: 29 , type: "vertex", label: "range",
start: { line: 3, character: 11 }, end: { line: 3, character: 16 },
tag: {
type: "definition", text: "world", kind: 12,
fullRange: {
start: { line: 3, character: 2 }, end: { line: 4, character: 3 }
}
}
}
// The document symbol
{ id: 39 , type: "vertex", label: "documentSymbolResult",
result: [ { id: 7 , children: [ { id: 18 }, { id: 29 } ] } ]
}
{ id: 40 , type: "edge", label: "textDocument/documentSymbol",
outV: 2, inV: 39
}
Anfrage: textDocument/diagnostic
Die einzigen fehlenden Informationen, die in einem Dump nützlich sind, sind die dem Dokument zugeordneten Diagnosen. Diagnosen in LSP werden als Push-Benachrichtigungen modelliert, die vom Server an den Client gesendet werden. Dies funktioniert nicht gut mit einem Dump, der auf Anfragemethodennamen basiert. Die Push-Benachrichtigung kann jedoch als Anfrage emuliert werden, bei der das Ergebnis der Anfrage der Wert ist, der während des Pushs als Parameter gesendet wird.
Im Dump modellieren wir Diagnosen wie folgt
- Wir führen eine Pseudobeschränkung
textDocument/diagnosticein. - Wir führen ein `diagnosticResult` ein, das die dem Dokument zugeordneten Diagnosen enthält.
Das Ergebnis sieht so aus
export interface DiagnosticResult {
label: 'diagnosticResult';
result: lsp.Diagnostic[];
}
Das gegebene TypeScript-Beispiel
function foo() {
let x: string = 10;
}
Erzeugt die folgende Ausgabe
{ id: 2, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
{ id: 18, type: "vertex", label: "diagnosticResult",
result: [
{
severity: 1, code: 2322,
message: "Type '10' is not assignable to type 'string'.",
range: {
start : { line: 1, character: 5 }, end: { line: 1, character: 6 }
}
}
]
}
{ id: 19, type: "edge", label: "textDocument/diagnostic", outV: 2, inV: 18 }
Da Diagnosen in Dumps nicht sehr häufig vorkommen, wurden keine Anstrengungen unternommen, Bereiche in Diagnosen wiederzuverwenden.
Anfrage: textDocument/semanticTokens/full
Schließlich definieren der textDocument/semanticTokens/full-Edge und der Typ SemanticTokensResult ein Mittel zum Exportieren von Informationen über die Semantik von Bereichen im Text. Dieser Mechanismus ist in erster Linie ein Mittel zur Klassifizierung von interessanten Punkten, um zusätzliche, reichhaltigere, semantische Code-Farbgebung zu ermöglichen, die Informationen über den Code anzeigt, die nicht allein durch Syntaxparser ermittelt werden können (Färbung aufgelöster Typen, Formatierung nach Sichtbarkeit usw.).
function hello() {
console.log('Hello');
}
{ id: 2, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
{ id: 112, type: "vertex", label: "semanticTokensResult", result: {"data": [1, 2, 7, 0, 0] } }
{ id: 113, type: "edge", label: "textDocument/semanticTokens", outV: 2, inV: 112 }
Das entsprechende SemanticTokensResult ist wie folgt definiert
export interface SemanticTokensResult {
label: 'semanticTokensResult';
result: lsp.SemanticTokens;
}
Das obige Beispiel definiert einen einzelnen, mit 5 Integern kodierten Bereich bei 'console'.
Der Typ SemanticTokens, sein Mitglied data und die Kodierung der darin enthaltenen Integers stimmen mit den Darstellungen dieser Konzepte im Language Server Protocol überein.
Der 4. und 5. Integer in jedem 5-Integer-kodierten Token stellen die SemanticTokensType und SemanticTokensModifiers dar. Ähnlich wie bei LSP werden diese Integer über Einträge im Capabilities-Vertex auf Token-Typnamen und Token-Modifikatoren-Namen abgebildet.
Zum Beispiel wird der unten deklarierte semantische Token-Typ object auf 0 abgebildet. Alle Token, bei denen die vierte und fünfte Ganzzahl von 5 auf 0 gesetzt ist, werden für die Farbgebung als object betrachtet.
{
"semanticTokensProvider": {
"tokenTypes": [ "object" ],
"tokenModifiers": [ "static" ]
},
"label": "capabilities"
}
Weitere Informationen finden Sie unter LSP Semantic Tokens Protocol.
Der `Project`-Knoten
Normalerweise arbeiten Language Server in einer Art Projektkontext. In TypeScript wird ein Projekt mithilfe einer tsconfig.json-Datei definiert. C# und C++ haben ihre eigenen Mittel. Die Projektdatei enthält normalerweise Informationen über Kompilierungsoptionen und andere Parameter. Diese im Dump zu haben, kann wertvoll sein. LSIF definiert daher einen `project`-Knoten. Zusätzlich werden alle Dokumente, die zu diesem Projekt gehören, über eine contains-Kante mit dem Projekt verbunden. Wenn in den vorherigen Beispielen eine tsconfig.json vorhanden wäre, würden die ersten ausgegebenen Kanten und Knoten wie folgt aussehen
{ id: 1, type: "vertex", label: "project",
resource: "file:///Users/dirkb/tsconfig.json", kind: "typescript"
}
{ id: 2, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript"
}
{ id: 3, type: "edge", label: "contains", outV: 1, inVs: [2] }
Die Definition des project-Knotens sieht wie folgt aus
export interface Project extends V {
/**
* The label property.
*/
label: VertexLabels.project;
/**
* The project kind like 'typescript' or 'csharp'. See also the language ids
* in the specification.
* See https://msdocs.de/language-server-protocol/specification
*/
kind: string;
/**
* The resource URI of the project file.
*/
resource?: Uri;
/**
* Optional the content of the project file, `base64` encoded.
*/
contents?: string;
}
Eingebettete Inhalte
Es kann wertvoll sein, den Inhalt eines Dokument- oder Projektdateis auch in den Dump einzubetten. Zum Beispiel, wenn der Inhalt des Dokuments ein virtuelles Dokument ist, das aus Programmetadaten generiert wurde. Das Indexformat unterstützt daher eine optionale contents-Eigenschaft auf den Knoten document und project. Wenn verwendet, muss der Inhalt base64-kodiert sein.
Fortgeschrittene Konzepte
Ereignisse
Um die Verarbeitung eines LSIF-Exports zu erleichtern, z. B. den Import in eine Datenbank, gibt der Export Start- und Endereignisse für Dokumente und Projekte aus. Nach der Ausgabe des Endereignisses eines Dokuments darf der Export keine weiteren Daten enthalten, die sich auf dieses Dokument beziehen. Es können beispielsweise keine Bereiche aus diesem Dokument in item-Edges referenziert werden. Auch keine Ergebnismengen oder andere Vertices, die mit den Bereichen in diesem Dokument verknüpft sind. Das Dokument kann jedoch in einem contains-Edge referenziert werden, der das Dokument einem Projekt hinzufügt. Die Start-/Endereignisse für Dokumente sehen wie folgt aus
// The actual document
{ id: 4, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript",
contents: "..."
}
// The begin event
{ id: 5, type: "vertex", label: "$event",
kind: "begin", scope: "document" , data: 4
}
// The end event
{ id: 53, type: "vertex", label: "$event",
kind: "end", scope: "document" , data: 4
}
Zwischen dem Dokumentknoten 4 und dem Dokument-Start-Ereignis 5 dürfen keine Informationen spezifisch für Dokument 4 ausgegeben werden. Bitte beachten Sie, dass zu einem bestimmten Zeitpunkt mehr als ein Dokument geöffnet sein kann, was bedeutet, dass n verschiedene Dokument-Start-Ereignisse ohne entsprechende Dokument-End-Ereignisse vorhanden sein können.
Die Ereignisse für Projekte sehen ähnlich aus
{ id: 2, type: "vertex", label: "project", kind: "typescript" }
{ id: 4, type: "vertex", label: "document",
uri: "file:///Users/dirkb/sample.ts", languageId: "typescript",
contents: "..."
}
{ id: 5, type: "vertex", label: "$event",
kind: "begin", scope: "document" , data: 4
}
{ id: 3, type: "vertex", label: "$event",
kind: "begin", scope: "project", data: 2
}
{ id: 53, type: "vertex", label: "$event",
kind: "end", scope: "document", data: 4
}
{ id: 54, type: "edge", label: "contains", outV: 2, inVs: [4] }
{ id: 55, type: "vertex", label: "$event",
kind: "end", scope: "project", data: 2
}
Projekt-Exporte und externe Importe (Moniker)
Geändert in 0.5.0
Ein Anwendungsfall von LSIF ist die Erstellung von Exporte für freigegebene Versionen eines Produkts, sei es eine Bibliothek oder ein Programm. Wenn ein Projekt P2 eine Bibliothek P1 referenziert, wäre es auch nützlich, wenn die Informationen in diesen beiden Exporten in Beziehung gesetzt werden könnten. Um dies zu ermöglichen, führt LSIF optionale Moniker ein, die mit Bereichen über einen entsprechenden Edge verknüpft werden können. Die Moniker können verwendet werden, um zu beschreiben, was ein Projekt exportiert und was es importiert. Schauen wir uns zunächst den Exportfall an.
Betrachten Sie die folgende TypeScript-Datei namens index.ts
export function func(): void {
}
export class Emitter {
private doEmit() {
}
public emit() {
this.doEmit();
}
}
{ id: 4, type: "vertex", label: "document",
uri: "file:///Users/dirkb/index.ts", languageId: "typescript",
contents: "..."
}
{ id: 11, type: "vertex", label: "resultSet" }
{ id: 12, type: "vertex", label: "moniker", kind: "export",
scheme: "tsc", identifier: "lib/index:func", unique: "workspace"
}
{ id: 13, type: "edge", label: "moniker", outV: 11, inV: 12 }
{ id: 14, type: "vertex", label: "range",
start: { line: 0, character: 16 }, end: { line: 0, character: 20 }
}
{ id: 15, type: "edge", label: "next", outV: 14, inV: 11 }
{ id: 18, type: "vertex", label: "resultSet" }
{ id: 19, type: "vertex", label: "moniker",
kind: "export", scheme: "tsc", identifier: "lib/index:Emitter",
unique: "workspace"
}
{ id: 20, type: "edge", label: "moniker", outV: 18, inV: 19 }
{ id: 21, type: "vertex", label: "range",
start: { line: 3, character: 13 }, end: { line: 3, character: 20 }
}
{ id: 22, type: "edge", label: "next", outV: 21, inV: 18 }
{ id: 25, type: "vertex", label: "resultSet" }
{ id: 26, type: "vertex", label: "moniker",
kind: "export", scheme: "tsc", identifier: "lib/index:Emitter.doEmit",
unique: "workspace"
}
{ id: 27, type: "edge", label: "moniker", outV: 25, inV: 26 }
{ id: 28, type: "vertex", label: "range",
start: { line: 4, character: 10 }, end: { line: 4, character: 16 }
}
{ id: 29, type: "edge", label: "next", outV: 28, inV: 25 }
{ id: 32, type: "vertex", label: "resultSet" }
{ id: 33, type: "vertex", label: "moniker",
kind: "export", scheme: "tsc", identifier: "lib/index:Emitter.emit",
unique: "workspace"
}
{ id: 34, type: "edge", label: "moniker", outV: 32, inV: 33 }
{ id: 35, type: "vertex", label: "range",
start: { line: 7, character: 9 }, end: { line: 7, character: 13 }
}
{ id: 36, type: "edge", label: "next", outV: 35, inV: 32 }
Dies beschreibt die exportierte Deklaration in index.ts mit einem Moniker (z. B. einem Handle im Zeichenkettenformat), der an die entsprechende Bereichsdeklaration gebunden ist. Der generierte Moniker muss positionsunabhängig und stabil sein, damit er zur Identifizierung des Symbols in anderen Projekten oder Dokumenten verwendet werden kann. Er sollte ausreichend eindeutig sein, um nicht mit anderen Monikern in anderen Projekten übereinzustimmen, es sei denn, sie beziehen sich tatsächlich auf dasselbe Symbol. Ein Moniker hat daher die folgenden Eigenschaften
scheme, um anzugeben, wie dieidentifierszu interpretieren sind.identifier, um das Symbol tatsächlich zu identifizieren. Seine Struktur ist für den Scheme-Besitzer undurchsichtig. Im obigen Beispiel werden die Moniker vom TypeScript-Compiler tsc erstellt und können nur mit Monikern verglichen werden, die ebenfalls das Schemetschaben.kind, um anzugeben, ob der Moniker exportiert, importiert oder lokal für das Projekt ist.unique, um anzugeben, wie eindeutig der Moniker ist. Weitere Informationen hierzu finden Sie im Abschnitt über mehrere Projekte.
Bitte beachten Sie auch, dass die Methode Emitter#doEmit einen Export-Moniker hat, obwohl die Methode privat ist. Ob private Elemente Moniker haben, hängt von der Programmiersprache ab. Da TypeScript die Sichtbarkeit nicht erzwingen kann (es kompiliert zu JS, das dieses Konzept nicht hat), behandeln wir sie als sichtbar. Selbst der TypeScript-Language-Server tut dies. "Find all references" findet alle Referenzen auf private Methoden, auch wenn es als Sichtbarkeitsverletzung gekennzeichnet ist.
Systeme mit mehreren Projekten
Neu in 0.5.0, geändert in 0.6.0
Die meisten Softwaresysteme bestehen heute aus mehreren Projekten. Das wiederholte Erstellen von LSIF-Exporten für alle Projekte eines Systems, selbst wenn nur ein Projekt geändert wird, ist nicht sehr praktikabel, insbesondere wenn sich nur interne Teile eines Projekts geändert haben. LSIF erlaubt seit 0.4.0 daher, einen LSIF-Export pro Projekt zu erstellen und diese in der DB wieder zu größeren Systemen zu verknüpfen. 0.4.0 fehlten jedoch einige Konzepte, um dies realisierbar zu machen. Um diese zu motivieren, betrachten wir das folgende Beispiel
Projekt P1
Projekt P1 besteht aus einer p1Main.ts-Datei mit folgendem Inhalt
export interface Disposable {
dispose(): void;
}
let d: Disposable;
d.dispose();
Projekt P2
Projekt P2 hängt von P1 ab und besteht aus einer p2Main.ts-Datei mit folgendem Inhalt
import { Disposable } from 'p1';
class Widget implements Disposable {
public dispose(): void {
}
}
let w: Widget;
w.dispose();
Wenn ein Benutzer nun nach Referenzen zu Widget#dispose sucht, wird erwartet, dass die Referenz d.dispose in P1 in das Ergebnis einbezogen wird. Wenn jedoch P1 verarbeitet wird, kennt das Werkzeug P2 nicht. Und wenn P2 verarbeitet wird, kennt es normalerweise nicht die Quelle von P1. Es kennt nur die API-Form von P1 (z. B. in TypeScript die entsprechende d.ts-Datei).
Damit dies funktioniert, müssen wir zuerst Projekte in größere Einheiten zusammenfassen, damit wir wissen, in welchen Projekten d.dispose tatsächlich eine Übereinstimmung ist. Angenommen, es gibt ein völlig unabhängiges Projekt PX, das auch Disposable von P1 verwendet, aber P2 wird nie mit PX in einem System verknüpft. Ein Objekt vom Typ Widget kann also niemals in Code in PX fließen, daher sollten Referenzen in PX nicht aufgeführt werden. Wie Projekte gruppiert werden können, hängt hauptsächlich von der Programmiersprache ab. Außerdem hängt es davon ab, ob diese Informationen nützlich sind, auch vom Speicher-Backend, in dem ein LSIF-Export gespeichert wird. Ein guter Anhaltspunkt ist jedoch die Quelle, aus der ein Export generiert wurde. Wir führen daher die Notation eines Source-Vertex ein, um die Quelle des Exports anzugeben. Der Source-Vertex ist ein Wurzel-Vertex im Export und nicht mit anderen Knoten verbunden. Schauen wir uns die konkreten Exporte für P1 und P2 an
{ id:2, type: "vertex", label: "source",
workspaceRoot: "file:///Users/dirkb/samples/ts-cascade",
repository: {
type: "git",
url: "git+https://github.com/samples/ts-cascade.git"
}
}
Der Source-Vertex enthält folgende nützliche Informationen
- den
workspaceRoot: Dies ist die URI des Arbeitsbereichs, die bei der Erstellung des Exports verwendet wurde. Sie ermöglicht eine relative Interpretation anderer URIs im Export, wie z. B. Dokument-URIs. - das
repository: Es gibt das Repository an, in dem der Quellcode gespeichert ist, falls verfügbar.
Der Export für Projekt P2 enthält denselben Source-Vertex
{ id:2, type: "vertex", label: "source",
workspaceRoot: "file:///Users/dirkb/samples/ts-cascade",
repository: {
type: "git",
url: "git+https://github.com/samples/ts-cascade.git"
}
}
Beachten Sie, dass P1 und P2 dieselben Quellinformationen haben, was ein guter Indikator für das Speicher-Backend ist, um Referenzen zwischen diesen beiden Projekten aufzulösen. Die Projektgruppierung ist jedoch möglicherweise nicht auf Quell-Repositories beschränkt. Daher sollte ein Speicher-Backend einen Weg definieren, Projekte hierarchisch zu gruppieren. Dies ermöglicht beispielsweise Suchen wie: Finde alle Referenzen auf die Funktion foo in der Organisation O.
Schauen wir uns nun an, wie wir sicherstellen, dass die Suche nach Referenzen für Widget#dispose die Übereinstimmung d.dispose() in P1 findet. Zuerst schauen wir uns an, welche Art von Informationen im Export von P1 für Disposable#dispose enthalten sein wird
// The result set for the Disposable#dispose symbol
{ id: 21, type: "vertex", label: "resultSet" }
// The export moniker of Disposable#dispose in P1 (note kind export).
{ id: 22, type: "vertex", label: "moniker",
scheme: "tsc", identifier: "p1/lib/p1Main:Disposable.dispose",
unique: "workspace", kind:"export"
}
{ id: 23, type: "edge", label: "moniker", outV: 21, inV: 22 }
// The actual definition of the symbol
{ id: 24, type: "vertex", label: "range",
start: { line: 1, character: 1 }, end: { line: 1, character: 8 },
tag: {
type: definition, text: "dispose", kind: 7,
fullRange: {
start : { line: 1, character:1 }, end: { line: 1, character: 17 }
}
}
}
// Bind the reference result to the result set
{ id: 57, type: "vertex", label: "referenceResult" }
{ id: 58, type: "edge", label: "textDocument/references", outV: 21, inV: 57 }
Interessant ist hier Zeile 22, die den Moniker für Disposable#dispose definiert. Er hat eine neue Eigenschaft unique, die angibt, dass der Moniker innerhalb eines workspace von Projekten eindeutig ist, aber nicht unbedingt außerhalb. Weitere mögliche Werte für unique sind
document, um anzugeben, dass der Moniker nur innerhalb eines Dokuments eindeutig ist. Wird beispielsweise für lokale Variablen oder private Member verwendet.project, um anzugeben, dass der Moniker nur innerhalb eines Projekts eindeutig ist. Wird beispielsweise für projektspezifische Symbole verwendet.workspace, um anzugeben, dass der Moniker innerhalb eines Arbeitsbereichs von Projekten eindeutig ist. Wird beispielsweise für exportierte Member verwendet.scheme, um anzugeben, dass der Moniker innerhalb des Schemas des Monikers eindeutig ist. Wenn der Moniker beispielsweise für einen bestimmten Paketmanager generiert wird (siehe npm-Beispiel unten), dann sind diese Moniker normalerweise innerhalb des Themas des Monikers eindeutig (z. B. tragen alle für npm generierten Moniker dasnpm-Scheme und sind eindeutig).global, um anzugeben, dass der Moniker global eindeutig ist (z. B. ist seine Kennung unabhängig vom Scheme oder der Art eindeutig).
Beim Generieren des Exports für P2 sehen die Informationen für Widget#dispose wie folgt aus
// The import moniker for importing Disposable#dispose into P2
{ id: 22, type: "vertex", label: "moniker",
scheme: "tsc", identifier: "p1/lib/p1Main:Disposable.dispose",
unique: "workspace", kind: "import"
}
// The result set for Widget#dispose
{ id: 78, type: "vertex", label: "resultSet" }
// The moniker for Widget#dispose. Note that the moniker is local since the
// Widget class is not exported
{ id: 79, type: "vertex", label: "moniker",
scheme: "tsc", identifier: "2Q46RTVRZTuVW1ajf68/Vw==",
unique: "document", kind: "local"
}
{ id: 80, type: "edge", label: "moniker", outV: 78, inV: 79 }
// The actual definition of the symbol
{ id: 81, type: "vertex", label: "range",
start: { line: 3, character: 8 }, end: { line: 3, character: 15 },
tag: {
type: "definition", text: "dispose", kind: 6,
fullRange: {
start: { line: 3, character: 1 }, end: { line: 4, character: 2 }
}
}
}
// Bind the reference result to Widget#dispose
{ id: 116, type: "vertex", label: "referenceResult" }
{ id: 117, type: "edge", label: "textDocument/references", outV: 78, inV: 116}
{ id: 118, type: "edge", label: "item",
outV: 116, inVs: [43], shard: 52, property: "referenceResults"
}
// Link the reference result set of Disposable#dispose to this result set
// using a moniker
{ id: 119, type: "edge", label: "item",
outV: 116, inVs: [22], shard: 52, property: "referenceLinks"
}
{ id: 120, type: "edge", label: "item",
outV: 43, inVs: [81], shard: 52, property: "definitions"
}
{ id: 121, type: "edge", label: "item",
outV: 43, inVs: [96], shard: 52, property: "references"
}
Die bemerkenswerten Teile sind
- der Vertex mit
id: 22: ist der Import-Moniker fürDisposable#disposeaus P1. - der Edge mit
id: 119: fügt einen Referenzlink zum Referenzergebnis vonWidget#disposehinzu. Item-Edges mitreferenceLinkssind konzeptionell ähnlich wie Item-Edges mit der EigenschaftreferenceResults. Sie ermöglichen zusammengesetzte Referenzergebnisse. Der Unterschied besteht darin, dass einreferenceResults-Item-Edge ein anderes Ergebnis über die Vertex-ID referenziert, da das Referenzergebnis Teil desselben Exports ist. EinreferenceLinks-Item-Edge referenziert ein anderes Ergebnis über einen Moniker. Die eigentliche Auflösung muss also in einer Datenbank erfolgen, die die Daten für P1 und P2 enthält. Wie beireferenceResults-Item-Edges ist ein Language Server für die Deduplizierung der endgültigen Bereiche verantwortlich.
Paketmanager
Geändert in 0.5.0
Wie exportierte Elemente in anderen Projekten in den meisten Programmiersprachen sichtbar sind, hängt davon ab, wie Dateien in eine Bibliothek oder ein Programm verpackt werden. In TypeScript ist der Standardpaketmanager npm.
Betrachten Sie die folgende package.json-Datei
{
"name": "lsif-ts-sample",
"version": "1.0.0",
"description": "",
"main": "lib/index.js",
"author": "MS",
"license": "MIT",
}
für die folgende TypeScript-Datei (wie oben)
export function func(): void {
}
export class Emitter {
private doEmit() {
}
public emit() {
this.doEmit();
}
}
dann können diese Moniker in Moniker übersetzt werden, die vom Paketmanager npm abhängen. Anstatt die Moniker zu ersetzen, geben wir einen zweiten Satz von Monikern aus und verknüpfen die tsc-Moniker mit den entsprechenden npm-Monikern über einen attach-Edge
{ id: 991, type: "vertex", label: "packageInformation",
name: "lsif-ts-sample", manager: "npm", version: "1.0.0"
}
{ id: 987, type: "vertex", label: "moniker",
kind: "export", scheme: "npm", identifier: "lsif-ts-sample::func",
unique: "scheme"
}
{ id: 986, type: "edge", label: "packageInformation", outV: 987, inV: 991 }
{ id: 985, type: "edge", label: "attach", outV: 987, inV: 12 }
{ id: 984, type: "vertex", label: "moniker",
kind: "export", scheme: "npm", identifier: "lsif-ts-sample::Emitter",
unique: "scheme"
}
{ id: 983, type: "edge", label: "packageInformation", outV: 984, inV: 991 }
{ id: 982, type: "edge", label: "attach", outV: 984, inV: 19 }
{ id: 981, type: "vertex", label: "moniker",
kind: "export", scheme: "npm",
identifier: "lsif-ts-sample::Emitter.doEmit", unique: "scheme"
}
{ id: 980, type: "edge", label: "packageInformation", outV: 981, inV: 991 }
{ id: 979, type: "edge", label: "attach", outV: 981, inV: 26 }
{ id: 978, type: "vertex", label: "moniker",
kind: "export", scheme: "npm",
identifier: "lsif-ts-sample::Emitter.emit", unique: "scheme"
}
{ id: 977, type: "edge", label: "packageInformation", outV: 978, inV: 991 }
{ id: 976, type: "edge", label: "attach", outV: 978, inV: 33 }
Zu beobachtende Dinge
- Ein spezieller
packageInformation-Vertex wurde ausgegeben, um auf die entsprechenden npm-Paketinformationen zu verweisen. - der npm-Moniker verweist auf den Paketnamen.
- sein
unique-Wert istscheme, was bedeutet, dass die Kennung des Monikers eindeutig über allenpm-Moniker ist. - da die Datei
index.tsdie npm-Hauptdatei ist, hat der Moniker-Identifier keinen Dateipfad. Dies ist vergleichbar mit dem Importieren dieses Moduls in TypeScript oder JavaScript, wobei nur der Modulname und kein Dateipfad verwendet wird (z. B.import * as lsif from 'lsif-ts-sample'). - Der
attach-Edge verweist vom npm-Moniker-Vertex auf den tsc-Moniker-Vertex.
Für LSIF empfehlen wir, dass ein zweites Werkzeug verwendet wird, um die vom Indexer ausgegebenen Moniker paketmanagerabhängig zu machen. Dies unterstützt die Verwendung verschiedener Paketmanager und ermöglicht die Einbeziehung benutzerdefinierter Build-Tools. In der TypeScript-Implementierung geschieht dies durch ein npm-spezifisches Werkzeug, das die Moniker unter Berücksichtigung der npm-Paketinformationen anhängt.
Das Melden des Imports von externen Symbolen erfolgt auf die gleiche Weise. LSIF gibt Moniker vom Typ import aus. Betrachten Sie das folgende TypeScript-Beispiel
import * as mobx from 'mobx';
let map: mobx.ObservableMap = new mobx.ObservableMap();
wobei mobx das npm mobx Paket ist. Das Ausführen des tsc-Indexwerkzeugs erzeugt
{ id: 41, type: "vertex", label: "document",
uri: "file:///samples/node_modules/mobx/lib/types/observablemap.d.ts",
languageId: "typescript", contents: "..."
}
{ id: 55, type: "vertex", label: "resultSet" }
{ id: 57, type: "vertex", label: "moniker",
kind: "import", scheme: "tsc",
identifier: "node_modules/mobx/lib/mobx:ObservableMap", unique: 'workspace'
}
{ id: 58, type: "edge", label: "moniker", outV: 55, inV: 57 }
{ id: 59, type: "vertex", label: "range",
start: { line: 17, character: 538 }, end: { line: 17, character: 551 }
}
{ id: 60, type: "edge", label: "next", outV: 59, inV: 55 }
Drei Dinge sind hier zu beachten: Erstens verwendet TypeScript Deklarationsdateien für extern importierte Symbole. Dies hat den Vorteil, dass die Monikerinformationen an die Deklarationsbereiche in diesen Dateien angehängt werden können. In anderen Sprachen können die Informationen an die Datei angehängt werden, die das Symbol tatsächlich referenziert. Oder es wird ein virtuelles Dokument für das referenzierte Element generiert. Zweitens erzeugt das Werkzeug diese Informationen nur für tatsächlich referenzierte Symbole, nicht für alle verfügbaren Symbole. Drittens sind diese Moniker tsc-spezifisch und zeigen auf den node_modules-Ordner.
Das Durchleiten dieser Informationen durch das npm-Werkzeug erzeugt jedoch Folgendes
{ id: 991, type: "vertex", label: "packageInformation",
name: "mobx", manager: "npm", version: "5.6.0",
repository: { type: "git", url: "git+https://github.com/mobxjs/mobx.git" }
}
{ id: 978, type: "vertex", label: "moniker",
kind: "import", scheme: "npm", identifier: "mobx::ObservableMap",
unique: 'scheme'
}
{ id: 977, type: "edge", label: "packageInformation", outV: 978, inV: 991 }
{ id: 976, type: "edge", label: "attach", outV: 978, inV: 57 }
was den Moniker spezifisch für das npm-Paket mobx gemacht hat. Zusätzlich wurden Informationen über das mobx-Paket selbst ausgegeben.
Normalerweise werden Moniker an Ergebnis-Mengen angehängt, da sie für alle Bereiche, die auf die Ergebnis-Menge verweisen, gleich sind. Für Dumps, die keine Ergebnis-Mengen verwenden, können Moniker jedoch auch auf Bereiche angewendet werden.
Für Werkzeuge, die den Export verarbeiten und in eine Datenbank importieren, ist es manchmal nützlich zu wissen, ob ein Ergebnis lokal zu einer Datei gehört oder nicht (z. B. können Funktionsargumente nur innerhalb der Datei navigiert werden). Um Werkzeuge zur Nachverarbeitung dabei zu helfen, dies effizient zu entscheiden, sollten Werkzeuge zur LSIF-Generierung auch für lokale Variablen einen Moniker generieren. Die entsprechende Art (kind) ist local. Die Kennung sollte innerhalb des Dokuments eindeutig sein.
Für das folgende Beispiel
function foo(x: number): void {
}
Das Moniker für x sieht wie folgt aus
{ id: 13, type: "vertex", label: "resultSet" }
{ id: 14, type: "vertex", label: "moniker",
kind: "local", scheme: "tsc", identifier: "SfeOP6s53Y2HAkcViolxYA==",
unique: 'document'
}
{ id: 15, type: "edge", label: "moniker", outV: 13, inV: 14 }
{ id: 16, type: "vertex", label: "range",
start: { line: 0, character: 13 }, end: { line: 0, character: 14 },
tag: {
type: "definition", text: "x", kind: 7,
fullRange: {
start: { line: 0, character: 13 }, end: { line: 0, character: 22 }
}
}
}
{ id: 17, type: "edge", label: "next", outV: 16, inV: 13 }
Zusätzlich zu diesen Moniker-Schemata, die mit $ beginnen, sind reserviert und sollten von einem LSIF-Tool nicht verwendet werden.
Ergebnisbereiche
Bereiche in LSIF haben derzeit zwei Bedeutungen
- sie fungieren als LSP-Anforderungssensitive Bereiche in einem Dokument (z. B. verwenden wir sie, um zu entscheiden, ob für eine gegebene Position ein entsprechendes LSP-Anforderungsergebnis existiert)
- sie fungieren als Navigationsziele (z. B. sind sie das Ergebnis einer Go-To-Deklarationsnavigation).
Um die erste Bedeutung zu erfüllen, spezifiziert LSIF, dass Bereiche sich nicht überlappen oder gleich sein dürfen. Diese Einschränkung ist jedoch für die zweite Bedeutung nicht notwendig. Um gleich oder überlappende Zielbereiche zu unterstützen, führen wir einen Knoten resultRange ein. Es ist nicht erlaubt, ein resultRange als Ziel in einer contains-Kante zu verwenden.
Metadaten-Knoten
Geändert in 0.5.0
Zur Unterstützung der Versionierung definiert LSIF einen Metadaten-Knoten wie folgt
export interface MetaData {
/**
* The label property.
*/
label: 'metaData';
/**
* The version of the LSIF format using semver notation. See
* https://semver.org/. Please note the version numbers starting with 0
* don't adhere to semver and adopters have to assume the each new version
* is breaking.
*/
version: string;
/**
* The string encoding used to compute line and character values in
* positions and ranges. Currently only 'utf-16' is support due to the
* limitations in LSP.
*/
positionEncoding: 'utf-16',
/**
* Information about the tool that created the dump
*/
toolInfo?: {
name: string;
version?: string;
args?: string[];
}
}
Generierungsbeschränkungen
Erweitert in 0.6.0
Die folgenden Generierungsbeschränkungen (von denen einige bereits im Dokument erwähnt wurden) existieren
- Ein Knoten muss generiert werden, bevor er in einer Kante referenziert werden kann.
- Ein
rangeundresultRangekönnen nur in einem Dokument enthalten sein. - Ein
resultRangekann nicht als Ziel in einercontains-Kante verwendet werden. - Nachdem ein Dokument-Endereignis ausgegeben wurde, können nur Ergebnismengen, Referenz- oder Implementierungsergebnisse, die über dieses Dokument ausgegeben wurden, in Edges referenziert werden. Es ist beispielsweise nicht erlaubt, Bereiche oder Ergebnisbereiche aus diesem Dokument zu referenzieren. Dies schließt auch das Hinzufügen von Monikern zu Bereichen oder Ergebnismengen ein. Die Dokumentdaten können sozusagen nicht mehr geändert werden.
- Wenn Bereiche auf Ergebnissets verweisen und Moniker generiert werden, müssen sie auf dem Ergebnisset generiert werden und können nicht auf einzelnen Bereichen generiert werden.
- Wenn ein Bereich in einem Items-Edge referenziert wird, muss der Bereich mit dem Contains-Edge an ein Dokument angehängt worden sein. Dies stellt sicher, dass das Ziel dokument eines Bereichs bekannt ist. (@since 0.6.0)
Zusätzliche Informationen
Tools
lsif-protocol: Protokoll definiert als TypeScript-Interfaceslsif-util: Hilfsprogramme für die LSIF-Entwicklunglsif-tsc: LSIF-Indexer für TypeScriptlsif-npm: Linker für NPM-Moniker
Offene Fragen (Open Questions)
Bei der Implementierung für TypeScript und npm haben wir eine Liste von offenen Fragen in Form von GitHub-Issues gesammelt, die uns bereits bekannt sind.