diff --git a/packages/lsp-client/CHANGELOG.md b/packages/lsp-client/CHANGELOG.md index d8de2fd..c81ea99 100644 --- a/packages/lsp-client/CHANGELOG.md +++ b/packages/lsp-client/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v0.0.62 + +- chore: bump lsp-client version to 0.0.62 ([fefe775](https://github.com/fengkx/beancount-lsp/commit/fefe775)) +- fix: correct configuration property name in DocumentStore ([ef05566](https://github.com/fengkx/beancount-lsp/commit/ef05566)) +- chore: update dependencies in package.json files ([ea57ffe](https://github.com/fengkx/beancount-lsp/commit/ea57ffe)) + ## v0.0.60 - chore: bump lsp-client version to 0.0.60 ([7fb03fe](https://github.com/fengkx/beancount-lsp/commit/7fb03fe)) diff --git a/packages/lsp-client/README.md b/packages/lsp-client/README.md index 8d4c7e1..c0a6b17 100644 --- a/packages/lsp-client/README.md +++ b/packages/lsp-client/README.md @@ -47,7 +47,7 @@ The extension provides several configuration options to customize its behavior: - `messages`: Shows info-level messages, warnings, and errors (default) - `debug`: Shows debug-level messages and below - `verbose`: Shows all log messages (most verbose) -- `beanLsp.manBeanFile`: Specifies the main Beancount file to use for analysis. This should be relative to the workspace root. Default is "main.bean". +- `beanLsp.mainBeanFile`: Specifies the main Beancount file to use for analysis. This should be relative to the workspace root. Default is "main.bean". - `beancount.diagnostics.tolerance`: Tolerance value for transaction balancing. Set to 0 for exact matching. Default is 0.005. - `beancount.diagnostics.warnOnIncompleteTransaction`: Show warnings for incomplete transactions (marked with '!' flag). These are transactions that are considered unconfirmed in Beancount. Default is true. - `beanLsp.inlayHints.enable`: Enable or disable inlay hints showing calculated amounts for auto-balanced transactions. Default is true. @@ -60,7 +60,7 @@ The extension provides several configuration options to customize its behavior: ```json { - "beanLsp.manBeanFile": "main.bean", + "beanLsp.mainBeanFile": "main.bean", "beanLsp.trace.server": "debug", "beancount.diagnostics.tolerance": 0.005, "beancount.diagnostics.warnOnIncompleteTransaction": true, diff --git a/packages/lsp-client/package.json b/packages/lsp-client/package.json index 42cba2c..17cf1b8 100644 --- a/packages/lsp-client/package.json +++ b/packages/lsp-client/package.json @@ -3,7 +3,7 @@ "displayName": "Beancount language service", "description": "An Beancount language service supporting features like syntax highlighting, code completion, and more.", "icon": "assets/icon.png", - "version": "0.0.60", + "version": "0.0.62", "publisher": "fengkx", "private": true, "repository": { @@ -75,7 +75,7 @@ "default": "messages", "description": "Controls the verbosity of logging and traces the communication between VS Code and the language server. Values from least to most verbose: off (errors only), error, warn, messages (info), debug, verbose (trace)." }, - "beanLsp.manBeanFile": { + "beanLsp.mainBeanFile": { "scope": "resource", "type": "string", "default": "main.bean", diff --git a/packages/lsp-client/src/browser/extension.ts b/packages/lsp-client/src/browser/extension.ts index 490a388..3f96807 100644 --- a/packages/lsp-client/src/browser/extension.ts +++ b/packages/lsp-client/src/browser/extension.ts @@ -32,11 +32,6 @@ export function activate(context: vscode.ExtensionContext): void { statusBarItem.show(); context.subscriptions.push(statusBarItem); - // Show notification that this is experimental - vscode.window.showInformationMessage( - 'Beancount LSP browser extension is experimental and might have limited functionality compared to the desktop version.', - ); - try { // Resolve the web-tree-sitter.wasm path const webTreeSitterWasmPath = resolveWebTreeSitterWasmPath(context); diff --git a/packages/lsp-client/src/common/client.ts b/packages/lsp-client/src/common/client.ts index cbf7b5f..6f1f95a 100644 --- a/packages/lsp-client/src/common/client.ts +++ b/packages/lsp-client/src/common/client.ts @@ -1,7 +1,8 @@ import { CustomMessages, Logger, logLevelToString, mapTraceServerToLogLevel } from '@bean-lsp/shared'; import * as vscode from 'vscode'; // Import from base package for shared types between node and browser -import { LanguageClientOptions, State } from 'vscode-languageclient'; +import type { InitializeParams, LanguageClientOptions, ProtocolConnection } from 'vscode-languageclient'; +import { State } from 'vscode-languageclient'; import { Utils as UriUtils } from 'vscode-uri'; import { tools } from './llm/tools'; import { ClientOptions, ExtensionContext } from './types'; @@ -223,6 +224,17 @@ export function setupStatusBar(ctx: ExtensionContext<'browser' | 'node'>): void * Sets up custom message handlers for the client */ export function setupCustomMessageHandlers(ctx: ExtensionContext<'browser' | 'node'>): void { + const originalInitialize = ctx.client['doInitialize'].bind(ctx.client); + ctx.client['doInitialize'] = async (connection: ProtocolConnection, params: InitializeParams) => { + console.log('===== doInitialize', params); + // @ts-expect-error customMessage is not part of the protocol + params.capabilities['customMessage'] = { + [CustomMessages.ListBeanFile]: true, + [CustomMessages.FileRead]: true, + }; + return originalInitialize(connection, params); + }; + ctx.client.onRequest(CustomMessages.ListBeanFile, async () => { const exclude = await generateExcludePattern(); const files = await vscode.workspace.findFiles('**/*.{bean,beancount}', exclude); diff --git a/packages/lsp-server/src/browser/server.ts b/packages/lsp-server/src/browser/server.ts index d995a36..968fee2 100644 --- a/packages/lsp-server/src/browser/server.ts +++ b/packages/lsp-server/src/browser/server.ts @@ -5,6 +5,7 @@ import { ProposedFeatures, } from 'vscode-languageserver/browser'; +import { DocumentStore } from 'src/common/document-store'; import { ServerOptions, startServer } from '../common/startServer'; import { factory } from './storage'; @@ -16,10 +17,13 @@ const messageWriter = new BrowserMessageWriter(self); const connection = createConnection(ProposedFeatures.all, messageReader, messageWriter); // Server options - will be populated by the initialization options in startServer -const serverOptions: ServerOptions = {}; +const serverOptions: ServerOptions = { + isBrowser: true, +}; +const documents = new DocumentStore(connection); // Start the server with the options -startServer(connection, factory, undefined, serverOptions); +startServer(connection, factory, documents, undefined, serverOptions); // Listen on the connection connection.listen(); diff --git a/packages/lsp-server/src/common/document-store.ts b/packages/lsp-server/src/common/document-store.ts index fc2fda8..0e46137 100644 --- a/packages/lsp-server/src/common/document-store.ts +++ b/packages/lsp-server/src/common/document-store.ts @@ -4,9 +4,11 @@ import { Connection, Emitter, Event, + InitializeParams, Range, TextDocumentContentChangeEvent, TextDocuments, + WorkspaceFolder, } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { URI, Utils as UriUtils } from 'vscode-uri'; @@ -29,10 +31,13 @@ export class DocumentStore extends TextDocuments { readonly onDidChangeContent2: Event = this._onDidChangeContent2.event; private _beanFiles: string[] = []; + private _initializeParams: InitializeParams | undefined; private logger = new Logger('DocumentStore'); - constructor(private readonly _connection: Connection) { + constructor( + private readonly _connection: Connection, + ) { super({ create: TextDocument.create, update: (doc, changes, version) => { @@ -61,13 +66,34 @@ export class DocumentStore extends TextDocuments { }); this.listen(_connection); } + + public setInitializeParams(initializeParams: InitializeParams) { + this._initializeParams = initializeParams; + } + private readonly _documentsCache = new LRUMap(200); async refetchBeanFiles(): Promise { + // Check if client supports ListBeanFile capability + // @ts-expect-error customMessage is not part of the protocol + if (!this._initializeParams?.capabilities?.customMessage?.[CustomMessages.ListBeanFile]) { + if (!this._initializeParams?.workspaceFolders?.[0]) { + this._beanFiles = []; + return; + } + this._beanFiles = await this.fallbackListBeanFiles(this._initializeParams.workspaceFolders[0]); + return; + } + const files = await this._connection.sendRequest(CustomMessages.ListBeanFile); this._beanFiles = files; } + protected async fallbackListBeanFiles(_workspaceFolder: WorkspaceFolder): Promise { + this.logger.warn('Client does not support ListBeanFile capability'); + return this.all().map(doc => doc.uri); + } + get beanFiles(): string[] { return this._beanFiles; } @@ -92,11 +118,25 @@ export class DocumentStore extends TextDocuments { } private async _requestDocument(uri: string): Promise { - const reply = await this._connection.sendRequest(CustomMessages.FileRead, uri); + const reply = await this.fileRead(uri); const bytes = new Uint8Array(reply); return TextDocument.create(uri, LANGUAGE_ID, 1, this._decoder.decode(bytes)); } + private async fileRead(uri: string): Promise { + // Check if client supports FileRead capability + // @ts-expect-error customMessage is not part of the protocol + if (!this._initializeParams?.capabilities?.customMessage?.[CustomMessages.FileRead]) { + return this.fallbackFileRead(uri); + } + return this._connection.sendRequest(CustomMessages.FileRead, uri); + } + + protected async fallbackFileRead(_uri: string): Promise { + this.logger.warn('Client does not support FileRead capability'); + return new ArrayBuffer(0); + } + removeFile(uri: string): boolean { return this._documentsCache.delete(uri); } @@ -116,9 +156,9 @@ export class DocumentStore extends TextDocuments { return null; } - if (workspace && !config.manBeanFile) { + if (workspace && !config.mainBeanFile) { this._connection!.window.showWarningMessage( - `Using default 'main.bean' as manBeanFile, You should configure 'beanLsp.manBeanFile'`, + `Using default 'main.bean' as manBeanFile, You should configure 'beanLsp.mainBeanFile'`, ); } const rootUri = workspace[0]?.uri; @@ -127,7 +167,7 @@ export class DocumentStore extends TextDocuments { return null; } - const mainAbsPath = UriUtils.joinPath(URI.parse(rootUri), config.manBeanFile ?? 'main.bean'); + const mainAbsPath = UriUtils.joinPath(URI.parse(rootUri), config.mainBeanFile ?? 'main.bean'); return mainAbsPath.toString() as string; } diff --git a/packages/lsp-server/src/common/features/types.ts b/packages/lsp-server/src/common/features/types.ts index 9867b64..fc498a0 100644 --- a/packages/lsp-server/src/common/features/types.ts +++ b/packages/lsp-server/src/common/features/types.ts @@ -35,3 +35,8 @@ export interface RealBeancountManager { } export type BeancountManagerFactory = (connection: Connection, extensionUri: string) => RealBeancountManager; + +export interface PlatformMethods { + findBeanFiles: () => Promise; + readFile: (uri: string) => Promise; +} diff --git a/packages/lsp-server/src/common/startServer.ts b/packages/lsp-server/src/common/startServer.ts index 1c875a7..390810e 100644 --- a/packages/lsp-server/src/common/startServer.ts +++ b/packages/lsp-server/src/common/startServer.ts @@ -43,6 +43,8 @@ export interface IStorageFactory { export interface ServerOptions { webTreeSitterWasmPath?: string; logLevel?: LogLevel; + + isBrowser: boolean; } // Create a server logger @@ -51,8 +53,9 @@ const serverLogger = new Logger('Server'); export function startServer( connection: Connection, factory: IStorageFactory, + documents: DocumentStore, beanMgrFactory: BeancountManagerFactory | undefined, - options: ServerOptions = {}, + options: ServerOptions, ): void { // Set initial log level from options if (options.logLevel !== undefined) { @@ -72,11 +75,11 @@ export function startServer( const features: Feature[] = []; - let documents: DocumentStore; let symbolIndex: SymbolIndex; let beanMgr: RealBeancountManager | undefined; connection.onInitialize(async (params: InitializeParams): Promise => { + documents.setInitializeParams(params); const result: InitializeResult = { capabilities: { textDocumentSync: TextDocumentSyncKind.Incremental, @@ -130,7 +133,6 @@ export function startServer( await symbolStorage.autoloadPromise; connection.onExit(() => factory.destroy(symbolStorage)); - documents = new DocumentStore(connection); const trees = new Trees(documents, options.webTreeSitterWasmPath!); const optionsManager = BeancountOptionsManager.getInstance(); const historyContext = new HistoryContext(trees); @@ -204,6 +206,8 @@ export function startServer( const mainBeanFile = await documents.getMainBeanFileUri(); serverLogger.info(`mainBeanFile ${mainBeanFile}`); await documents.refetchBeanFiles(); + await symbolIndex.initFiles(documents.beanFiles); + await symbolIndex.unleashFiles([]); if (mainBeanFile) { await symbolIndex.initFiles([mainBeanFile]); diff --git a/packages/lsp-server/src/node/beancount-manager.ts b/packages/lsp-server/src/node/beancount-manager.ts index 8bd93e1..d3e4404 100644 --- a/packages/lsp-server/src/node/beancount-manager.ts +++ b/packages/lsp-server/src/node/beancount-manager.ts @@ -1,14 +1,14 @@ import { Logger } from '@bean-lsp/shared'; import { $, execa } from 'execa'; +import { Connection, DidSaveTextDocumentParams } from 'vscode-languageserver'; +import { URI } from 'vscode-uri'; import { Amount, BeancountError, BeancountFlag, BeancountManagerFactory, RealBeancountManager, -} from 'src/common/features/types'; -import { Connection, DidSaveTextDocumentParams } from 'vscode-languageserver'; -import { URI } from 'vscode-uri'; +} from '../common/features/types'; import { globalEventBus, GlobalEvents } from '../common/utils/event-bus'; interface AccountDetails { diff --git a/packages/lsp-server/src/node/server.ts b/packages/lsp-server/src/node/server.ts index f0c0c1e..c9bedc0 100644 --- a/packages/lsp-server/src/node/server.ts +++ b/packages/lsp-server/src/node/server.ts @@ -1,18 +1,42 @@ +import { glob } from 'fast-glob'; +import { readFile } from 'fs/promises'; +import { DocumentStore } from 'src/common/document-store'; +import { pathToFileURL } from 'url'; import { createConnection, ProposedFeatures } from 'vscode-languageserver/node'; - +import { URI } from 'vscode-uri'; import { ServerOptions, startServer } from '../common/startServer'; import { beananagerFactory } from './beancount-manager'; import { factory } from './storage'; +class DocumentStoreInNode extends DocumentStore { + protected override async fallbackListBeanFiles(workspaceFolder: { uri: string }): Promise { + const workspacePath = URI.parse(workspaceFolder.uri).fsPath; + const files = await glob('**/*.{bean,beancount}', { + cwd: workspacePath, + ignore: ['.venv/**', '**/*.log', '**/*.tmp', '**/*.tmp.*', '**/*.tmp.*.*', '**/*.pyc'], + absolute: true, + }); + return files.map((p) => pathToFileURL(p).toString()); + } + + protected override async fallbackFileRead(uri: string): Promise { + const buffer = await readFile(new URL(uri)); + return new Uint8Array(buffer); + } +} + // Create a connection for the server, using Node's IPC as a transport. // Also include all preview / proposed LSP features. const connection = createConnection(ProposedFeatures.all); // Server options - will be populated by the initialization options in startServer -const serverOptions: ServerOptions = {}; +const serverOptions: ServerOptions = { + isBrowser: false, +}; +const documents = new DocumentStoreInNode(connection); // Start the server with the options -startServer(connection, factory, beananagerFactory, serverOptions); +startServer(connection, factory, documents, beananagerFactory, serverOptions); // Listen on the connection connection.listen(); diff --git a/packages/lsp-server/tsup.config.ts b/packages/lsp-server/tsup.config.ts index 27a1d65..cadcf86 100644 --- a/packages/lsp-server/tsup.config.ts +++ b/packages/lsp-server/tsup.config.ts @@ -35,6 +35,7 @@ const nodeConfig = defineConfig({ noExternal: [ ...commonConfig.noExternal, 'execa', + 'fast-glob', ], });