From 1500b582b3caa28d0c36a9e620d04cd8274c2ffb Mon Sep 17 00:00:00 2001 From: ecmel Date: Wed, 2 Oct 2024 16:05:55 +0300 Subject: [PATCH] implemented core functionality --- server/src/qLangServer.ts | 238 +++++++++++++++++++++------------ server/src/server.ts | 13 +- src/extension.ts | 14 -- test/suite/qLangServer.test.ts | 1 + test/suite/utils.test.ts | 5 + 5 files changed, 166 insertions(+), 105 deletions(-) diff --git a/server/src/qLangServer.ts b/server/src/qLangServer.ts index c337c12e..5b549e89 100644 --- a/server/src/qLangServer.ts +++ b/server/src/qLangServer.ts @@ -27,8 +27,10 @@ import { Diagnostic, DiagnosticSeverity, DidChangeConfigurationParams, + DidChangeWatchedFilesParams, DocumentSymbol, DocumentSymbolParams, + FileChangeType, InitializeParams, LSPAny, Location, @@ -47,6 +49,8 @@ import { TextEdit, WorkspaceEdit, } from "vscode-languageserver/node"; +import { glob } from "glob"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { FindKind, Token, @@ -69,24 +73,37 @@ import { RCurly, } from "./parser"; import { lint } from "./linter"; +import { readFile } from "node:fs"; interface Settings { debug: boolean; linting: boolean; + refactoring: "Workspace" | "Window"; } -const defaultSettings: Settings = { debug: false, linting: false }; +const defaultSettings: Settings = { + debug: false, + linting: false, + refactoring: "Workspace", +}; + +interface Tokenized { + uri: string; + tokens: Token[]; +} export default class QLangServer { private declare connection: Connection; private declare params: InitializeParams; private declare settings: Settings; + private declare cached: Map; public declare documents: TextDocuments; constructor(connection: Connection, params: InitializeParams) { this.connection = connection; this.params = params; this.settings = defaultSettings; + this.cached = new Map(); this.documents = new TextDocuments(TextDocument); this.documents.listen(this.connection); this.documents.onDidClose(this.onDidClose.bind(this)); @@ -96,6 +113,9 @@ 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.onDidChangeWatchedFiles( + this.onDidChangeWatchedFiles.bind(this), + ); this.connection.languages.callHierarchy.onPrepare( this.onPrepareCallHierarchy.bind(this), ); @@ -133,21 +153,30 @@ export default class QLangServer { } public setSettings(settings: LSPAny) { - this.settings = settings; + this.settings = { + debug: settings.debug || false, + linting: settings.linting || false, + refactoring: settings.refactoring || "Workspace", + }; } public onDidChangeConfiguration({ settings }: DidChangeConfigurationParams) { if ("kdb" in settings) { - const kdb = settings.kdb; - this.setSettings({ - debug: kdb.debug_parser === true || false, - linting: kdb.linting === true || false, - }); + this.setSettings(settings.kdb); } } - public onDidClose({ document }: TextDocumentChangeEvent) { - this.connection.sendDiagnostics({ uri: document.uri, diagnostics: [] }); + public onDidChangeWatchedFiles({ changes }: DidChangeWatchedFilesParams) { + this.parseFiles( + changes.reduce((matches, change) => { + if (change.type === FileChangeType.Deleted) { + this.cached.delete(change.uri); + } else { + matches.push(fileURLToPath(change.uri)); + } + return matches; + }, [] as string[]), + ); } public onDidChangeContent({ @@ -168,6 +197,10 @@ export default class QLangServer { } } + public onDidClose({ document }: TextDocumentChangeEvent) { + this.connection.sendDiagnostics({ uri: document.uri, diagnostics: [] }); + } + public onDocumentSymbol({ textDocument, }: DocumentSymbolParams): DocumentSymbol[] { @@ -187,14 +220,11 @@ export default class QLangServer { public onReferences({ textDocument, position }: ReferenceParams): Location[] { const tokens = this.parse(textDocument); const source = positionToToken(tokens, position); - return this.documents - .all() + return this.context({ uri: textDocument.uri, tokens }) .map((document) => - findIdentifiers( - FindKind.Reference, - document.uri === textDocument.uri ? tokens : this.parse(document), - source, - ).map((token) => Location.create(document.uri, rangeFromToken(token))), + findIdentifiers(FindKind.Reference, document.tokens, source).map( + (token) => Location.create(document.uri, rangeFromToken(token)), + ), ) .flat(); } @@ -205,14 +235,11 @@ export default class QLangServer { }: DefinitionParams): Location[] { const tokens = this.parse(textDocument); const source = positionToToken(tokens, position); - return this.documents - .all() + return this.context({ uri: textDocument.uri, tokens }) .map((document) => - findIdentifiers( - FindKind.Definition, - document.uri === textDocument.uri ? tokens : this.parse(document), - source, - ).map((token) => Location.create(document.uri, rangeFromToken(token))), + findIdentifiers(FindKind.Definition, document.tokens, source).map( + (token) => Location.create(document.uri, rangeFromToken(token)), + ), ) .flat(); } @@ -224,13 +251,10 @@ export default class QLangServer { }: RenameParams): WorkspaceEdit { const tokens = this.parse(textDocument); const source = positionToToken(tokens, position); - return this.documents.all().reduce( + const all = this.settings.refactoring === "Workspace"; + return this.context({ uri: textDocument.uri, tokens }, all).reduce( (edit, document) => { - const refs = findIdentifiers( - FindKind.Rename, - document.uri === textDocument.uri ? tokens : this.parse(document), - source, - ); + const refs = findIdentifiers(FindKind.Rename, document.tokens, source); if (refs.length > 0) { const name = { image: newName, @@ -252,23 +276,20 @@ export default class QLangServer { }: CompletionParams): CompletionItem[] { const tokens = this.parse(textDocument); const source = positionToToken(tokens, position); - return this.documents - .all() + return this.context({ uri: textDocument.uri, tokens }) .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), - }; - }), + findIdentifiers(FindKind.Completion, document.tokens, source).map( + (token) => { + return { + label: token.image, + labelDetails: { + detail: ` .${namespace(token)}`, + }, + kind: CompletionItemKind.Variable, + insertText: relative(token, source), + }; + }, + ), ) .flat(); } @@ -369,26 +390,27 @@ export default class QLangServer { }: 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) && item.data) - .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(); + return item.data + ? this.context({ uri: item.uri, tokens }) + .map((document) => + findIdentifiers(FindKind.Reference, document.tokens, 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({ @@ -396,25 +418,54 @@ export default class QLangServer { }: 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) && item.data) - .map((token) => { - return { - to: { - kind: SymbolKind.Object, - name: token.image, - uri: document.uri, - range: rangeFromToken(inLambda(token)!), - selectionRange: rangeFromToken(token), - }, - fromRanges: [], - } as CallHierarchyOutgoingCall; - }), - ) - .flat(); + return item.data + ? this.context({ uri: item.uri, tokens }) + .map((document) => + findIdentifiers(FindKind.Reference, document.tokens, 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() + : []; + } + + public scan() { + this.connection.workspace.getWorkspaceFolders().then((folders) => { + if (folders) { + folders.forEach((folder) => { + glob( + "**/*.{q,quke}", + { cwd: fileURLToPath(folder.uri), ignore: "node_modules/**" }, + (err, matches) => { + if (!err) { + this.parseFiles(matches); + } + }, + ); + }); + } + }); + } + + private parseFiles(matches: string[]) { + matches.forEach((match) => + readFile(match, "utf-8", (err, file) => { + if (!err) { + this.cached.set(pathToFileURL(match).toString(), parse(file)); + } + }), + ); } private parse(textDocument: TextDocumentIdentifier): Token[] { @@ -424,6 +475,25 @@ export default class QLangServer { } return parse(document.getText()); } + + private context({ uri, tokens }: Tokenized, all = true): Tokenized[] { + if (all) { + this.documents.all().forEach((document) => { + this.cached.set( + document.uri, + document.uri === uri ? tokens : parse(document.getText()), + ); + }); + return Array.from(this.cached.entries(), (entry) => ({ + uri: entry[0], + tokens: entry[1], + })); + } + return this.documents.all().map((document) => ({ + uri: document.uri, + tokens: document.uri === uri ? tokens : parse(document.getText()), + })); + } } function rangeFromToken(token: Token): Range { diff --git a/server/src/server.ts b/server/src/server.ts index 32ed684f..9225fefa 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -35,13 +35,12 @@ connection.onInitialized(() => { section: "kdb", }); - if (connection.workspace) { - connection.workspace.getConfiguration("kdb").then((settings) => { - if (server) { - server.setSettings(settings); - } - }); - } + connection.workspace.getConfiguration("kdb").then((settings) => { + if (server) { + server.setSettings(settings); + server.scan(); + } + }); }); connection.listen(); diff --git a/src/extension.ts b/src/extension.ts index 22605719..05c51176 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,7 +18,6 @@ import { EventEmitter, ExtensionContext, Range, - TabInputText, TextDocumentContentProvider, Uri, WorkspaceEdit, @@ -686,19 +685,6 @@ 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 f72e67d5..243db3bf 100644 --- a/test/suite/qLangServer.test.ts +++ b/test/suite/qLangServer.test.ts @@ -58,6 +58,7 @@ describe("qLangServer", () => { onDidChangeConfiguration() {}, onRequest() {}, onSelectionRanges() {}, + onDidChangeWatchedFiles() {}, languages: { callHierarchy: { onPrepare() {}, diff --git a/test/suite/utils.test.ts b/test/suite/utils.test.ts index 98b00083..c3fa43c0 100644 --- a/test/suite/utils.test.ts +++ b/test/suite/utils.test.ts @@ -15,6 +15,7 @@ import * as assert from "assert"; import * as sinon from "sinon"; import * as vscode from "vscode"; import mock from "mock-fs"; +import { env } from "node:process"; import { TreeItemCollapsibleState } from "vscode"; import { ext } from "../../src/extensionVariables"; import * as QTable from "../../src/ipc/QTable"; @@ -1854,6 +1855,7 @@ describe("Utils", () => { let updateConfigurationStub: sinon.SinonStub; let showInformationMessageStub: sinon.SinonStub; let executeCommandStub: sinon.SinonStub; + let QHOME = ""; beforeEach(() => { getConfigurationStub = sinon @@ -1870,9 +1872,12 @@ describe("Utils", () => { executeCommandStub = sinon .stub(vscode.commands, "executeCommand") .resolves(); + QHOME = env.QHOME; + env.QHOME = ""; }); afterEach(() => { + env.QHOME = QHOME; sinon.restore(); });