Skip to content

Commit

Permalink
Merge pull request #427 from KxSystems/ee-multi
Browse files Browse the repository at this point in the history
Multi file language server features
  • Loading branch information
ecmel authored Oct 1, 2024
2 parents da84b42 + af70d50 commit 87552e2
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 32 deletions.
180 changes: 148 additions & 32 deletions server/src/qLangServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@

import { Position, TextDocument } from "vscode-languageserver-textdocument";
import {
CallHierarchyIncomingCall,
CallHierarchyIncomingCallsParams,
CallHierarchyItem,
CallHierarchyOutgoingCall,
CallHierarchyOutgoingCallsParams,
CallHierarchyPrepareParams,
CompletionItem,
CompletionItemKind,
CompletionParams,
Expand Down Expand Up @@ -90,6 +96,15 @@ export default class QLangServer {
this.connection.onDefinition(this.onDefinition.bind(this));
this.connection.onRenameRequest(this.onRenameRequest.bind(this));
this.connection.onCompletion(this.onCompletion.bind(this));
this.connection.languages.callHierarchy.onPrepare(
this.onPrepareCallHierarchy.bind(this),
);
this.connection.languages.callHierarchy.onIncomingCalls(
this.onIncomingCallsCallHierarchy.bind(this),
);
this.connection.languages.callHierarchy.onOutgoingCalls(
this.onOutgoingCallsCallHierarchy.bind(this),
);
this.connection.onDidChangeConfiguration(
this.onDidChangeConfiguration.bind(this),
);
Expand All @@ -113,6 +128,7 @@ export default class QLangServer {
renameProvider: true,
completionProvider: { resolveProvider: false },
selectionRangeProvider: true,
callHierarchyProvider: true,
};
}

Expand Down Expand Up @@ -171,9 +187,16 @@ export default class QLangServer {
public onReferences({ textDocument, position }: ReferenceParams): Location[] {
const tokens = this.parse(textDocument);
const source = positionToToken(tokens, position);
return findIdentifiers(FindKind.Reference, tokens, source).map((token) =>
Location.create(textDocument.uri, rangeFromToken(token)),
);
return this.documents
.all()
.map((document) =>
findIdentifiers(
FindKind.Reference,
document.uri === textDocument.uri ? tokens : this.parse(document),
source,
).map((token) => Location.create(document.uri, rangeFromToken(token))),
)
.flat();
}

public onDefinition({
Expand All @@ -182,34 +205,45 @@ export default class QLangServer {
}: DefinitionParams): Location[] {
const tokens = this.parse(textDocument);
const source = positionToToken(tokens, position);
return findIdentifiers(FindKind.Definition, tokens, source).map((token) =>
Location.create(textDocument.uri, rangeFromToken(token)),
);
return this.documents
.all()
.map((document) =>
findIdentifiers(
FindKind.Definition,
document.uri === textDocument.uri ? tokens : this.parse(document),
source,
).map((token) => Location.create(document.uri, rangeFromToken(token))),
)
.flat();
}

public onRenameRequest({
textDocument,
position,
newName,
}: RenameParams): WorkspaceEdit | null {
}: RenameParams): WorkspaceEdit {
const tokens = this.parse(textDocument);
const source = positionToToken(tokens, position);
const refs = findIdentifiers(FindKind.Rename, tokens, source);
if (refs.length === 0) {
return null;
}
const name = <Token>{
image: newName,
namespace: source?.namespace,
};
const edits = refs.map((token) => {
return TextEdit.replace(rangeFromToken(token), relative(name, token));
});
return {
changes: {
[textDocument.uri]: edits,
return this.documents.all().reduce(
(edit, document) => {
const refs = findIdentifiers(
FindKind.Rename,
document.uri === textDocument.uri ? tokens : this.parse(document),
source,
);
if (refs.length > 0) {
const name = <Token>{
image: newName,
namespace: source?.namespace,
};
edit.changes![document.uri] = refs.map((token) =>
TextEdit.replace(rangeFromToken(token), relative(name, token)),
);
}
return edit;
},
};
{ changes: {} } as WorkspaceEdit,
);
}

public onCompletion({
Expand All @@ -218,16 +252,25 @@ export default class QLangServer {
}: CompletionParams): CompletionItem[] {
const tokens = this.parse(textDocument);
const source = positionToToken(tokens, position);
return findIdentifiers(FindKind.Completion, tokens, source).map((token) => {
return {
label: token.image,
labelDetails: {
detail: ` .${namespace(token)}`,
},
kind: CompletionItemKind.Variable,
insertText: relative(token, source),
};
});
return this.documents
.all()
.map((document) =>
findIdentifiers(
FindKind.Completion,
document.uri === textDocument.uri ? tokens : this.parse(document),
source,
).map((token) => {
return {
label: token.image,
labelDetails: {
detail: ` .${namespace(token)}`,
},
kind: CompletionItemKind.Variable,
insertText: relative(token, source),
};
}),
)
.flat();
}

public onExpressionRange({
Expand Down Expand Up @@ -300,6 +343,79 @@ export default class QLangServer {
return ranges;
}

public onPrepareCallHierarchy({
textDocument,
position,
}: CallHierarchyPrepareParams): CallHierarchyItem[] {
const tokens = this.parse(textDocument);
const source = positionToToken(tokens, position);
if (source && assignable(source)) {
return [
{
kind: SymbolKind.Variable,
name: source.image,
uri: textDocument.uri,
range: rangeFromToken(source),
selectionRange: rangeFromToken(source),
},
];
}
return [];
}

public onIncomingCallsCallHierarchy({
item,
}: CallHierarchyIncomingCallsParams): CallHierarchyIncomingCall[] {
const tokens = this.parse({ uri: item.uri });
const source = positionToToken(tokens, item.range.end);
return this.documents
.all()
.map((document) =>
findIdentifiers(FindKind.Reference, this.parse(document), source)
.filter((token) => !assigned(token))
.map((token) => {
const lambda = inLambda(token);
return {
from: {
kind: lambda ? SymbolKind.Object : SymbolKind.Function,
name: token.image,
uri: document.uri,
range: rangeFromToken(lambda || token),
selectionRange: rangeFromToken(token),
},
fromRanges: [],
} as CallHierarchyIncomingCall;
}),
)
.flat();
}

public onOutgoingCallsCallHierarchy({
item,
}: CallHierarchyOutgoingCallsParams): CallHierarchyOutgoingCall[] {
const tokens = this.parse({ uri: item.uri });
const source = positionToToken(tokens, item.range.end);
return this.documents
.all()
.map((document) =>
findIdentifiers(FindKind.Reference, this.parse(document), source)
.filter((token) => inLambda(token) && !assigned(token))
.map((token) => {
return {
to: {
kind: SymbolKind.Object,
name: token.image,
uri: document.uri,
range: rangeFromToken(inLambda(token)!),
selectionRange: rangeFromToken(token),
},
fromRanges: [],
} as CallHierarchyOutgoingCall;
}),
)
.flat();
}

private parse(textDocument: TextDocumentIdentifier): Token[] {
const document = this.documents.get(textDocument.uri);
if (!document) {
Expand Down
14 changes: 14 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
EventEmitter,
ExtensionContext,
Range,
TabInputText,
TextDocumentContentProvider,
Uri,
WorkspaceEdit,
Expand Down Expand Up @@ -685,6 +686,19 @@ export async function activate(context: ExtensionContext) {
clientOptions,
);

const docs = window.tabGroups.all
.flatMap((group) => group.tabs)
.map((tab) => tab.input as TabInputText);

for (const doc of docs) {
if (
doc.uri &&
(doc.uri.path.endsWith(".q") || doc.uri.path.endsWith(".quke"))
) {
await workspace.openTextDocument(doc.uri);
}
}

await client.start();

connectClientCommands(context, client);
Expand Down
35 changes: 35 additions & 0 deletions test/suite/qLangServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe("qLangServer", () => {
const position = document.positionAt(offset || content.length);
const textDocument = TextDocumentIdentifier.create("test.q");
sinon.stub(server.documents, "get").value(() => document);
sinon.stub(server.documents, "all").value(() => [document]);
return {
textDocument,
position,
Expand All @@ -57,6 +58,13 @@ describe("qLangServer", () => {
onDidChangeConfiguration() {},
onRequest() {},
onSelectionRanges() {},
languages: {
callHierarchy: {
onPrepare() {},
onIncomingCalls() {},
onOutgoingCalls() {},
},
},
});

const params = <InitializeParams>{
Expand All @@ -80,6 +88,7 @@ describe("qLangServer", () => {
assert.ok(capabilities.renameProvider);
assert.ok(capabilities.completionProvider);
assert.ok(capabilities.selectionRangeProvider);
assert.ok(capabilities.callHierarchyProvider);
});
});

Expand Down Expand Up @@ -330,4 +339,30 @@ describe("qLangServer", () => {
assert.strictEqual(result[0].range.end.character, 9);
});
});

describe("onPrepareCallHierarchy", () => {
it("should prepare call hierarchy", () => {
const params = createDocument("a:1;a");
const result = server.onPrepareCallHierarchy(params);
assert.strictEqual(result.length, 1);
});
});

describe("onIncomingCallsCallHierarchy", () => {
it("should return incoming calls", () => {
const params = createDocument("a:1;a");
const items = server.onPrepareCallHierarchy(params);
const result = server.onIncomingCallsCallHierarchy({ item: items[0] });
assert.strictEqual(result.length, 1);
});
});

describe("onOutgoingCallsCallHierarchy", () => {
it("should return outgoing calls", () => {
const params = createDocument("a:1;{a");
const items = server.onPrepareCallHierarchy(params);
const result = server.onOutgoingCallsCallHierarchy({ item: items[0] });
assert.strictEqual(result.length, 1);
});
});
});

0 comments on commit 87552e2

Please sign in to comment.