From 7cda2e9f1b082b74947bba8ed9e141e764c1b0cb Mon Sep 17 00:00:00 2001 From: Binh Duc Tran Date: Sun, 7 Jan 2024 14:56:05 +0700 Subject: [PATCH] fix(startup): fix start standalone outside vscode test(standalone): test startup success docs: add external vscode usage session --- CHANGELOG.md | 1 + README.md | 6 ++ bin/cucumber-language-server.cjs | 10 +-- src/wasm/startStandaloneServer.ts | 4 + test/CucumberLanguageServer.test.ts | 23 +---- test/standalone.test.ts | 125 ++++++++++++++++++++++++++++ 6 files changed, 142 insertions(+), 27 deletions(-) create mode 100644 test/standalone.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 242e5687..fbb68956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fixed c-sharp glob paths for step definitions and feature files - [#89](https://github.com/cucumber/language-server/pull/89) - Specify minimum supported node version ([#100](https://github.com/cucumber/language-server/pull/100)) +- Fixed the issue preventing standalone operation outside of VS Code - [#74](https://github.com/cucumber/language-server/pull/74) ### Added - Allow Javascript/Typescript glue files with the following file extensions: cjs, mjs, cts, mts - [#85](https://github.com/cucumber/language-server/pull/85) diff --git a/README.md b/README.md index 52da5609..9c7eb9ce 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,9 @@ provide them. The server retrieves `cucumber.*` settings from the client with a [workspace/configuration](https://microsoft.github.io/language-server-protocol/specification#workspace_configuration) request. See [Settings](https://github.com/cucumber/language-server/blob/main/src/types.ts) for details about the expected format. + +## External VSCode Usage + +We've encountered an issue with the Node version used by [Treesitter](https://github.com/tree-sitter/tree-sitter/issues/2338), a +dependency of this language server, when working outside of VSCode. For optimal +compatibility, please use the same Node version as version 18 of VSCode. diff --git a/bin/cucumber-language-server.cjs b/bin/cucumber-language-server.cjs index 1e623d9a..55b4a79b 100755 --- a/bin/cucumber-language-server.cjs +++ b/bin/cucumber-language-server.cjs @@ -3,13 +3,11 @@ require('source-map-support').install() const { startStandaloneServer } = require('../dist/cjs/src/wasm/startStandaloneServer') const { NodeFiles } = require('../dist/cjs/src/node/NodeFiles') -const url = require('url') -const { version } = require('../src/version') +const path = require('path') +const { version } = require('../dist/cjs/src/version') -const wasmBaseUrl = url.pathToFileURL( - `${__dirname}/../node_modules/@cucumber/language-service/dist` -) -const { connection } = startStandaloneServer(wasmBaseUrl.href, (rootUri) => new NodeFiles(rootUri)) +const wasmBasePath = path.resolve(`${__dirname}/../node_modules/@cucumber/language-service/dist`) +const { connection } = startStandaloneServer(wasmBasePath, (rootUri) => new NodeFiles(rootUri)) // Don't die on unhandled Promise rejections process.on('unhandledRejection', (reason, p) => { diff --git a/src/wasm/startStandaloneServer.ts b/src/wasm/startStandaloneServer.ts index 23586964..c91f6f6b 100644 --- a/src/wasm/startStandaloneServer.ts +++ b/src/wasm/startStandaloneServer.ts @@ -12,4 +12,8 @@ export function startStandaloneServer(wasmBaseUrl: string, makeFiles: (rootUri: const documents = new TextDocuments(TextDocument) new CucumberLanguageServer(connection, documents, adapter, makeFiles, () => undefined) connection.listen() + + return { + connection, + } } diff --git a/test/CucumberLanguageServer.test.ts b/test/CucumberLanguageServer.test.ts index fec2e4fd..1ebe0307 100644 --- a/test/CucumberLanguageServer.test.ts +++ b/test/CucumberLanguageServer.test.ts @@ -1,7 +1,7 @@ import { WasmParserAdapter } from '@cucumber/language-service/wasm' import assert from 'assert' import { Duplex } from 'stream' -import { Logger, StreamMessageReader, StreamMessageWriter } from 'vscode-jsonrpc/node' +import { NullLogger, StreamMessageReader, StreamMessageWriter } from 'vscode-jsonrpc/node' import { Connection, createProtocolConnection, @@ -80,11 +80,10 @@ describe('CucumberLanguageServer', () => { }, workspaceFolders: null, } - const logger = new NullLogger() clientConnection = createProtocolConnection( new StreamMessageReader(outputStream), new StreamMessageWriter(inputStream), - logger + NullLogger ) clientConnection.onError((err) => { console.error('ERROR', err) @@ -206,21 +205,3 @@ class TestStream extends Duplex { // no-op } } - -class NullLogger implements Logger { - error(): void { - // no-op - } - - warn(): void { - // no-op - } - - info(): void { - // no-op - } - - log(): void { - // no-op - } -} diff --git a/test/standalone.test.ts b/test/standalone.test.ts new file mode 100644 index 00000000..2475b9a1 --- /dev/null +++ b/test/standalone.test.ts @@ -0,0 +1,125 @@ +import assert from 'assert' +import { ChildProcess, fork } from 'child_process' +import { Duplex } from 'stream' +import { NullLogger, StreamMessageReader, StreamMessageWriter } from 'vscode-jsonrpc/node' +import { + createProtocolConnection, + DidChangeConfigurationNotification, + DidChangeConfigurationParams, + InitializeParams, + InitializeRequest, + LogMessageNotification, + LogMessageParams, + ProtocolConnection, +} from 'vscode-languageserver' + +import { Settings } from '../src/types.js' + +describe('Standalone', () => { + let serverFork: ChildProcess + let logMessages: LogMessageParams[] + let clientConnection: ProtocolConnection + + beforeEach(async () => { + logMessages = [] + serverFork = fork('./bin/cucumber-language-server.cjs', ['--stdio'], { + silent: true, + }) + + const initializeParams: InitializeParams = { + rootUri: `file://${process.cwd()}`, + processId: NaN, // This id is used by vscode-languageserver. Set as NaN so that the watchdog responsible for watching this process does not run. + capabilities: { + workspace: { + configuration: true, + didChangeWatchedFiles: { + dynamicRegistration: true, + }, + }, + textDocument: { + moniker: { + dynamicRegistration: false, + }, + completion: { + completionItem: { + snippetSupport: true, + }, + }, + semanticTokens: { + tokenTypes: [], + tokenModifiers: [], + formats: [], + requests: {}, + }, + formatting: { + dynamicRegistration: true, + }, + }, + }, + workspaceFolders: null, + } + if (!serverFork.stdin || !serverFork.stdout) { + throw 'Process created without stdio streams' + } + clientConnection = createProtocolConnection( + new StreamMessageReader(serverFork.stdout as Duplex), + new StreamMessageWriter(serverFork.stdin as Duplex), + NullLogger + ) + clientConnection.onError((err) => { + console.error('ERROR', err) + }) + clientConnection.onNotification(LogMessageNotification.type, (params) => { + if (params.type !== 3) { + logMessages.push(params) + } + }) + clientConnection.onUnhandledNotification((n) => { + console.error('Unhandled notification', n) + }) + clientConnection.listen() + const { serverInfo } = await clientConnection.sendRequest( + InitializeRequest.type, + initializeParams + ) + assert.strictEqual(serverInfo?.name, 'Cucumber Language Server') + }) + + afterEach(() => { + clientConnection.end() + clientConnection.dispose() + serverFork.kill('SIGTERM') // Try to terminate first + serverFork.kill('SIGKILL') // Then try to kill if it is not killed + }) + + context('workspace/didChangeConfiguration', () => { + it(`startup success`, async () => { + // First we need to configure the server, telling it where to find Gherkin documents and Glue code. + // Note that *pushing* settings from the client to the server is deprecated in the LSP. We're only using it + // here because it's easier to implement in the test. + const settings: Settings = { + features: ['testdata/**/*.feature'], + glue: ['testdata/**/*.js'], + parameterTypes: [], + snippetTemplates: {}, + } + const configParams: DidChangeConfigurationParams = { + settings, + } + + await clientConnection.sendNotification(DidChangeConfigurationNotification.type, configParams) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + + assert.strictEqual( + logMessages.length, + // TODO: change this to 0 when `workspace/semanticTokens/refresh` issue was solved + 1, + // print readable log messages + logMessages + .map(({ type, message }) => `**Type**: ${type}\n**Message**:\n${message}\n`) + .join('\n--------------------\n') + ) + }) + }) +})