From 6dddd3c59d6702665eebf7ca5dde998435e63281 Mon Sep 17 00:00:00 2001 From: Shuang Wu Date: Fri, 27 Jan 2023 20:32:01 -0500 Subject: [PATCH] feat: support qmlls --- package-lock.json | 91 +++++++++++++++++----- package.json | 22 ++++++ python/.coveragerc | 1 + python/scripts/qmlls.py | 14 ++++ python/tests/test_qmlls.py | 17 +++++ src/configurations.ts | 4 +- src/extension.ts | 29 ++++++- src/predefined-variable-resolver.ts | 12 +-- src/python.ts | 6 +- src/qmlls/client.ts | 112 ++++++++++++++++++++++++++++ src/run.ts | 6 +- src/test/suite/run.test.ts | 10 ++- src/test/suite/utils.test.ts | 35 ++++++++- src/tool-utils.ts | 2 +- src/types.ts | 2 +- src/utils.ts | 19 +++++ 16 files changed, 345 insertions(+), 37 deletions(-) create mode 100644 python/scripts/qmlls.py create mode 100644 python/tests/test_qmlls.py create mode 100644 src/qmlls/client.ts diff --git a/package-lock.json b/package-lock.json index fbee711..dfd157b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "vscode-languageclient": "^8.0.2", "vscode-uri": "^3.0.3", "zod": "^3.20.2" }, @@ -1437,8 +1438,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/big-integer": { "version": "1.6.51", @@ -1481,7 +1481,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1751,8 +1750,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/conventional-changelog-angular": { "version": "5.0.13", @@ -3581,7 +3579,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -3694,7 +3691,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4658,7 +4654,6 @@ "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -5217,6 +5212,41 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz", + "integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz", + "integrity": "sha512-lHlthJtphG9gibGb/y72CKqQUxwPsMXijJVpHEC2bvbFqxmkj9LwQ3aGU9dwjBLqsX1S4KjShYppLvg1UJDF/Q==", + "dependencies": { + "minimatch": "^3.0.4", + "semver": "^7.3.5", + "vscode-languageserver-protocol": "3.17.2" + }, + "engines": { + "vscode": "^1.67.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz", + "integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==", + "dependencies": { + "vscode-jsonrpc": "8.0.2", + "vscode-languageserver-types": "3.17.2" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz", + "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==" + }, "node_modules/vscode-uri": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", @@ -5316,8 +5346,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "2.2.1", @@ -6415,8 +6444,7 @@ "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "big-integer": { "version": "1.6.51", @@ -6450,7 +6478,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6653,8 +6680,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "conventional-changelog-angular": { "version": "5.0.13", @@ -8035,7 +8061,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -8117,7 +8142,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8812,7 +8836,6 @@ "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, "requires": { "lru-cache": "^6.0.0" } @@ -9240,6 +9263,35 @@ "spdx-expression-parse": "^3.0.0" } }, + "vscode-jsonrpc": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz", + "integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==" + }, + "vscode-languageclient": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz", + "integrity": "sha512-lHlthJtphG9gibGb/y72CKqQUxwPsMXijJVpHEC2bvbFqxmkj9LwQ3aGU9dwjBLqsX1S4KjShYppLvg1UJDF/Q==", + "requires": { + "minimatch": "^3.0.4", + "semver": "^7.3.5", + "vscode-languageserver-protocol": "3.17.2" + } + }, + "vscode-languageserver-protocol": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz", + "integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==", + "requires": { + "vscode-jsonrpc": "8.0.2", + "vscode-languageserver-types": "3.17.2" + } + }, + "vscode-languageserver-types": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz", + "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA==" + }, "vscode-uri": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", @@ -9317,8 +9369,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "2.2.1", diff --git a/package.json b/package.json index 00bf842..a05a4b2 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "prepare": "husky install" }, "dependencies": { + "vscode-languageclient": "^8.0.2", "vscode-uri": "^3.0.3", "zod": "^3.20.2" }, @@ -238,6 +239,27 @@ "markdownDescription": "The options passed to Qt `qmllint` executable. See [here](https://github.com/seanwu1105/vscode-qt-for-python#predefined-variables) for a detailed list of predefined variables.", "scope": "resource" }, + "qtForPython.qmlls.enabled": { + "type": "boolean", + "default": true, + "markdownDescription": "Enable the Qt `qmlls` integration.", + "scope": "window" + }, + "qtForPython.qmlls.path": { + "type": "string", + "default": "", + "markdownDescription": "The path to Qt `qmlls` executable. Set to empty string to automatically resolve from the installed Python package. See [here](https://github.com/seanwu1105/vscode-qt-for-python#predefined-variables) for a detailed list of predefined variables.", + "scope": "window" + }, + "qtForPython.qmlls.options": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "markdownDescription": "The options passed to Qt `qmlls` executable. See [here](https://github.com/seanwu1105/vscode-qt-for-python#predefined-variables) for a detailed list of predefined variables.", + "scope": "window" + }, "qtForPython.rcc.path": { "type": "string", "default": "", diff --git a/python/.coveragerc b/python/.coveragerc index 5893891..77b9e33 100644 --- a/python/.coveragerc +++ b/python/.coveragerc @@ -2,6 +2,7 @@ omit = **/test_*.py scripts/qmllint.py + scripts/qmlls.py scripts/rcc.py scripts/uic.py scripts/designer.py diff --git a/python/scripts/qmlls.py b/python/scripts/qmlls.py new file mode 100644 index 0000000..2b44b2d --- /dev/null +++ b/python/scripts/qmlls.py @@ -0,0 +1,14 @@ +# pylint: disable=import-error + +import re +import sys + +from utils import is_installed + +if __name__ == "__main__": + if is_installed("PySide6"): + from PySide6.scripts.pyside_tool import qmlls + else: + sys.exit("No qmlls can be found in current Python environment.") + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(qmlls()) diff --git a/python/tests/test_qmlls.py b/python/tests/test_qmlls.py new file mode 100644 index 0000000..eb2c39f --- /dev/null +++ b/python/tests/test_qmlls.py @@ -0,0 +1,17 @@ +import subprocess + +from tests import SCRIPTS_DIR + + +def test_qmlls_help(): + result = invoke_qmlls_py(["--help"]) + assert result.returncode == 0 + assert len(result.stdout.decode("utf-8")) > 0 + + +def invoke_qmlls_py(args: list[str]): + return subprocess.run( + ["poetry", "run", "python", "qmlls.py", *args], + cwd=SCRIPTS_DIR, + capture_output=True, + ) diff --git a/src/configurations.ts b/src/configurations.ts index e509bcc..2a61a49 100644 --- a/src/configurations.ts +++ b/src/configurations.ts @@ -17,7 +17,7 @@ export function getPathFromConfig({ tool, resource }: GetPathFromConfig) { type GetPathFromConfig = { readonly tool: SupportedTool - readonly resource: URI + readonly resource: URI | undefined } export function getOptionsFromConfig({ @@ -36,5 +36,5 @@ export function getOptionsFromConfig({ type GetOptionsFromConfig = { readonly tool: SupportedTool - readonly resource: URI + readonly resource: URI | undefined } diff --git a/src/extension.ts b/src/extension.ts index 926974a..e8d8a5b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ import { commands, window } from 'vscode' import { COMMANDS } from './commands' import { EXTENSION_NAMESPACE } from './constants' import { registerQmlLint } from './qmllint/register' +import { registerQmlLanguageServer } from './qmlls/client' import type { ExecError, StdErrError } from './run' import type { ErrorResult, SuccessResult } from './types' import { registerUicLiveExecution } from './uic/uic-live-execution' @@ -25,6 +26,13 @@ export async function activate(context: ExtensionContext) { extensionPath: context.extensionPath, onResult: onResultReceived, }) + + registerQmlLanguageServer({ + subscriptions: context.subscriptions, + extensionPath: context.extensionPath, + outputChannel, + onResult: onResultReceived, + }) } function registerCommands({ extensionPath, subscriptions }: ExtensionContext) { @@ -64,7 +72,10 @@ function onResultReceived( switch (result.kind) { case 'Success': return outputChannel.appendLine( - JSON.stringify(result.value, null, indent), + prefixLogging({ + message: JSON.stringify(result.value, null, indent), + severity: 'INFO', + }), ) case 'ParseError': case 'TypeError': @@ -81,6 +92,20 @@ function onResultReceived( } async function showError(message: string) { - outputChannel.appendLine(message) + outputChannel.appendLine(prefixLogging({ message, severity: 'ERROR' })) return window.showErrorMessage(message) } + +function prefixLogging({ message, severity }: PrefixLoggingArgs) { + return `[${severity.padEnd( + Math.max(...LoggingSeverity.map(s => s.length)), + )} - ${new Date().toLocaleTimeString()}] ${message}` +} + +type PrefixLoggingArgs = { + readonly message: string + readonly severity: LoggingSeverity +} + +const LoggingSeverity = ['INFO', 'ERROR'] as const +type LoggingSeverity = typeof LoggingSeverity[number] diff --git a/src/predefined-variable-resolver.ts b/src/predefined-variable-resolver.ts index abe5fb9..8670b6b 100644 --- a/src/predefined-variable-resolver.ts +++ b/src/predefined-variable-resolver.ts @@ -5,7 +5,7 @@ import * as os from 'node:os' import * as path from 'node:path' import { env, window, workspace } from 'vscode' import type { URI } from 'vscode-uri' -import { notNil } from './utils' +import { isNil, notNil } from './utils' export function resolvePredefinedVariables({ str, @@ -26,10 +26,10 @@ export function resolvePredefinedVariables({ type ResolvePredefinedVariablesArgs = { readonly str: string - readonly resource: URI + readonly resource: URI | undefined } -function getResolver(resource: URI) { +function getResolver(resource: URI | undefined) { return { userHome: () => os.homedir(), @@ -86,10 +86,12 @@ function getResolver(resource: URI) { // -- Additional Variables -- - resource: () => resource.fsPath, + resource: () => resource?.fsPath ?? '', resourceWorkspaceFolder: () => - workspace.getWorkspaceFolder(resource)?.uri.fsPath ?? '', + isNil(resource) + ? '' + : workspace.getWorkspaceFolder(resource)?.uri.fsPath ?? '', relativeResource: () => path.relative( diff --git a/src/python.ts b/src/python.ts index 5fc9c66..fa18747 100644 --- a/src/python.ts +++ b/src/python.ts @@ -27,7 +27,7 @@ export async function resolveScriptCommand({ export type ResolveScriptCommandArgs = { readonly tool: SupportedTool readonly extensionPath: string - readonly resource: URI + readonly resource: URI | undefined } export type ResolveScriptCommandResult = @@ -35,7 +35,7 @@ export type ResolveScriptCommandResult = | ErrorResult<'NotFound'> async function getPythonInterpreterPath( - resource: URI, + resource: URI | undefined, ): Promise { // Get path with the Python extension public API: https://github.com/microsoft/vscode-python/blob/main/src/client/apiTypes.ts const pythonExtensionApi = await extensions @@ -69,7 +69,7 @@ type GetPythonInterpreterPathResult = // Excerpt from: https://github.com/microsoft/vscode-python/blob/344c912a1c15d07eb9b14bf749c7529a7fa0877b/src/client/apiTypes.ts#L15 export type PythonExtensionApi = { readonly settings: { - getExecutionDetails(resource: URI): { + getExecutionDetails(resource: URI | undefined): { readonly execCommand: CommandArgs | undefined } } diff --git a/src/qmlls/client.ts b/src/qmlls/client.ts new file mode 100644 index 0000000..ee68399 --- /dev/null +++ b/src/qmlls/client.ts @@ -0,0 +1,112 @@ +import type { Disposable, OutputChannel } from 'vscode' +import { workspace } from 'vscode' +import type { + LanguageClientOptions, + ServerOptions, +} from 'vscode-languageclient/node' +import { + LanguageClient, + RevealOutputChannelOn, +} from 'vscode-languageclient/node' +import { wrapAndJoinCommandArgsWithQuotes } from '../run' +import { getToolCommand } from '../tool-utils' +import type { ErrorResult, SuccessResult } from '../types' +import { withConcatMap } from '../utils' + +export async function registerQmlLanguageServer({ + subscriptions, + extensionPath, + outputChannel, + onResult, +}: RegisterQmlLanguageServerArgs) { + let client: LanguageClient | undefined + + subscriptions.push( + // Make sure the configuration is in "window" scope. + workspace.onDidChangeConfiguration( + withConcatMap(async e => { + if (!e.affectsConfiguration('qtForPython.qmlls')) return + + return await activateClient() + }), + ), + ) + + await activateClient() + + async function activateClient() { + if (!workspace.getConfiguration('qtForPython.qmlls').get('enabled')) { + await stopClient() + return + } + + await stopClient() + + const startClientResult = await startClient({ + extensionPath, + outputChannel, + }) + + if (startClientResult.kind !== 'Success') return onResult(startClientResult) + + client = startClientResult.value + } + + async function stopClient() { + await client?.stop() + client = undefined + } +} + +type RegisterQmlLanguageServerArgs = { + readonly subscriptions: Disposable[] + readonly extensionPath: string + readonly outputChannel: OutputChannel + readonly onResult: (result: StartClientResult) => void +} + +async function startClient({ + extensionPath, + outputChannel, +}: StartClientArgs): Promise { + const getToolCommandResult = await getToolCommand({ + tool: 'qmlls', + extensionPath, + resource: undefined, + }) + + if (getToolCommandResult.kind !== 'Success') return getToolCommandResult + + const serverOptions: ServerOptions = { + command: wrapAndJoinCommandArgsWithQuotes( + getToolCommandResult.value.command, + ), + args: [...getToolCommandResult.value.options], + options: { shell: true }, + } + + const clientOptions: LanguageClientOptions = { + documentSelector: [{ scheme: 'file', language: 'qml' }], + outputChannel, + traceOutputChannel: outputChannel, + revealOutputChannelOn: RevealOutputChannelOn.Never, + } + + const client = new LanguageClient( + 'qmlls', + 'QML Language Server', + serverOptions, + clientOptions, + ) + + await client.start() + + return { kind: 'Success', value: client } +} + +type StartClientArgs = { + readonly extensionPath: string + readonly outputChannel: OutputChannel +} + +type StartClientResult = SuccessResult | ErrorResult<'NotFound'> diff --git a/src/run.ts b/src/run.ts index 09566b8..b4628c4 100644 --- a/src/run.ts +++ b/src/run.ts @@ -8,7 +8,7 @@ import { notNil } from './utils' export async function run({ command, cwd }: RunArgs): Promise { return new Promise(resolve => { exec( - command.map(s => (s.includes(' ') ? `"${s}"` : s)).join(' '), + wrapAndJoinCommandArgsWithQuotes(command), { cwd }, (error, stdout, stderr) => { if (notNil(error)) resolve({ kind: 'ExecError', error, stdout, stderr }) @@ -38,3 +38,7 @@ export type StdErrError = { readonly stdout: string readonly stderr: string } + +export function wrapAndJoinCommandArgsWithQuotes(args: CommandArgs): string { + return args.map(arg => (arg.includes(' ') ? `"${arg}"` : arg)).join(' ') +} diff --git a/src/test/suite/run.test.ts b/src/test/suite/run.test.ts index f0f799a..6e98768 100644 --- a/src/test/suite/run.test.ts +++ b/src/test/suite/run.test.ts @@ -2,7 +2,7 @@ import * as assert from 'node:assert' import * as path from 'node:path' import * as process from 'node:process' import type { RunResult } from '../../run' -import { run } from '../../run' +import { run, wrapAndJoinCommandArgsWithQuotes } from '../../run' suite('run', () => { let result: RunResult @@ -35,3 +35,11 @@ suite('run', () => { }, ) }) + +suite('wrapAndJoinCommandArgsWithQuotes', () => { + test('should wrap and join command args with quotes', async () => { + const args = ['a', 'b c', 'd'] + const expected = 'a "b c" d' + assert.strictEqual(wrapAndJoinCommandArgsWithQuotes(args), expected) + }) +}) diff --git a/src/test/suite/utils.test.ts b/src/test/suite/utils.test.ts index 9f773e3..42c119c 100644 --- a/src/test/suite/utils.test.ts +++ b/src/test/suite/utils.test.ts @@ -1,5 +1,7 @@ +/* eslint-disable @typescript-eslint/no-magic-numbers */ import * as assert from 'node:assert' -import { isNil, notNil } from '../../utils' +import { isNil, notNil, withConcatMap } from '../../utils' +import { sleep, waitFor } from './test-utils' suite('utils', () => { test('notNil', () => { @@ -21,4 +23,35 @@ suite('utils', () => { assert.strictEqual(isNil([]), false) assert.strictEqual(isNil({}), false) }) + + suite('withConcatMap', () => { + let executedValues: number[] + + const handler = withConcatMap(async (e: number) => { + await sleep(e) + executedValues.push(e) + }) + + suite('only one event', () => { + const events = [10] + + setup(() => (executedValues = [])) + + test('should get result value in sequential order', async () => { + events.forEach(handler) + await waitFor(() => assert.deepStrictEqual(executedValues, events)) + }) + }) + + suite('three events', () => { + const events = [10, 5, 4] + + setup(() => (executedValues = [])) + + test('should get result value in sequential order', async () => { + events.forEach(handler) + await waitFor(() => assert.deepStrictEqual(executedValues, events)) + }) + }) + }) }) diff --git a/src/tool-utils.ts b/src/tool-utils.ts index 83c9dd8..9050f06 100644 --- a/src/tool-utils.ts +++ b/src/tool-utils.ts @@ -40,7 +40,7 @@ export async function getToolCommand({ type GetToolCommandArgs = { readonly tool: SupportedTool readonly extensionPath: string - readonly resource: URI + readonly resource: URI | undefined } export type GetToolCommandResult = diff --git a/src/types.ts b/src/types.ts index 7a54771..736fd33 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ // Should NOT depend on vscode -export type SupportedTool = 'qmllint' | 'rcc' | 'uic' | 'designer' +export type SupportedTool = 'qmllint' | 'qmlls' | 'rcc' | 'uic' | 'designer' export type SuccessResult = { readonly kind: `${Name}Success` diff --git a/src/utils.ts b/src/utils.ts index 7ae9b12..b8a79e0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,3 +9,22 @@ export function isNil( ): value is undefined | null { return !notNil(value) } + +export function withConcatMap(callback: (event: E) => Promise) { + const queue: E[] = [] + + return async (event: E) => { + queue.push(event) + + if (queue.length > 1) return // Already running, leave this new one for later. + + while (queue.length > 0) { + const event = queue[0] + if (isNil(event)) continue + + await callback(event) + + queue.shift() + } + } +}