From d43361656e0d4ff599f91378d784e25fa5f3f936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Mon, 29 Aug 2022 19:43:15 +0100 Subject: [PATCH 01/10] Refactor --- test/CucumberLanguageServer.test.ts | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/test/CucumberLanguageServer.test.ts b/test/CucumberLanguageServer.test.ts index 9b9a4aea..ebf8c448 100644 --- a/test/CucumberLanguageServer.test.ts +++ b/test/CucumberLanguageServer.test.ts @@ -35,21 +35,6 @@ describe('CucumberLanguageServer', () => { beforeEach(async () => { inputStream = new TestStream() outputStream = new TestStream() - const logger = new NullLogger() - clientConnection = createProtocolConnection( - new StreamMessageReader(outputStream), - new StreamMessageWriter(inputStream), - logger - ) - clientConnection.onError((err) => { - console.error('ERROR', err) - }) - // Ignore log messages - clientConnection.onNotification(LogMessageNotification.type, () => undefined) - clientConnection.onUnhandledNotification((n) => { - console.error('Unhandled notification', n) - }) - clientConnection.listen() serverConnection = createConnection(inputStream, outputStream) documents = new TextDocuments(TextDocument) @@ -92,6 +77,21 @@ describe('CucumberLanguageServer', () => { }, workspaceFolders: null, } + const logger = new NullLogger() + clientConnection = createProtocolConnection( + new StreamMessageReader(outputStream), + new StreamMessageWriter(inputStream), + logger + ) + clientConnection.onError((err) => { + console.error('ERROR', err) + }) + // Ignore log messages + clientConnection.onNotification(LogMessageNotification.type, () => undefined) + clientConnection.onUnhandledNotification((n) => { + console.error('Unhandled notification', n) + }) + clientConnection.listen() const { serverInfo } = await clientConnection.sendRequest( InitializeRequest.type, initializeParams From 5667a9c44dfa2896040d3330bb40a8d46356b99e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Mon, 29 Aug 2022 23:53:11 +0100 Subject: [PATCH 02/10] Refactor to run in-process (in VSCode) --- package.json | 10 +++++++--- src/CucumberLanguageServer.ts | 12 ++++++++---- src/fs.ts | 17 ++++++++++------- src/index.ts | 2 ++ src/newWasmServer.ts | 29 +++++++++++++++++++++++++++++ src/node/startNodeServer.ts | 2 +- src/version.ts | 2 +- src/wasm/startWasmServer.ts | 2 +- 8 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 src/index.ts create mode 100644 src/newWasmServer.ts diff --git a/package.json b/package.json index 7c8df3fc..6b1003a1 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,15 @@ "version": "0.12.13", "description": "Cucumber Language Server", "type": "module", - "main": "dist/cjs/src/node/startNodeServer.js", - "module": "dist/esm/src/node/startNodeServer.js", - "types": "dist/esm/src/node/startNodeServer.d.ts", + "main": "dist/cjs/src/index.js", + "module": "dist/esm/src/index.js", + "types": "dist/esm/src/index.d.ts", "exports": { ".": { + "import": "./dist/esm/src/index.js", + "require": "./dist/cjs/src/index.js" + }, + "./node": { "import": "./dist/esm/src/node/startNodeServer.js", "require": "./dist/cjs/src/node/startNodeServer.js" }, diff --git a/src/CucumberLanguageServer.ts b/src/CucumberLanguageServer.ts index 9fa6b21d..0c344aec 100644 --- a/src/CucumberLanguageServer.ts +++ b/src/CucumberLanguageServer.ts @@ -90,7 +90,9 @@ export class CucumberLanguageServer { parserAdapter: ParserAdapter ) { this.expressionBuilder = new ExpressionBuilder(parserAdapter) + connection.onInitialize(async (params) => { + connection.console.log(`PARAMS: ${JSON.stringify(params, null, 2)}`) await parserAdapter.init() if (params.clientInfo) { connection.console.info( @@ -102,10 +104,12 @@ export class CucumberLanguageServer { if (params.rootPath) { this.rootPath = params.rootPath + } else if (params.rootUri) { + this.rootPath = new URL(params.rootUri).pathname } else if (params.workspaceFolders && params.workspaceFolders.length > 0) { this.rootPath = new URL(params.workspaceFolders[0].uri).pathname } else { - connection.console.error(`Client did not send rootPath or workspaceFolders`) + connection.console.error(`Could not determine rootPath`) } // Some users have reported that the globs don't find any files. This is to debug that issue connection.console.info(`Root path : ${this.rootPath}`) @@ -376,8 +380,8 @@ export class CucumberLanguageServer { // TODO: Send WorkDoneProgressBegin notification // https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workDoneProgress - this.connection.console.info(`Reindexing...`) - const gherkinSources = await loadGherkinSources(settings.features) + this.connection.console.info(`Reindexing ${this.rootPath}`) + const gherkinSources = await loadGherkinSources(this.rootPath, settings.features) this.connection.console.info( `* Found ${gherkinSources.length} feature file(s) in ${JSON.stringify(settings.features)}` ) @@ -386,7 +390,7 @@ export class CucumberLanguageServer { [] ) this.connection.console.info(`* Found ${stepTexts.length} steps in those feature files`) - const glueSources = await loadGlueSources(settings.glue) + const glueSources = await loadGlueSources(this.rootPath, settings.glue) this.connection.console.info( `* Found ${glueSources.length} glue file(s) in ${JSON.stringify(settings.glue)}` ) diff --git a/src/fs.ts b/src/fs.ts index cccc32ed..74bd08a4 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -1,7 +1,7 @@ import { LanguageName, Source } from '@cucumber/language-service' import fg from 'fast-glob' import fs from 'fs/promises' -import { extname, resolve as resolvePath } from 'path' +import { extname, join, resolve as resolvePath } from 'path' import url from 'url' export const glueExtByLanguageName: Record = { @@ -19,9 +19,10 @@ const glueLanguageNameByExt = Object.fromEntries( const glueExtensions = new Set(Object.keys(glueLanguageNameByExt)) export async function loadGlueSources( + cwd: string, globs: readonly string[] ): Promise[]> { - return loadSources(globs, glueExtensions, glueLanguageNameByExt) + return loadSources(cwd, globs, glueExtensions, glueLanguageNameByExt) } export function getLanguage(ext: string): LanguageName | undefined { @@ -29,28 +30,30 @@ export function getLanguage(ext: string): LanguageName | undefined { } export async function loadGherkinSources( + cwd: string, globs: readonly string[] ): Promise[]> { - return loadSources(globs, new Set(['.feature']), { '.feature': 'gherkin' }) + return loadSources(cwd, globs, new Set(['.feature']), { '.feature': 'gherkin' }) } type LanguageNameByExt = Record -export async function findPaths(globs: readonly string[]): Promise { +export async function findPaths(cwd: string, globs: readonly string[]): Promise { const pathPromises = globs.reduce[]>((prev, glob) => { - return prev.concat(fg(glob, { caseSensitiveMatch: false, onlyFiles: true })) + return prev.concat(fg(glob, { caseSensitiveMatch: false, onlyFiles: true, cwd })) }, []) const pathArrays = await Promise.all(pathPromises) const paths = pathArrays.flatMap((paths) => paths) - return [...new Set(paths).values()].sort() + return [...new Set(paths).values()].sort().map((path) => join(cwd, path)) } async function loadSources( + cwd: string, globs: readonly string[], extensions: Set, languageNameByExt: LanguageNameByExt ): Promise[]> { - const paths = await findPaths(globs) + const paths = await findPaths(cwd, globs) return Promise.all( paths diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..9fe236b5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export * from './newWasmServer.js' +export * from './startServer.js' diff --git a/src/newWasmServer.ts b/src/newWasmServer.ts new file mode 100644 index 00000000..1b2d4fb9 --- /dev/null +++ b/src/newWasmServer.ts @@ -0,0 +1,29 @@ +import { ParserAdapter } from '@cucumber/language-service' +import { WasmParserAdapter } from '@cucumber/language-service/wasm' +import { PassThrough } from 'stream' +import { TextDocuments } from 'vscode-languageserver' +import { createConnection } from 'vscode-languageserver/node' +import { TextDocument } from 'vscode-languageserver-textdocument' + +import { CucumberLanguageServer } from './CucumberLanguageServer.js' + +export type StreamInfo = { + writer: NodeJS.WritableStream + reader: NodeJS.ReadableStream +} + +export function newWasmServer(wasmBaseUrl: string) { + const adapter: ParserAdapter = new WasmParserAdapter(wasmBaseUrl) + const inputStream = new PassThrough() + const outputStream = new PassThrough() + + const connection = createConnection(inputStream, outputStream) + const documents = new TextDocuments(TextDocument) + new CucumberLanguageServer(connection, documents, adapter) + connection.listen() + + return { + writer: inputStream, + reader: outputStream, + } +} diff --git a/src/node/startNodeServer.ts b/src/node/startNodeServer.ts index 7f23f3c9..9b2dce44 100644 --- a/src/node/startNodeServer.ts +++ b/src/node/startNodeServer.ts @@ -1,6 +1,6 @@ import { NodeParserAdapter } from '@cucumber/language-service/node' -import { startServer } from '../startServer' +import { startServer } from '../startServer.js' export function startNodeServer() { startServer(new NodeParserAdapter()) diff --git a/src/version.ts b/src/version.ts index 518aad3d..41b4675c 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = '0.12.9' +export const version = '0.12.13' diff --git a/src/wasm/startWasmServer.ts b/src/wasm/startWasmServer.ts index 6c3097d5..cf757833 100644 --- a/src/wasm/startWasmServer.ts +++ b/src/wasm/startWasmServer.ts @@ -1,6 +1,6 @@ import { WasmParserAdapter } from '@cucumber/language-service/wasm' -import { startServer } from '../startServer' +import { startServer } from '../startServer.js' export function startWasmServer(wasmBaseUrl: string) { startServer(new WasmParserAdapter(wasmBaseUrl)) From 15238179e30916dd39e509a2584a2f45478a5071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Thu, 1 Sep 2022 18:22:41 +0100 Subject: [PATCH 03/10] Expose suggestions on LanguageServer --- src/CucumberLanguageServer.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/CucumberLanguageServer.ts b/src/CucumberLanguageServer.ts index 0c344aec..161cf011 100644 --- a/src/CucumberLanguageServer.ts +++ b/src/CucumberLanguageServer.ts @@ -12,6 +12,7 @@ import { jsSearchIndex, ParserAdapter, semanticTokenTypes, + Suggestion, } from '@cucumber/language-service' import { stat as statCb } from 'fs' import { extname, relative } from 'path' @@ -83,6 +84,11 @@ export class CucumberLanguageServer { private expressionBuilderResult: ExpressionBuilderResult | undefined = undefined private reindexingTimeout: NodeJS.Timeout private rootPath: string + #suggestions: readonly Suggestion[] + + get suggestions() { + return this.#suggestions + } constructor( private readonly connection: Connection, @@ -426,13 +432,15 @@ export class CucumberLanguageServer { this.connection.languages.semanticTokens.refresh() try { - const suggestions = buildSuggestions( + this.#suggestions = buildSuggestions( this.expressionBuilderResult.registry, stepTexts, this.expressionBuilderResult.expressionLinks.map((l) => l.expression) ) - this.connection.console.info(`* Built ${suggestions.length} suggestions for auto complete`) - this.searchIndex = jsSearchIndex(suggestions) + this.connection.console.info( + `* Built ${this.#suggestions.length} suggestions for auto complete` + ) + this.searchIndex = jsSearchIndex(this.#suggestions) } catch (err) { this.connection.console.error(err.stack) this.connection.console.error( From fd7d135e53136a1fb41a62802d393bcba02c4ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Thu, 1 Sep 2022 18:58:21 +0100 Subject: [PATCH 04/10] Hide more fs access behind the Files interface --- src/CucumberLanguageServer.ts | 19 ++++++------------- src/Files.ts | 6 ++++++ src/fs.ts | 14 +++++++++----- src/newWasmServer.ts | 5 +++-- src/node/NodeFiles.ts | 20 ++++++++++++++++++++ src/node/startNodeServer.ts | 3 ++- src/startServer.ts | 5 +++-- src/wasm/startWasmServer.ts | 5 +++-- test/CucumberLanguageServer.test.ts | 4 +++- 9 files changed, 55 insertions(+), 26 deletions(-) create mode 100644 src/Files.ts create mode 100644 src/node/NodeFiles.ts diff --git a/src/CucumberLanguageServer.ts b/src/CucumberLanguageServer.ts index 161cf011..6b3cbd27 100644 --- a/src/CucumberLanguageServer.ts +++ b/src/CucumberLanguageServer.ts @@ -14,9 +14,7 @@ import { semanticTokenTypes, Suggestion, } from '@cucumber/language-service' -import { stat as statCb } from 'fs' import { extname, relative } from 'path' -import { promisify } from 'util' import { CodeAction, CodeActionKind, @@ -30,13 +28,12 @@ import { import { TextDocument } from 'vscode-languageserver-textdocument' import { buildStepTexts } from './buildStepTexts.js' +import { Files } from './Files' import { getLanguage, loadGherkinSources, loadGlueSources } from './fs.js' import { getStepDefinitionSnippetLinks } from './getStepDefinitionSnippetLinks.js' import { Settings } from './types.js' import { version } from './version.js' -const stat = promisify(statCb) - type ServerInfo = { name: string version: string @@ -93,7 +90,8 @@ export class CucumberLanguageServer { constructor( private readonly connection: Connection, private readonly documents: TextDocuments, - parserAdapter: ParserAdapter + parserAdapter: ParserAdapter, + private readonly files: Files ) { this.expressionBuilder = new ExpressionBuilder(parserAdapter) @@ -220,12 +218,7 @@ export class CucumberLanguageServer { return [] } const mustacheTemplate = settings.snippetTemplates[languageName] - let createFile = false - try { - await stat(new URL(link.targetUri)) - } catch { - createFile = true - } + const createFile = !(await files.exists(link.targetUri)) const relativePath = relative(this.rootPath, new URL(link.targetUri).pathname) const codeAction = getGenerateSnippetCodeAction( diagnostics, @@ -387,7 +380,7 @@ export class CucumberLanguageServer { // https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workDoneProgress this.connection.console.info(`Reindexing ${this.rootPath}`) - const gherkinSources = await loadGherkinSources(this.rootPath, settings.features) + const gherkinSources = await loadGherkinSources(this.files, this.rootPath, settings.features) this.connection.console.info( `* Found ${gherkinSources.length} feature file(s) in ${JSON.stringify(settings.features)}` ) @@ -396,7 +389,7 @@ export class CucumberLanguageServer { [] ) this.connection.console.info(`* Found ${stepTexts.length} steps in those feature files`) - const glueSources = await loadGlueSources(this.rootPath, settings.glue) + const glueSources = await loadGlueSources(this.files, this.rootPath, settings.glue) this.connection.console.info( `* Found ${glueSources.length} glue file(s) in ${JSON.stringify(settings.glue)}` ) diff --git a/src/Files.ts b/src/Files.ts new file mode 100644 index 00000000..dc0c62d2 --- /dev/null +++ b/src/Files.ts @@ -0,0 +1,6 @@ +import { DocumentUri } from 'vscode-languageserver-types' + +export interface Files { + exists(uri: DocumentUri): Promise + readFile(path: string): Promise +} diff --git a/src/fs.ts b/src/fs.ts index 74bd08a4..d47f9d5f 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -1,10 +1,11 @@ import { LanguageName, Source } from '@cucumber/language-service' import fg from 'fast-glob' -import fs from 'fs/promises' import { extname, join, resolve as resolvePath } from 'path' import url from 'url' -export const glueExtByLanguageName: Record = { +import { Files } from './Files' + +const glueExtByLanguageName: Record = { typescript: '.ts', java: '.java', c_sharp: '.cs', @@ -19,10 +20,11 @@ const glueLanguageNameByExt = Object.fromEntries( const glueExtensions = new Set(Object.keys(glueLanguageNameByExt)) export async function loadGlueSources( + files: Files, cwd: string, globs: readonly string[] ): Promise[]> { - return loadSources(cwd, globs, glueExtensions, glueLanguageNameByExt) + return loadSources(files, cwd, globs, glueExtensions, glueLanguageNameByExt) } export function getLanguage(ext: string): LanguageName | undefined { @@ -30,10 +32,11 @@ export function getLanguage(ext: string): LanguageName | undefined { } export async function loadGherkinSources( + files: Files, cwd: string, globs: readonly string[] ): Promise[]> { - return loadSources(cwd, globs, new Set(['.feature']), { '.feature': 'gherkin' }) + return loadSources(files, cwd, globs, new Set(['.feature']), { '.feature': 'gherkin' }) } type LanguageNameByExt = Record @@ -48,6 +51,7 @@ export async function findPaths(cwd: string, globs: readonly string[]): Promise< } async function loadSources( + files: Files, cwd: string, globs: readonly string[], extensions: Set, @@ -62,7 +66,7 @@ async function loadSources( (path) => new Promise>((resolve) => { const languageName = languageNameByExt[extname(path)] - return fs.readFile(path, 'utf-8').then((content) => + return files.readFile(path).then((content) => resolve({ languageName, content, diff --git a/src/newWasmServer.ts b/src/newWasmServer.ts index 1b2d4fb9..58e3520f 100644 --- a/src/newWasmServer.ts +++ b/src/newWasmServer.ts @@ -6,20 +6,21 @@ import { createConnection } from 'vscode-languageserver/node' import { TextDocument } from 'vscode-languageserver-textdocument' import { CucumberLanguageServer } from './CucumberLanguageServer.js' +import { Files } from './Files' export type StreamInfo = { writer: NodeJS.WritableStream reader: NodeJS.ReadableStream } -export function newWasmServer(wasmBaseUrl: string) { +export function newWasmServer(wasmBaseUrl: string, files: Files) { const adapter: ParserAdapter = new WasmParserAdapter(wasmBaseUrl) const inputStream = new PassThrough() const outputStream = new PassThrough() const connection = createConnection(inputStream, outputStream) const documents = new TextDocuments(TextDocument) - new CucumberLanguageServer(connection, documents, adapter) + new CucumberLanguageServer(connection, documents, adapter, files) connection.listen() return { diff --git a/src/node/NodeFiles.ts b/src/node/NodeFiles.ts new file mode 100644 index 00000000..f1a352d3 --- /dev/null +++ b/src/node/NodeFiles.ts @@ -0,0 +1,20 @@ +import fs from 'fs/promises' +import { DocumentUri } from 'vscode-languageserver-types' + +import { Files } from '../Files' + +export class NodeFiles implements Files { + async exists(uri: DocumentUri): Promise { + try { + await fs.stat(new URL(uri)) + } catch { + return false + } + + return Promise.resolve(false) + } + + readFile(path: string): Promise { + return fs.readFile(path, 'utf-8') + } +} diff --git a/src/node/startNodeServer.ts b/src/node/startNodeServer.ts index 9b2dce44..8145f0cd 100644 --- a/src/node/startNodeServer.ts +++ b/src/node/startNodeServer.ts @@ -1,7 +1,8 @@ import { NodeParserAdapter } from '@cucumber/language-service/node' import { startServer } from '../startServer.js' +import { NodeFiles } from './NodeFiles' export function startNodeServer() { - startServer(new NodeParserAdapter()) + startServer(new NodeParserAdapter(), new NodeFiles()) } diff --git a/src/startServer.ts b/src/startServer.ts index 4b4d4e15..cfdb2e3a 100644 --- a/src/startServer.ts +++ b/src/startServer.ts @@ -4,12 +4,13 @@ import { createConnection, ProposedFeatures } from 'vscode-languageserver/node' import { TextDocument } from 'vscode-languageserver-textdocument' import { CucumberLanguageServer } from './CucumberLanguageServer.js' +import { Files } from './Files' import { version } from './version.js' -export function startServer(adapter: ParserAdapter) { +export function startServer(adapter: ParserAdapter, files: Files) { const connection = createConnection(ProposedFeatures.all) const documents = new TextDocuments(TextDocument) - new CucumberLanguageServer(connection, documents, adapter) + new CucumberLanguageServer(connection, documents, adapter, files) connection.listen() // Don't die on unhandled Promise rejections diff --git a/src/wasm/startWasmServer.ts b/src/wasm/startWasmServer.ts index cf757833..2c6b9365 100644 --- a/src/wasm/startWasmServer.ts +++ b/src/wasm/startWasmServer.ts @@ -1,7 +1,8 @@ import { WasmParserAdapter } from '@cucumber/language-service/wasm' +import { Files } from '../Files' import { startServer } from '../startServer.js' -export function startWasmServer(wasmBaseUrl: string) { - startServer(new WasmParserAdapter(wasmBaseUrl)) +export function startWasmServer(wasmBaseUrl: string, files: Files) { + startServer(new WasmParserAdapter(wasmBaseUrl), files) } diff --git a/test/CucumberLanguageServer.test.ts b/test/CucumberLanguageServer.test.ts index ebf8c448..106766c0 100644 --- a/test/CucumberLanguageServer.test.ts +++ b/test/CucumberLanguageServer.test.ts @@ -23,6 +23,7 @@ import { TextDocument } from 'vscode-languageserver-textdocument' import { CompletionItem, CompletionItemKind } from 'vscode-languageserver-types' import { CucumberLanguageServer } from '../src/CucumberLanguageServer.js' +import { NodeFiles } from '../src/node/NodeFiles' import { Settings } from '../src/types' describe('CucumberLanguageServer', () => { @@ -41,7 +42,8 @@ describe('CucumberLanguageServer', () => { new CucumberLanguageServer( serverConnection, documents, - new WasmParserAdapter('node_modules/@cucumber/language-service/dist') + new WasmParserAdapter('node_modules/@cucumber/language-service/dist'), + new NodeFiles() ) serverConnection.listen() From b9e9ff13ea6800b838e1f24868cb142213d9d400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Thu, 1 Sep 2022 19:10:29 +0100 Subject: [PATCH 05/10] Extract Files.findFiles() --- src/Files.ts | 1 + src/fs.ts | 14 +++++++++----- src/node/NodeFiles.ts | 5 +++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Files.ts b/src/Files.ts index dc0c62d2..8f5973c9 100644 --- a/src/Files.ts +++ b/src/Files.ts @@ -3,4 +3,5 @@ import { DocumentUri } from 'vscode-languageserver-types' export interface Files { exists(uri: DocumentUri): Promise readFile(path: string): Promise + findFiles(cwd: string, glob: string): Promise } diff --git a/src/fs.ts b/src/fs.ts index d47f9d5f..f53ec835 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -1,5 +1,4 @@ import { LanguageName, Source } from '@cucumber/language-service' -import fg from 'fast-glob' import { extname, join, resolve as resolvePath } from 'path' import url from 'url' @@ -41,9 +40,14 @@ export async function loadGherkinSources( type LanguageNameByExt = Record -export async function findPaths(cwd: string, globs: readonly string[]): Promise { - const pathPromises = globs.reduce[]>((prev, glob) => { - return prev.concat(fg(glob, { caseSensitiveMatch: false, onlyFiles: true, cwd })) +export async function findPaths( + files: Files, + cwd: string, + globs: readonly string[] +): Promise { + const pathPromises = globs.reduce[]>((prev, glob) => { + const pathsPromise = files.findFiles(cwd, glob) + return prev.concat(pathsPromise) }, []) const pathArrays = await Promise.all(pathPromises) const paths = pathArrays.flatMap((paths) => paths) @@ -57,7 +61,7 @@ async function loadSources( extensions: Set, languageNameByExt: LanguageNameByExt ): Promise[]> { - const paths = await findPaths(cwd, globs) + const paths = await findPaths(files, cwd, globs) return Promise.all( paths diff --git a/src/node/NodeFiles.ts b/src/node/NodeFiles.ts index f1a352d3..7eb4db27 100644 --- a/src/node/NodeFiles.ts +++ b/src/node/NodeFiles.ts @@ -1,3 +1,4 @@ +import fg from 'fast-glob' import fs from 'fs/promises' import { DocumentUri } from 'vscode-languageserver-types' @@ -17,4 +18,8 @@ export class NodeFiles implements Files { readFile(path: string): Promise { return fs.readFile(path, 'utf-8') } + + findFiles(cwd: string, glob: string): Promise { + return fg(glob, { cwd, caseSensitiveMatch: false, onlyFiles: true }) + } } From 7dbaaf2ec8216aa70017f301fcf48e857f27bc00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Thu, 1 Sep 2022 19:17:44 +0100 Subject: [PATCH 06/10] Implement extname --- src/Files.ts | 5 +++++ src/fs.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Files.ts b/src/Files.ts index 8f5973c9..6c2e321a 100644 --- a/src/Files.ts +++ b/src/Files.ts @@ -5,3 +5,8 @@ export interface Files { readFile(path: string): Promise findFiles(cwd: string, glob: string): Promise } + +export function extname(path: string): string { + // Roughly-enough implements https://nodejs.org/dist/latest-v18.x/docs/api/path.html#pathextnamepath + return path.substring(path.lastIndexOf('.'), path.length) || '' +} diff --git a/src/fs.ts b/src/fs.ts index f53ec835..5f6175e6 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -1,8 +1,8 @@ import { LanguageName, Source } from '@cucumber/language-service' -import { extname, join, resolve as resolvePath } from 'path' +import { join, resolve as resolvePath } from 'path' import url from 'url' -import { Files } from './Files' +import { extname, Files } from './Files' const glueExtByLanguageName: Record = { typescript: '.ts', From 4e2958b0d0b647d301ac16541f9b3eb367b4e089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Thu, 1 Sep 2022 19:20:39 +0100 Subject: [PATCH 07/10] Implement Files.join() --- src/Files.ts | 1 + src/fs.ts | 4 ++-- src/node/NodeFiles.ts | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Files.ts b/src/Files.ts index 6c2e321a..b7d79d3b 100644 --- a/src/Files.ts +++ b/src/Files.ts @@ -4,6 +4,7 @@ export interface Files { exists(uri: DocumentUri): Promise readFile(path: string): Promise findFiles(cwd: string, glob: string): Promise + join(...paths: string[]): string } export function extname(path: string): string { diff --git a/src/fs.ts b/src/fs.ts index 5f6175e6..925edf9c 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -1,5 +1,5 @@ import { LanguageName, Source } from '@cucumber/language-service' -import { join, resolve as resolvePath } from 'path' +import { resolve as resolvePath } from 'path' import url from 'url' import { extname, Files } from './Files' @@ -51,7 +51,7 @@ export async function findPaths( }, []) const pathArrays = await Promise.all(pathPromises) const paths = pathArrays.flatMap((paths) => paths) - return [...new Set(paths).values()].sort().map((path) => join(cwd, path)) + return [...new Set(paths).values()].sort().map((path) => files.join(cwd, path)) } async function loadSources( diff --git a/src/node/NodeFiles.ts b/src/node/NodeFiles.ts index 7eb4db27..78b727bf 100644 --- a/src/node/NodeFiles.ts +++ b/src/node/NodeFiles.ts @@ -1,5 +1,6 @@ import fg from 'fast-glob' import fs from 'fs/promises' +import path from 'path' import { DocumentUri } from 'vscode-languageserver-types' import { Files } from '../Files' @@ -22,4 +23,8 @@ export class NodeFiles implements Files { findFiles(cwd: string, glob: string): Promise { return fg(glob, { cwd, caseSensitiveMatch: false, onlyFiles: true }) } + + join(...paths: string[]): string { + return path.join(...paths) + } } From c254ce1b5610b47c77cb86efe4ff8d061b0c8681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Thu, 1 Sep 2022 22:13:34 +0100 Subject: [PATCH 08/10] Extract toUri --- src/Files.ts | 1 + src/fs.ts | 4 +--- src/node/NodeFiles.ts | 10 +++++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Files.ts b/src/Files.ts index b7d79d3b..3c1ae60b 100644 --- a/src/Files.ts +++ b/src/Files.ts @@ -5,6 +5,7 @@ export interface Files { readFile(path: string): Promise findFiles(cwd: string, glob: string): Promise join(...paths: string[]): string + toUri(path: string): string } export function extname(path: string): string { diff --git a/src/fs.ts b/src/fs.ts index 925edf9c..c024d9e6 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -1,6 +1,4 @@ import { LanguageName, Source } from '@cucumber/language-service' -import { resolve as resolvePath } from 'path' -import url from 'url' import { extname, Files } from './Files' @@ -74,7 +72,7 @@ async function loadSources( resolve({ languageName, content, - uri: url.pathToFileURL(resolvePath(path)).href, + uri: files.toUri(path), }) ) }) diff --git a/src/node/NodeFiles.ts b/src/node/NodeFiles.ts index 78b727bf..a2c382b7 100644 --- a/src/node/NodeFiles.ts +++ b/src/node/NodeFiles.ts @@ -1,6 +1,7 @@ import fg from 'fast-glob' import fs from 'fs/promises' -import path from 'path' +import path, { resolve as resolvePath } from 'path' +import url from 'url' import { DocumentUri } from 'vscode-languageserver-types' import { Files } from '../Files' @@ -9,11 +10,10 @@ export class NodeFiles implements Files { async exists(uri: DocumentUri): Promise { try { await fs.stat(new URL(uri)) + return true } catch { return false } - - return Promise.resolve(false) } readFile(path: string): Promise { @@ -27,4 +27,8 @@ export class NodeFiles implements Files { join(...paths: string[]): string { return path.join(...paths) } + + toUri(path: string): string { + return url.pathToFileURL(resolvePath(path)).href + } } From 7969e34a6d8bab82bc18f374ed2334b77e730136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Thu, 8 Sep 2022 14:29:57 +0100 Subject: [PATCH 09/10] Refactor --- src/CucumberLanguageServer.ts | 15 ++++++++------- src/Files.ts | 7 +++---- src/fs.ts | 14 +++++--------- src/index.ts | 1 + src/newWasmServer.ts | 4 ++-- src/node/NodeFiles.ts | 17 +++++++++++------ src/node/startNodeServer.ts | 2 +- src/startServer.ts | 4 ++-- src/wasm/startWasmServer.ts | 4 ++-- test/CucumberLanguageServer.test.ts | 2 +- 10 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/CucumberLanguageServer.ts b/src/CucumberLanguageServer.ts index 6b3cbd27..e27a5caa 100644 --- a/src/CucumberLanguageServer.ts +++ b/src/CucumberLanguageServer.ts @@ -14,7 +14,6 @@ import { semanticTokenTypes, Suggestion, } from '@cucumber/language-service' -import { extname, relative } from 'path' import { CodeAction, CodeActionKind, @@ -28,7 +27,7 @@ import { import { TextDocument } from 'vscode-languageserver-textdocument' import { buildStepTexts } from './buildStepTexts.js' -import { Files } from './Files' +import { extname, Files } from './Files' import { getLanguage, loadGherkinSources, loadGlueSources } from './fs.js' import { getStepDefinitionSnippetLinks } from './getStepDefinitionSnippetLinks.js' import { Settings } from './types.js' @@ -82,6 +81,7 @@ export class CucumberLanguageServer { private reindexingTimeout: NodeJS.Timeout private rootPath: string #suggestions: readonly Suggestion[] + #files: Files get suggestions() { return this.#suggestions @@ -91,7 +91,7 @@ export class CucumberLanguageServer { private readonly connection: Connection, private readonly documents: TextDocuments, parserAdapter: ParserAdapter, - private readonly files: Files + private readonly makeFiles: (rootUri: string) => Files ) { this.expressionBuilder = new ExpressionBuilder(parserAdapter) @@ -115,6 +115,7 @@ export class CucumberLanguageServer { } else { connection.console.error(`Could not determine rootPath`) } + this.#files = makeFiles(this.rootPath) // Some users have reported that the globs don't find any files. This is to debug that issue connection.console.info(`Root path : ${this.rootPath}`) connection.console.info(`Current dir : ${process.cwd()}`) @@ -218,8 +219,8 @@ export class CucumberLanguageServer { return [] } const mustacheTemplate = settings.snippetTemplates[languageName] - const createFile = !(await files.exists(link.targetUri)) - const relativePath = relative(this.rootPath, new URL(link.targetUri).pathname) + const createFile = !(await this.#files.exists(link.targetUri)) + const relativePath = this.#files.relative(link.targetUri) const codeAction = getGenerateSnippetCodeAction( diagnostics, link, @@ -380,7 +381,7 @@ export class CucumberLanguageServer { // https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workDoneProgress this.connection.console.info(`Reindexing ${this.rootPath}`) - const gherkinSources = await loadGherkinSources(this.files, this.rootPath, settings.features) + const gherkinSources = await loadGherkinSources(this.#files, settings.features) this.connection.console.info( `* Found ${gherkinSources.length} feature file(s) in ${JSON.stringify(settings.features)}` ) @@ -389,7 +390,7 @@ export class CucumberLanguageServer { [] ) this.connection.console.info(`* Found ${stepTexts.length} steps in those feature files`) - const glueSources = await loadGlueSources(this.files, this.rootPath, settings.glue) + const glueSources = await loadGlueSources(this.#files, settings.glue) this.connection.console.info( `* Found ${glueSources.length} glue file(s) in ${JSON.stringify(settings.glue)}` ) diff --git a/src/Files.ts b/src/Files.ts index 3c1ae60b..cb22384e 100644 --- a/src/Files.ts +++ b/src/Files.ts @@ -1,10 +1,9 @@ -import { DocumentUri } from 'vscode-languageserver-types' - export interface Files { - exists(uri: DocumentUri): Promise + exists(uri: string): Promise readFile(path: string): Promise - findFiles(cwd: string, glob: string): Promise + findFiles(glob: string): Promise join(...paths: string[]): string + relative(uri: string): string toUri(path: string): string } diff --git a/src/fs.ts b/src/fs.ts index c024d9e6..2188d6b3 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -18,10 +18,9 @@ const glueExtensions = new Set(Object.keys(glueLanguageNameByExt)) export async function loadGlueSources( files: Files, - cwd: string, globs: readonly string[] ): Promise[]> { - return loadSources(files, cwd, globs, glueExtensions, glueLanguageNameByExt) + return loadSources(files, globs, glueExtensions, glueLanguageNameByExt) } export function getLanguage(ext: string): LanguageName | undefined { @@ -30,36 +29,33 @@ export function getLanguage(ext: string): LanguageName | undefined { export async function loadGherkinSources( files: Files, - cwd: string, globs: readonly string[] ): Promise[]> { - return loadSources(files, cwd, globs, new Set(['.feature']), { '.feature': 'gherkin' }) + return loadSources(files, globs, new Set(['.feature']), { '.feature': 'gherkin' }) } type LanguageNameByExt = Record export async function findPaths( files: Files, - cwd: string, globs: readonly string[] ): Promise { const pathPromises = globs.reduce[]>((prev, glob) => { - const pathsPromise = files.findFiles(cwd, glob) + const pathsPromise = files.findFiles(glob) return prev.concat(pathsPromise) }, []) const pathArrays = await Promise.all(pathPromises) const paths = pathArrays.flatMap((paths) => paths) - return [...new Set(paths).values()].sort().map((path) => files.join(cwd, path)) + return [...new Set(paths).values()].sort().map((path) => files.join(path)) } async function loadSources( files: Files, - cwd: string, globs: readonly string[], extensions: Set, languageNameByExt: LanguageNameByExt ): Promise[]> { - const paths = await findPaths(files, cwd, globs) + const paths = await findPaths(files, globs) return Promise.all( paths diff --git a/src/index.ts b/src/index.ts index 9fe236b5..7a204f42 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ +export * from './Files.js' export * from './newWasmServer.js' export * from './startServer.js' diff --git a/src/newWasmServer.ts b/src/newWasmServer.ts index 58e3520f..fd68bf2b 100644 --- a/src/newWasmServer.ts +++ b/src/newWasmServer.ts @@ -13,14 +13,14 @@ export type StreamInfo = { reader: NodeJS.ReadableStream } -export function newWasmServer(wasmBaseUrl: string, files: Files) { +export function newWasmServer(wasmBaseUrl: string, makeFiles: (rootUri: string) => Files) { const adapter: ParserAdapter = new WasmParserAdapter(wasmBaseUrl) const inputStream = new PassThrough() const outputStream = new PassThrough() const connection = createConnection(inputStream, outputStream) const documents = new TextDocuments(TextDocument) - new CucumberLanguageServer(connection, documents, adapter, files) + new CucumberLanguageServer(connection, documents, adapter, makeFiles) connection.listen() return { diff --git a/src/node/NodeFiles.ts b/src/node/NodeFiles.ts index a2c382b7..ef2e650c 100644 --- a/src/node/NodeFiles.ts +++ b/src/node/NodeFiles.ts @@ -1,13 +1,14 @@ import fg from 'fast-glob' import fs from 'fs/promises' -import path, { resolve as resolvePath } from 'path' +import path, { relative, resolve as resolvePath } from 'path' import url from 'url' -import { DocumentUri } from 'vscode-languageserver-types' import { Files } from '../Files' export class NodeFiles implements Files { - async exists(uri: DocumentUri): Promise { + constructor(private readonly rootUri: string) {} + + async exists(uri: string): Promise { try { await fs.stat(new URL(uri)) return true @@ -20,12 +21,16 @@ export class NodeFiles implements Files { return fs.readFile(path, 'utf-8') } - findFiles(cwd: string, glob: string): Promise { - return fg(glob, { cwd, caseSensitiveMatch: false, onlyFiles: true }) + findFiles(glob: string): Promise { + return fg(glob, { cwd: this.rootUri, caseSensitiveMatch: false, onlyFiles: true }) } join(...paths: string[]): string { - return path.join(...paths) + return path.join(this.rootUri, ...paths) + } + + relative(uri: string): string { + return relative(this.rootUri, new URL(uri).pathname) } toUri(path: string): string { diff --git a/src/node/startNodeServer.ts b/src/node/startNodeServer.ts index 8145f0cd..451b3474 100644 --- a/src/node/startNodeServer.ts +++ b/src/node/startNodeServer.ts @@ -4,5 +4,5 @@ import { startServer } from '../startServer.js' import { NodeFiles } from './NodeFiles' export function startNodeServer() { - startServer(new NodeParserAdapter(), new NodeFiles()) + startServer(new NodeParserAdapter(), (rootUri) => new NodeFiles(rootUri)) } diff --git a/src/startServer.ts b/src/startServer.ts index cfdb2e3a..cc2fe741 100644 --- a/src/startServer.ts +++ b/src/startServer.ts @@ -7,10 +7,10 @@ import { CucumberLanguageServer } from './CucumberLanguageServer.js' import { Files } from './Files' import { version } from './version.js' -export function startServer(adapter: ParserAdapter, files: Files) { +export function startServer(adapter: ParserAdapter, makeFiles: (rootUri: string) => Files) { const connection = createConnection(ProposedFeatures.all) const documents = new TextDocuments(TextDocument) - new CucumberLanguageServer(connection, documents, adapter, files) + new CucumberLanguageServer(connection, documents, adapter, makeFiles) connection.listen() // Don't die on unhandled Promise rejections diff --git a/src/wasm/startWasmServer.ts b/src/wasm/startWasmServer.ts index 2c6b9365..d540d525 100644 --- a/src/wasm/startWasmServer.ts +++ b/src/wasm/startWasmServer.ts @@ -3,6 +3,6 @@ import { WasmParserAdapter } from '@cucumber/language-service/wasm' import { Files } from '../Files' import { startServer } from '../startServer.js' -export function startWasmServer(wasmBaseUrl: string, files: Files) { - startServer(new WasmParserAdapter(wasmBaseUrl), files) +export function startWasmServer(wasmBaseUrl: string, makeFiles: (rootUri: string) => Files) { + startServer(new WasmParserAdapter(wasmBaseUrl), makeFiles) } diff --git a/test/CucumberLanguageServer.test.ts b/test/CucumberLanguageServer.test.ts index 106766c0..597e9d95 100644 --- a/test/CucumberLanguageServer.test.ts +++ b/test/CucumberLanguageServer.test.ts @@ -43,7 +43,7 @@ describe('CucumberLanguageServer', () => { serverConnection, documents, new WasmParserAdapter('node_modules/@cucumber/language-service/dist'), - new NodeFiles() + (rootUri) => new NodeFiles(rootUri) ) serverConnection.listen() From b50a1346283d2c6480a9d01fa196b3d70dd81855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aslak=20Helles=C3=B8y?= Date: Mon, 12 Sep 2022 22:47:50 +0100 Subject: [PATCH 10/10] Export more from node module --- node/package.json | 6 +++--- package.json | 8 ++++---- src/CucumberLanguageServer.ts | 2 +- src/fs.ts | 8 +++++--- src/node/index.ts | 2 ++ src/wasm/index.ts | 1 + wasm/package.json | 6 +++--- 7 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 src/node/index.ts create mode 100644 src/wasm/index.ts diff --git a/node/package.json b/node/package.json index 6371344c..132eecad 100644 --- a/node/package.json +++ b/node/package.json @@ -1,7 +1,7 @@ { "name": "wasm", "description": "Cucumber Language server using tree-sitter node bindings", - "main": "../dist/cjs/src/node/startNodeServer.js", - "module": "../dist/esm/src/node/startNodeServer.js", - "types": "../dist/esm/src/node/startNodeServer.d.ts" + "main": "../dist/cjs/src/node/index.js", + "module": "../dist/esm/src/node/index.js", + "types": "../dist/esm/src/node/index.d.ts" } diff --git a/package.json b/package.json index 2b882969..8075e14b 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,12 @@ "require": "./dist/cjs/src/index.js" }, "./node": { - "import": "./dist/esm/src/node/startNodeServer.js", - "require": "./dist/cjs/src/node/startNodeServer.js" + "import": "./dist/esm/src/node/index.js", + "require": "./dist/cjs/src/node/index.js" }, "./wasm": { - "import": "./dist/esm/src/wasm/startWasmServer.js", - "require": "./dist/cjs/src/wasm/startWasmServer.js" + "import": "./dist/esm/src/wasm/index.js", + "require": "./dist/cjs/src/wasm/index.js" } }, "files": [ diff --git a/src/CucumberLanguageServer.ts b/src/CucumberLanguageServer.ts index cadac237..6f527175 100644 --- a/src/CucumberLanguageServer.ts +++ b/src/CucumberLanguageServer.ts @@ -97,7 +97,7 @@ export class CucumberLanguageServer { this.expressionBuilder = new ExpressionBuilder(parserAdapter) connection.onInitialize(async (params) => { - connection.console.log(`PARAMS: ${JSON.stringify(params, null, 2)}`) + // connection.console.log(`PARAMS: ${JSON.stringify(params, null, 2)}`) await parserAdapter.init() if (params.clientInfo) { connection.console.info( diff --git a/src/fs.ts b/src/fs.ts index 8dc28bfb..f3be24a9 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -46,13 +46,15 @@ export async function findPaths( files: Files, globs: readonly string[] ): Promise { - const pathPromises = globs.reduce[]>((prev, glob) => { + // Run all the globs in parallel + const pathsPromises = globs.reduce[]>((prev, glob) => { const pathsPromise = files.findFiles(glob) return prev.concat(pathsPromise) }, []) - const pathArrays = await Promise.all(pathPromises) + const pathArrays = await Promise.all(pathsPromises) + // Flatten them all const paths = pathArrays.flatMap((paths) => paths) - return [...new Set(paths).values()].sort().map((path) => files.join(path)) + return [...new Set(paths).values()].sort() } async function loadSources( diff --git a/src/node/index.ts b/src/node/index.ts new file mode 100644 index 00000000..9eab3403 --- /dev/null +++ b/src/node/index.ts @@ -0,0 +1,2 @@ +export * from './NodeFiles.js' +export * from './startNodeServer.js' diff --git a/src/wasm/index.ts b/src/wasm/index.ts new file mode 100644 index 00000000..ce2a0b39 --- /dev/null +++ b/src/wasm/index.ts @@ -0,0 +1 @@ +export * from './startWasmServer.js' diff --git a/wasm/package.json b/wasm/package.json index d81c77ce..248c3920 100644 --- a/wasm/package.json +++ b/wasm/package.json @@ -1,7 +1,7 @@ { "name": "wasm", "description": "Cucumber Language server using tree-sitter wasm bindings", - "main": "../dist/cjs/src/wasm/startWasmServer.js", - "module": "../dist/esm/src/wasm/startWasmServer.js", - "types": "../dist/esm/src/wasm/startWasmServer.d.ts" + "main": "../dist/cjs/src/wasm/index.js", + "module": "../dist/esm/src/wasm/index.js", + "types": "../dist/esm/src/wasm/index.d.ts" }