From ae3e11e11b56283413176a9f16c66489862dc4c2 Mon Sep 17 00:00:00 2001 From: ecmel Date: Mon, 30 Sep 2024 21:40:25 +0300 Subject: [PATCH 1/2] implemented mult file server features --- server/src/qLangServer.ts | 180 +++++++++++++++++++++++++++------ src/extension.ts | 14 +++ test/suite/qLangServer.test.ts | 35 +++++++ test/suite/utils.test.ts | 4 +- 4 files changed, 199 insertions(+), 34 deletions(-) diff --git a/server/src/qLangServer.ts b/server/src/qLangServer.ts index 0529e1e8..81fcac8d 100644 --- a/server/src/qLangServer.ts +++ b/server/src/qLangServer.ts @@ -13,6 +13,12 @@ import { Position, TextDocument } from "vscode-languageserver-textdocument"; import { + CallHierarchyIncomingCall, + CallHierarchyIncomingCallsParams, + CallHierarchyItem, + CallHierarchyOutgoingCall, + CallHierarchyOutgoingCallsParams, + CallHierarchyPrepareParams, CompletionItem, CompletionItemKind, CompletionParams, @@ -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), ); @@ -113,6 +128,7 @@ export default class QLangServer { renameProvider: true, completionProvider: { resolveProvider: false }, selectionRangeProvider: true, + callHierarchyProvider: true, }; } @@ -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({ @@ -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 = { - 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 = { + 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({ @@ -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({ @@ -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) { diff --git a/src/extension.ts b/src/extension.ts index 973beb1c..616b998c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,6 +18,7 @@ import { EventEmitter, ExtensionContext, Range, + TabInputText, TextDocumentContentProvider, Uri, WorkspaceEdit, @@ -689,6 +690,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); diff --git a/test/suite/qLangServer.test.ts b/test/suite/qLangServer.test.ts index f8eaf616..f72e67d5 100644 --- a/test/suite/qLangServer.test.ts +++ b/test/suite/qLangServer.test.ts @@ -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, @@ -57,6 +58,13 @@ describe("qLangServer", () => { onDidChangeConfiguration() {}, onRequest() {}, onSelectionRanges() {}, + languages: { + callHierarchy: { + onPrepare() {}, + onIncomingCalls() {}, + onOutgoingCalls() {}, + }, + }, }); const params = { @@ -80,6 +88,7 @@ describe("qLangServer", () => { assert.ok(capabilities.renameProvider); assert.ok(capabilities.completionProvider); assert.ok(capabilities.selectionRangeProvider); + assert.ok(capabilities.callHierarchyProvider); }); }); @@ -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); + }); + }); }); diff --git a/test/suite/utils.test.ts b/test/suite/utils.test.ts index 98b00083..e366ac6d 100644 --- a/test/suite/utils.test.ts +++ b/test/suite/utils.test.ts @@ -1886,7 +1886,7 @@ describe("Utils", () => { assert.strictEqual(showInformationMessageStub.called, false); assert.strictEqual(executeCommandStub.called, false); }); - it("should continue if 'neverShowQInstallAgain' is false", async () => { + it.skip("should continue if 'neverShowQInstallAgain' is false", async () => { getConfigurationStub() .get.withArgs("kdb.neverShowQInstallAgain") .returns(false); @@ -1897,7 +1897,7 @@ describe("Utils", () => { assert.strictEqual(executeCommandStub.called, true); }); - it("should handle 'Never show again' response", async () => { + it.skip("should handle 'Never show again' response", async () => { getConfigurationStub() .get.withArgs("kdb.qHomeDirectory") .returns(undefined); From af70d50d9d02750aab27a549677316f96d908878 Mon Sep 17 00:00:00 2001 From: ecmel Date: Tue, 1 Oct 2024 11:52:24 +0300 Subject: [PATCH 2/2] do not skip tests --- test/suite/utils.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/suite/utils.test.ts b/test/suite/utils.test.ts index e366ac6d..98b00083 100644 --- a/test/suite/utils.test.ts +++ b/test/suite/utils.test.ts @@ -1886,7 +1886,7 @@ describe("Utils", () => { assert.strictEqual(showInformationMessageStub.called, false); assert.strictEqual(executeCommandStub.called, false); }); - it.skip("should continue if 'neverShowQInstallAgain' is false", async () => { + it("should continue if 'neverShowQInstallAgain' is false", async () => { getConfigurationStub() .get.withArgs("kdb.neverShowQInstallAgain") .returns(false); @@ -1897,7 +1897,7 @@ describe("Utils", () => { assert.strictEqual(executeCommandStub.called, true); }); - it.skip("should handle 'Never show again' response", async () => { + it("should handle 'Never show again' response", async () => { getConfigurationStub() .get.withArgs("kdb.qHomeDirectory") .returns(undefined);