Skip to content

Try support vim #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/lsp-client/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
4 changes: 2 additions & 2 deletions packages/lsp-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/lsp-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 0 additions & 5 deletions packages/lsp-client/src/browser/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import { resolveWebTreeSitterWasmPath } from '../common/utils';

// Explicitly declare types for the browser environment
declare const Worker: any;

Check warning on line 19 in packages/lsp-client/src/browser/extension.ts

View workflow job for this annotation

GitHub Actions / Build and Test

Unexpected any. Specify a different type

let client: LanguageClient;
let statusBarItem: vscode.StatusBarItem;
Expand All @@ -32,11 +32,6 @@
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);
Expand Down Expand Up @@ -107,7 +102,7 @@
/**
* Creates a language client that uses a web worker for the language server
*/
function createWorkerLanguageClient(context: vscode.ExtensionContext, clientOptions: any) {

Check warning on line 105 in packages/lsp-client/src/browser/extension.ts

View workflow job for this annotation

GitHub Actions / Build and Test

Unexpected any. Specify a different type
// Get path to the server's worker script
const workerPath = vscode.Uri.joinPath(context.extensionUri, 'server', 'browser.js');
clientLogger.info(`Worker path: ${workerPath.toString()}`);
Expand Down
14 changes: 13 additions & 1 deletion packages/lsp-client/src/common/client.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 6 additions & 2 deletions packages/lsp-server/src/browser/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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();
50 changes: 45 additions & 5 deletions packages/lsp-server/src/common/document-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,10 +31,13 @@ export class DocumentStore extends TextDocuments<TextDocument> {
readonly onDidChangeContent2: Event<TextDocumentChange2> = 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) => {
Expand Down Expand Up @@ -61,13 +66,34 @@ export class DocumentStore extends TextDocuments<TextDocument> {
});
this.listen(_connection);
}

public setInitializeParams(initializeParams: InitializeParams) {
this._initializeParams = initializeParams;
}

private readonly _documentsCache = new LRUMap<string, TextDocument>(200);

async refetchBeanFiles(): Promise<void> {
// 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<string[]>(CustomMessages.ListBeanFile);
this._beanFiles = files;
}

protected async fallbackListBeanFiles(_workspaceFolder: WorkspaceFolder): Promise<string[]> {
this.logger.warn('Client does not support ListBeanFile capability');
return this.all().map(doc => doc.uri);
}

get beanFiles(): string[] {
return this._beanFiles;
}
Expand All @@ -92,11 +118,25 @@ export class DocumentStore extends TextDocuments<TextDocument> {
}

private async _requestDocument(uri: string): Promise<TextDocument> {
const reply = await this._connection.sendRequest<number[]>(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<ArrayBuffer> {
// 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<ArrayBuffer>(CustomMessages.FileRead, uri);
}

protected async fallbackFileRead(_uri: string): Promise<ArrayBuffer> {
this.logger.warn('Client does not support FileRead capability');
return new ArrayBuffer(0);
}

removeFile(uri: string): boolean {
return this._documentsCache.delete(uri);
}
Expand All @@ -116,9 +156,9 @@ export class DocumentStore extends TextDocuments<TextDocument> {
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;
Expand All @@ -127,7 +167,7 @@ export class DocumentStore extends TextDocuments<TextDocument> {
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;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/lsp-server/src/common/features/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ export interface RealBeancountManager {
}

export type BeancountManagerFactory = (connection: Connection, extensionUri: string) => RealBeancountManager;

export interface PlatformMethods {
findBeanFiles: () => Promise<string[]>;
readFile: (uri: string) => Promise<string>;
}
10 changes: 7 additions & 3 deletions packages/lsp-server/src/common/startServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export interface IStorageFactory<T> {
export interface ServerOptions {
webTreeSitterWasmPath?: string;
logLevel?: LogLevel;

isBrowser: boolean;
}

// Create a server logger
Expand All @@ -51,8 +53,9 @@ const serverLogger = new Logger('Server');
export function startServer(
connection: Connection,
factory: IStorageFactory<unknown>,
documents: DocumentStore,
beanMgrFactory: BeancountManagerFactory | undefined,
options: ServerOptions = {},
options: ServerOptions,
): void {
// Set initial log level from options
if (options.logLevel !== undefined) {
Expand All @@ -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<InitializeResult> => {
documents.setInitializeParams(params);
const result: InitializeResult = {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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]);
Expand Down
6 changes: 3 additions & 3 deletions packages/lsp-server/src/node/beancount-manager.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
30 changes: 27 additions & 3 deletions packages/lsp-server/src/node/server.ts
Original file line number Diff line number Diff line change
@@ -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<string[]> {
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<ArrayBuffer> {
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();
1 change: 1 addition & 0 deletions packages/lsp-server/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const nodeConfig = defineConfig({
noExternal: [
...commonConfig.noExternal,
'execa',
'fast-glob',
],
});

Expand Down
Loading