From 5429aec0f161e7da1e191131ef1713c9ec69bba7 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Tue, 1 Jul 2025 15:58:36 +0200 Subject: [PATCH 01/18] Log LSP messages --- apps/lsp/src/config.ts | 11 +- apps/lsp/src/index.ts | 34 +++-- apps/lsp/src/logging.ts | 211 +++++++++++++++++--------------- apps/lsp/src/service/logging.ts | 12 ++ 4 files changed, 160 insertions(+), 108 deletions(-) diff --git a/apps/lsp/src/config.ts b/apps/lsp/src/config.ts index a51cf1a2..4cc81397 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -24,7 +24,8 @@ import { IncludeWorkspaceHeaderCompletions, LsConfiguration, defaultLsConfiguration, - PreferredMdPathExtensionStyle + PreferredMdPathExtensionStyle, + ILogger } from './service'; export type ValidateEnabled = 'ignore' | 'warning' | 'error' | 'hint'; @@ -131,9 +132,14 @@ export class ConfigurationManager extends Disposable { public readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; private _settings: Settings; + private _logger: ILogger; - constructor(private readonly connection_: Connection) { + constructor( + private readonly connection_: Connection, + logger: ILogger, + ) { super(); + this._logger = logger; this._settings = defaultSettings(); } @@ -163,6 +169,7 @@ export class ConfigurationManager extends Disposable { undefined ); this.connection_.onDidChangeConfiguration(() => { + this._logger.logNotification('didChangeConfiguration'); this.update(); }); } diff --git a/apps/lsp/src/index.ts b/apps/lsp/src/index.ts index 19cf7448..3db6e020 100644 --- a/apps/lsp/src/index.ts +++ b/apps/lsp/src/index.ts @@ -52,12 +52,17 @@ import { registerDiagnostics } from "./diagnostics"; // Also include all preview / proposed LSP features. const connection = createConnection(ProposedFeatures.all); +// Initialize logger +const logger = new LogFunctionLogger( + console.log.bind(console), +); + // Create text document manager const documents: TextDocuments = new TextDocuments(TextDocument); documents.listen(connection); // Configuration -const configManager = new ConfigurationManager(connection); +const configManager = new ConfigurationManager(connection, logger); const config = lsConfiguration(configManager); // Capabilities @@ -76,6 +81,8 @@ connection.onInitialize((params: InitializeParams) => { capabilities = params.capabilities; connection.onCompletion(async (params, token): Promise => { + logger.logRequest('completion'); + const document = documents.get(params.textDocument.uri); if (!document) { return []; @@ -85,6 +92,8 @@ connection.onInitialize((params: InitializeParams) => { }) connection.onHover(async (params, token): Promise => { + logger.logRequest('hover'); + const document = documents.get(params.textDocument.uri); if (!document) { return null; @@ -94,6 +103,8 @@ connection.onInitialize((params: InitializeParams) => { connection.onDocumentLinks(async (params, token): Promise => { + logger.logRequest('documentLinks'); + const document = documents.get(params.textDocument.uri); if (!document) { return []; @@ -102,10 +113,13 @@ connection.onInitialize((params: InitializeParams) => { }); connection.onDocumentLinkResolve(async (link, token): Promise => { + logger.logRequest('documentLinksResolve'); return mdLs?.resolveDocumentLink(link, token); }); connection.onDocumentSymbol(async (params, token): Promise => { + logger.logRequest('documentSymbol'); + const document = documents.get(params.textDocument.uri); if (!document) { return []; @@ -114,6 +128,8 @@ connection.onInitialize((params: InitializeParams) => { }); connection.onFoldingRanges(async (params, token): Promise => { + logger.logRequest('foldingRanges'); + const document = documents.get(params.textDocument.uri); if (!document) { return []; @@ -122,6 +138,8 @@ connection.onInitialize((params: InitializeParams) => { }); connection.onSelectionRanges(async (params, token): Promise => { + logger.logRequest('selectionRanges'); + const document = documents.get(params.textDocument.uri); if (!document) { return []; @@ -130,10 +148,13 @@ connection.onInitialize((params: InitializeParams) => { }); connection.onWorkspaceSymbol(async (params, token): Promise => { + logger.logRequest('workspaceSymbol'); return mdLs?.getWorkspaceSymbols(params.query, token) || []; }); connection.onReferences(async (params, token): Promise => { + logger.logRequest('references'); + const document = documents.get(params.textDocument.uri); if (!document) { return []; @@ -142,6 +163,8 @@ connection.onInitialize((params: InitializeParams) => { }); connection.onDefinition(async (params, token): Promise => { + logger.logRequest('definition'); + const document = documents.get(params.textDocument.uri); if (!document) { return undefined; @@ -182,10 +205,12 @@ connection.onInitialize((params: InitializeParams) => { // further config dependent initialization connection.onInitialized(async () => { + logger.logNotification('initialized'); // sync config if possible if (capabilities?.workspace?.configuration) { await configManager.subscribe(); + logger.setConfigurationManager(configManager); } // initialize connection to quarto @@ -207,12 +232,6 @@ connection.onInitialized(async () => { ); const quarto = await initializeQuarto(quartoContext); - // initialize logger - const logger = new LogFunctionLogger( - console.log.bind(console), - configManager - ); - // initialize workspace const workspace = languageServiceWorkspace( workspaceFolders?.map(value => URI.parse(value.uri)) || [], @@ -255,7 +274,6 @@ connection.onInitialized(async () => { // register custom methods registerCustomMethods(quarto, lspConnection, documents); - }); diff --git a/apps/lsp/src/logging.ts b/apps/lsp/src/logging.ts index 7bc2bf86..e7c866a5 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -1,98 +1,113 @@ -/* - * logging.ts - * - * Copyright (C) 2023 by Posit Software, PBC - * Copyright (c) Microsoft Corporation. All rights reserved. - * - * Unless you have received this program directly from Posit Software pursuant - * to the terms of a commercial license agreement with Posit Software, then - * this program is licensed to you under the terms of version 3 of the - * GNU Affero General Public License. This program is distributed WITHOUT - * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, - * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the - * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. - * - */ - -// based on: -// https://github.com/microsoft/vscode/blob/main/extensions/markdown-language-features/server/src/logging.ts - - -import { Disposable } from 'core'; - -import { ILogger, LogLevel } from "./service"; - -import { ConfigurationManager } from './config'; - -export class LogFunctionLogger extends Disposable implements ILogger { - - private static now(): string { - const now = new Date(); - return String(now.getUTCHours()).padStart(2, '0') - + ':' + String(now.getMinutes()).padStart(2, '0') - + ':' + String(now.getUTCSeconds()).padStart(2, '0') + '.' + String(now.getMilliseconds()).padStart(3, '0'); - } - - private static data2String(data: unknown): string { - if (data instanceof Error) { - if (typeof data.stack === 'string') { - return data.stack; - } - return data.message; - } - if (typeof data === 'string') { - return data; - } - return JSON.stringify(data, undefined, 2); - } - - private _logLevel: LogLevel; - - constructor( - private readonly _logFn: typeof console.log, - private readonly _config: ConfigurationManager, - ) { - super(); - - this._register(this._config.onDidChangeConfiguration(() => { - this._logLevel = LogFunctionLogger.readLogLevel(this._config); - })); - - this._logLevel = LogFunctionLogger.readLogLevel(this._config); - } - - private static readLogLevel(config: ConfigurationManager): LogLevel { - switch (config.getSettings().markdown.server.log) { - case 'trace': return LogLevel.Trace; - case 'debug': return LogLevel.Debug; - case 'off': - default: - return LogLevel.Off; - } - } - - get level(): LogLevel { return this._logLevel; } - - public log(level: LogLevel, message: string, data?: unknown): void { - if (this.level < level) { - return; - } - - this.appendLine(`[${this.toLevelLabel(level)} ${LogFunctionLogger.now()}] ${message}`); - if (data) { - this.appendLine(LogFunctionLogger.data2String(data)); - } - } - - private toLevelLabel(level: LogLevel): string { - switch (level) { - case LogLevel.Off: return 'Off'; - case LogLevel.Debug: return 'Debug'; - case LogLevel.Trace: return 'Trace'; - } - } - - private appendLine(value: string): void { - this._logFn(value); - } -} +/* + * logging.ts + * + * Copyright (C) 2023 by Posit Software, PBC + * Copyright (c) Microsoft Corporation. All rights reserved. + * + * Unless you have received this program directly from Posit Software pursuant + * to the terms of a commercial license agreement with Posit Software, then + * this program is licensed to you under the terms of version 3 of the + * GNU Affero General Public License. This program is distributed WITHOUT + * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, + * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the + * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. + * + */ + +// based on: +// https://github.com/microsoft/vscode/blob/main/extensions/markdown-language-features/server/src/logging.ts + + +import { Disposable } from 'core'; + +import { ILogger, LogLevel } from "./service"; + +import { ConfigurationManager } from './config'; + +export class LogFunctionLogger extends Disposable implements ILogger { + + private static now(): string { + const now = new Date(); + return String(now.getUTCHours()).padStart(2, '0') + + ':' + String(now.getMinutes()).padStart(2, '0') + + ':' + String(now.getUTCSeconds()).padStart(2, '0') + '.' + String(now.getMilliseconds()).padStart(3, '0'); + } + + private static data2String(data: unknown): string { + if (data instanceof Error) { + if (typeof data.stack === 'string') { + return data.stack; + } + return data.message; + } + if (typeof data === 'string') { + return data; + } + return JSON.stringify(data, undefined, 2); + } + + private _logLevel: LogLevel; + private _config?: ConfigurationManager; + + constructor( + private readonly _logFn: typeof console.log, + ) { + super(); + + // Be verbose during init until we have a chance to get the user configuration + this._logLevel = LogLevel.Debug; + } + + setConfigurationManager(config: ConfigurationManager) { + this._config = config; + + this._register(this._config.onDidChangeConfiguration(() => { + this._logLevel = LogFunctionLogger.readLogLevel(this._config!); + })); + + this._logLevel = LogFunctionLogger.readLogLevel(this._config); + } + + private static readLogLevel(config: ConfigurationManager): LogLevel { + switch (config.getSettings().markdown.server.log) { + case 'trace': return LogLevel.Trace; + case 'debug': return LogLevel.Debug; + case 'off': + default: + return LogLevel.Off; + } + } + + get level(): LogLevel { return this._logLevel; } + + public log(level: LogLevel, message: string, data?: unknown): void { + if (this.level < level) { + return; + } + + this.appendLine(`[${this.toLevelLabel(level)} ${LogFunctionLogger.now()}] ${message}`); + if (data) { + this.appendLine(LogFunctionLogger.data2String(data)); + } + } + + public logNotification(method: string) { + this.log(LogLevel.Trace, `Got notification: '${method}'`); + } + + public logRequest(method: string) { + this.log(LogLevel.Trace, `Got request: '${method}'`); + } + + private toLevelLabel(level: LogLevel): string { + switch (level) { + case LogLevel.Off: return 'off'; + case LogLevel.Debug: return 'debug'; + case LogLevel.Trace: return 'trace'; + } + } + + private appendLine(value: string): void { + this._logFn(value); + } +} diff --git a/apps/lsp/src/service/logging.ts b/apps/lsp/src/service/logging.ts index a44b47ef..198ea5ca 100644 --- a/apps/lsp/src/service/logging.ts +++ b/apps/lsp/src/service/logging.ts @@ -45,4 +45,16 @@ export interface ILogger { * @param data Additional information about what is being logged. */ log(level: LogLevel, message: string, data?: Record): void; + + /** + * Log notification at Trace level. + * @param method Message type name. + */ + logNotification(method: string): void; + + /** + * Log request at Trace level. + * @param method Message type name. + */ + logRequest(method: string): void; } From 08fe7c941afe66b593973b659467b134c86100a3 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Tue, 1 Jul 2025 16:29:45 +0200 Subject: [PATCH 02/18] Log via LSP once we're connected --- apps/lsp/src/index.ts | 8 ++++---- apps/lsp/src/logging.ts | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/lsp/src/index.ts b/apps/lsp/src/index.ts index 3db6e020..cb0212e1 100644 --- a/apps/lsp/src/index.ts +++ b/apps/lsp/src/index.ts @@ -53,9 +53,7 @@ import { registerDiagnostics } from "./diagnostics"; const connection = createConnection(ProposedFeatures.all); // Initialize logger -const logger = new LogFunctionLogger( - console.log.bind(console), -); +const logger = new LogFunctionLogger(console.log.bind(console)); // Create text document manager const documents: TextDocuments = new TextDocuments(TextDocument); @@ -71,10 +69,12 @@ let capabilities: ClientCapabilities | undefined; // Initialization options let initializationOptions: LspInitializationOptions | undefined; -// Markdowdn language service +// Markdown language service let mdLs: IMdLanguageService | undefined; connection.onInitialize((params: InitializeParams) => { + // We're connected, log messages via LSP + logger.setConnection(connection); // alias options and capabilities initializationOptions = params.initializationOptions; diff --git a/apps/lsp/src/logging.ts b/apps/lsp/src/logging.ts index e7c866a5..56d18e93 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -23,6 +23,7 @@ import { Disposable } from 'core'; import { ILogger, LogLevel } from "./service"; import { ConfigurationManager } from './config'; +import { Connection } from 'vscode-languageserver'; export class LogFunctionLogger extends Disposable implements ILogger { @@ -47,6 +48,7 @@ export class LogFunctionLogger extends Disposable implements ILogger { } private _logLevel: LogLevel; + private _connection?: Connection; private _config?: ConfigurationManager; constructor( @@ -58,6 +60,10 @@ export class LogFunctionLogger extends Disposable implements ILogger { this._logLevel = LogLevel.Debug; } + setConnection(connection: Connection) { + this._connection = connection; + } + setConfigurationManager(config: ConfigurationManager) { this._config = config; @@ -85,7 +91,7 @@ export class LogFunctionLogger extends Disposable implements ILogger { return; } - this.appendLine(`[${this.toLevelLabel(level)} ${LogFunctionLogger.now()}] ${message}`); + this.appendLine(`[lsp-${this.toLevelLabel(level)} ${LogFunctionLogger.now()}] ${message}`); if (data) { this.appendLine(LogFunctionLogger.data2String(data)); } @@ -108,6 +114,11 @@ export class LogFunctionLogger extends Disposable implements ILogger { } private appendLine(value: string): void { - this._logFn(value); + // If we're connected, send log messages to client as LSP notifications + if (this._connection) { + this._connection.console.log(value); + } else { + this._logFn(value); + } } } From 045cf913aa53f5d14c59ee21d656c89464d985a8 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Tue, 1 Jul 2025 17:09:48 +0200 Subject: [PATCH 03/18] Don't log time twice now that time is added on the frontend side --- apps/lsp/src/logging.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/lsp/src/logging.ts b/apps/lsp/src/logging.ts index 56d18e93..69ab9410 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -62,6 +62,7 @@ export class LogFunctionLogger extends Disposable implements ILogger { setConnection(connection: Connection) { this._connection = connection; + this.log(LogLevel.Debug, 'LSP is now connected'); } setConfigurationManager(config: ConfigurationManager) { @@ -91,7 +92,7 @@ export class LogFunctionLogger extends Disposable implements ILogger { return; } - this.appendLine(`[lsp-${this.toLevelLabel(level)} ${LogFunctionLogger.now()}] ${message}`); + this.appendLine(`[lsp-${this.toLevelLabel(level)}] ${message}`); if (data) { this.appendLine(LogFunctionLogger.data2String(data)); } From 78b4dc272d9c5dad64303e5421c32f906eee5a00 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Mon, 30 Jun 2025 12:58:57 +0200 Subject: [PATCH 04/18] Add `justfile` for CLI shortcuts --- apps/vscode/justfile | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 apps/vscode/justfile diff --git a/apps/vscode/justfile b/apps/vscode/justfile new file mode 100644 index 00000000..a5f5e7e4 --- /dev/null +++ b/apps/vscode/justfile @@ -0,0 +1,8 @@ +install: + rm -rf *.vsix && vsce package && code --install-extension *.vsix && positron --install-extension *.vsix + +install-vscode: + rm -rf *.vsix && vsce package && code --install-extension *.vsix + +install-positron: + rm -rf *.vsix && vsce package && positron --install-extension *.vsix From 13fa25c01c9405e19255213679f21763a7fa4d1c Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Wed, 2 Jul 2025 07:01:53 +0200 Subject: [PATCH 05/18] Finish plumbing configuration change notifications --- apps/lsp/.eslintrc.js | 3 +++ apps/lsp/src/config.ts | 20 ++++++++++++++------ apps/lsp/src/index.ts | 1 + apps/vscode/src/lsp/client.ts | 11 +++++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/apps/lsp/.eslintrc.js b/apps/lsp/.eslintrc.js index 2308ff96..f00ad881 100644 --- a/apps/lsp/.eslintrc.js +++ b/apps/lsp/.eslintrc.js @@ -1,4 +1,7 @@ module.exports = { root: true, extends: ["custom-server"], + rules: { + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + }, }; diff --git a/apps/lsp/src/config.ts b/apps/lsp/src/config.ts index 4cc81397..d6513ea8 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -25,7 +25,8 @@ import { LsConfiguration, defaultLsConfiguration, PreferredMdPathExtensionStyle, - ILogger + ILogger, + LogLevel } from './service'; export type ValidateEnabled = 'ignore' | 'warning' | 'error' | 'hint'; @@ -144,6 +145,7 @@ export class ConfigurationManager extends Disposable { } public async update() { + this._logger.log(LogLevel.Trace, 'Sending \'configuration\' request'); const settings = await this.connection_.workspace.getConfiguration(); this._settings = { @@ -163,15 +165,21 @@ export class ConfigurationManager extends Disposable { } public async subscribe() { - await this.update(); + // Ignore the settings in parameters, the modern usage is to fetch the settings + // when we get this notification + this.connection_.onDidChangeConfiguration((_params) => { + this._logger.logNotification('didChangeConfiguration'); + this.update(); + }); + + // Get notified of configuration changes by client await this.connection_.client.register( DidChangeConfigurationNotification.type, undefined ); - this.connection_.onDidChangeConfiguration(() => { - this._logger.logNotification('didChangeConfiguration'); - this.update(); - }); + + // Retrieve initial values for settings of interest + await this.update(); } public getSettings(): Settings { diff --git a/apps/lsp/src/index.ts b/apps/lsp/src/index.ts index cb0212e1..82eafb44 100644 --- a/apps/lsp/src/index.ts +++ b/apps/lsp/src/index.ts @@ -75,6 +75,7 @@ let mdLs: IMdLanguageService | undefined; connection.onInitialize((params: InitializeParams) => { // We're connected, log messages via LSP logger.setConnection(connection); + logger.logRequest('initialize'); // alias options and capabilities initializationOptions = params.initializationOptions; diff --git a/apps/vscode/src/lsp/client.ts b/apps/vscode/src/lsp/client.ts index 0799d3d8..74451a53 100644 --- a/apps/vscode/src/lsp/client.ts +++ b/apps/vscode/src/lsp/client.ts @@ -146,6 +146,17 @@ export async function activateLsp( clientOptions ); + // Notify LSP of quarto setting changes + context.subscriptions.push(workspace.onDidChangeConfiguration(e => { + if (client.state !== State.Running) { + return; + } + + if (e.affectsConfiguration("quarto")) { + client.sendNotification("workspace/didChangeConfiguration", {}); + } + })); + // return once the server is running return new Promise((resolve, reject) => { From d7c69405d984e168703dcd49f50b1993c4066545 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Wed, 2 Jul 2025 07:16:32 +0200 Subject: [PATCH 06/18] Make log level a quarto setting --- apps/lsp/src/config.ts | 11 ++++------- apps/lsp/src/logging.ts | 12 ++++++++---- apps/lsp/src/service/config.ts | 9 ++++++--- apps/vscode/package.json | 11 +++++++++++ 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/apps/lsp/src/config.ts b/apps/lsp/src/config.ts index d6513ea8..8e0d3d0a 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -28,6 +28,7 @@ import { ILogger, LogLevel } from './service'; +import { LogFunctionLogger } from './logging'; export type ValidateEnabled = 'ignore' | 'warning' | 'error' | 'hint'; @@ -36,6 +37,7 @@ export interface Settings { readonly colorTheme: string; }; readonly quarto: { + readonly logLevel: LogLevel; readonly path: string; readonly mathjax: { readonly scale: number; @@ -43,10 +45,6 @@ export interface Settings { } }; readonly markdown: { - readonly server: { - readonly log: 'off' | 'debug' | 'trace'; - }; - readonly preferredMdPathExtensionStyle: 'auto' | 'includeExtension' | 'removeExtension'; readonly suggest: { @@ -85,6 +83,7 @@ function defaultSettings(): Settings { colorTheme: 'Dark+' }, quarto: { + logLevel: LogLevel.Trace, path: "", mathjax: { scale: 1, @@ -92,9 +91,6 @@ function defaultSettings(): Settings { } }, markdown: { - server: { - log: 'off' - }, preferredMdPathExtensionStyle: 'auto', suggest: { paths: { @@ -154,6 +150,7 @@ export class ConfigurationManager extends Disposable { colorTheme: settings.workbench.colorTheme }, quarto: { + logLevel: LogFunctionLogger.parseLogLevel(settings.quarto.server.logLevel), path: settings.quarto.path, mathjax: { scale: settings.quarto.mathjax.scale, diff --git a/apps/lsp/src/logging.ts b/apps/lsp/src/logging.ts index 69ab9410..31dda9ed 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -69,14 +69,18 @@ export class LogFunctionLogger extends Disposable implements ILogger { this._config = config; this._register(this._config.onDidChangeConfiguration(() => { - this._logLevel = LogFunctionLogger.readLogLevel(this._config!); + this._logLevel = LogFunctionLogger.currentLogLevel(this._config!); })); - this._logLevel = LogFunctionLogger.readLogLevel(this._config); + this._logLevel = LogFunctionLogger.currentLogLevel(this._config); } - private static readLogLevel(config: ConfigurationManager): LogLevel { - switch (config.getSettings().markdown.server.log) { + private static currentLogLevel(config: ConfigurationManager): LogLevel { + return config.getSettings().quarto.logLevel; + } + + public static parseLogLevel(logLevel: string): LogLevel { + switch (logLevel) { case 'trace': return LogLevel.Trace; case 'debug': return LogLevel.Debug; case 'off': diff --git a/apps/lsp/src/service/config.ts b/apps/lsp/src/service/config.ts index 3f3f2281..f0b88031 100644 --- a/apps/lsp/src/service/config.ts +++ b/apps/lsp/src/service/config.ts @@ -17,6 +17,7 @@ import * as picomatch from 'picomatch'; import { URI } from 'vscode-uri'; import { MathjaxSupportedExtension } from 'editor-types'; +import { LogLevel } from './logging'; /** * Preferred style for file paths to {@link markdownFileExtensions markdown files}. @@ -39,6 +40,8 @@ export enum PreferredMdPathExtensionStyle { } export interface LsConfiguration { + readonly logLevel: LogLevel; + /** * List of file extensions should be considered markdown. * @@ -73,15 +76,15 @@ export interface LsConfiguration { readonly includeWorkspaceHeaderCompletions: 'never' | 'onSingleOrDoubleHash' | 'onDoubleHash'; - readonly colorTheme: "light" | "dark"; + readonly colorTheme: 'light' | 'dark'; readonly mathjaxScale: number; readonly mathjaxExtensions: readonly MathjaxSupportedExtension[]; - } export const defaultMarkdownFileExtension = 'qmd'; const defaultConfig: LsConfiguration = { + logLevel: LogLevel.Trace, markdownFileExtensions: [defaultMarkdownFileExtension, 'md'], knownLinkedToFileExtensions: [ 'jpg', @@ -104,7 +107,7 @@ const defaultConfig: LsConfiguration = { "**/env/**" ], includeWorkspaceHeaderCompletions: 'never', - colorTheme: "light", + colorTheme: 'light', mathjaxScale: 1, mathjaxExtensions: [] }; diff --git a/apps/vscode/package.json b/apps/vscode/package.json index 83c30344..02730b2f 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -1306,6 +1306,17 @@ "type": "string" }, "uniqueItems": true + }, + "quarto.server.logLevel": { + "scope": "window", + "type": "string", + "default": "warn", + "enum": [ + "trace", + "debug", + "off" + ], + "markdownDescription": "Log level for the Quarto language server." } } }, From ad8be1ae77ed32580be25462956c12459487afe1 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 3 Jul 2025 07:49:24 +0200 Subject: [PATCH 07/18] Take info, warn, and error log levels --- apps/lsp/src/config.ts | 2 +- apps/lsp/src/logging.ts | 14 +++++++++----- apps/lsp/src/service/logging.ts | 14 ++++++++++---- apps/vscode/package.json | 4 +++- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/apps/lsp/src/config.ts b/apps/lsp/src/config.ts index 8e0d3d0a..98b4a104 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -83,7 +83,7 @@ function defaultSettings(): Settings { colorTheme: 'Dark+' }, quarto: { - logLevel: LogLevel.Trace, + logLevel: LogLevel.Info, path: "", mathjax: { scale: 1, diff --git a/apps/lsp/src/logging.ts b/apps/lsp/src/logging.ts index 31dda9ed..6f453da4 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -83,16 +83,18 @@ export class LogFunctionLogger extends Disposable implements ILogger { switch (logLevel) { case 'trace': return LogLevel.Trace; case 'debug': return LogLevel.Debug; - case 'off': + case 'info': return LogLevel.Info; + case 'warn': return LogLevel.Warn; + case 'error': return LogLevel.Error; default: - return LogLevel.Off; + return LogLevel.Warn; } } get level(): LogLevel { return this._logLevel; } public log(level: LogLevel, message: string, data?: unknown): void { - if (this.level < level) { + if (level < this.level) { return; } @@ -112,9 +114,11 @@ export class LogFunctionLogger extends Disposable implements ILogger { private toLevelLabel(level: LogLevel): string { switch (level) { - case LogLevel.Off: return 'off'; - case LogLevel.Debug: return 'debug'; case LogLevel.Trace: return 'trace'; + case LogLevel.Debug: return 'debug'; + case LogLevel.Info: return 'info'; + case LogLevel.Warn: return 'warn'; + case LogLevel.Error: return 'error'; } } diff --git a/apps/lsp/src/service/logging.ts b/apps/lsp/src/service/logging.ts index 198ea5ca..44f3fe26 100644 --- a/apps/lsp/src/service/logging.ts +++ b/apps/lsp/src/service/logging.ts @@ -18,14 +18,20 @@ * The level of verbosity that the language service logs at. */ export enum LogLevel { - /** Disable logging */ - Off, + /** Log extremely verbose info about language server operation, such as calls into the file system */ + Trace, /** Log verbose info about language server operation, such as when references are re-computed for a md file. */ Debug, - /** Log extremely verbose info about language server operation, such as calls into the file system */ - Trace, + /** Informational messages that highlight the progress of the application at coarse-grained level. */ + Info, + + /** Potentially harmful situations which still allow the application to continue running. */ + Warn, + + /** Error events that might still allow the application to continue running. */ + Error, } /** diff --git a/apps/vscode/package.json b/apps/vscode/package.json index 02730b2f..1a69d34e 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -1314,7 +1314,9 @@ "enum": [ "trace", "debug", - "off" + "info", + "warn", + "error" ], "markdownDescription": "Log level for the Quarto language server." } From acfda25cb9d54db1047008b4b0d5e9c815dc8e38 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 3 Jul 2025 07:51:56 +0200 Subject: [PATCH 08/18] Add `log()` wrappers --- apps/lsp/src/config.ts | 2 +- apps/lsp/src/diagnostics.ts | 3 +-- apps/lsp/src/logging.ts | 25 +++++++++++++++---- apps/lsp/src/service/logging.ts | 10 ++++++-- apps/lsp/src/service/providers/diagnostics.ts | 8 +++--- .../src/service/providers/document-links.ts | 2 +- .../src/service/providers/document-symbols.ts | 2 +- apps/lsp/src/service/providers/folding.ts | 2 +- apps/lsp/src/service/providers/references.ts | 4 +-- .../lsp/src/service/providers/smart-select.ts | 2 +- apps/lsp/src/service/toc.ts | 2 +- apps/lsp/src/workspace.ts | 19 +++++++------- 12 files changed, 50 insertions(+), 31 deletions(-) diff --git a/apps/lsp/src/config.ts b/apps/lsp/src/config.ts index 98b4a104..e3d2ebf1 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -141,7 +141,7 @@ export class ConfigurationManager extends Disposable { } public async update() { - this._logger.log(LogLevel.Trace, 'Sending \'configuration\' request'); + this._logger.logTrace('Sending \'configuration\' request'); const settings = await this.connection_.workspace.getConfiguration(); this._settings = { diff --git a/apps/lsp/src/diagnostics.ts b/apps/lsp/src/diagnostics.ts index 4a9ce14d..725eb54f 100644 --- a/apps/lsp/src/diagnostics.ts +++ b/apps/lsp/src/diagnostics.ts @@ -35,7 +35,6 @@ import { ILogger, IMdLanguageService, IWorkspace, - LogLevel, } from "./service"; import { ConfigurationManager, @@ -130,7 +129,7 @@ export async function registerDiagnostics( FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport > => { - logger.log(LogLevel.Debug, "connection.languages.diagnostics.on", { + logger.logDebug("connection.languages.diagnostics.on", { document: params.textDocument.uri, }); diff --git a/apps/lsp/src/logging.ts b/apps/lsp/src/logging.ts index 6f453da4..26d67ca7 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -62,7 +62,7 @@ export class LogFunctionLogger extends Disposable implements ILogger { setConnection(connection: Connection) { this._connection = connection; - this.log(LogLevel.Debug, 'LSP is now connected'); + this.logInfo('LSP is now connected'); } setConfigurationManager(config: ConfigurationManager) { @@ -104,12 +104,27 @@ export class LogFunctionLogger extends Disposable implements ILogger { } } - public logNotification(method: string) { - this.log(LogLevel.Trace, `Got notification: '${method}'`); + public logTrace(message: string, data?: Record): void { + this.log(LogLevel.Trace, message, data); + } + public logDebug(message: string, data?: Record): void { + this.log(LogLevel.Debug, message, data); + } + public logInfo(message: string, data?: Record): void { + this.log(LogLevel.Info, message, data); + } + public logWarn(message: string, data?: Record): void { + this.log(LogLevel.Warn, message, data); + } + public logError(message: string, data?: Record): void { + this.log(LogLevel.Error, message, data); } - public logRequest(method: string) { - this.log(LogLevel.Trace, `Got request: '${method}'`); + public logNotification(method: string, data?: Record) { + this.logTrace(`Got notification: '${method}'`, data); + } + public logRequest(method: string, data?: Record) { + this.logTrace(`Got request: '${method}'`, data); } private toLevelLabel(level: LogLevel): string { diff --git a/apps/lsp/src/service/logging.ts b/apps/lsp/src/service/logging.ts index 44f3fe26..973ca2a9 100644 --- a/apps/lsp/src/service/logging.ts +++ b/apps/lsp/src/service/logging.ts @@ -52,15 +52,21 @@ export interface ILogger { */ log(level: LogLevel, message: string, data?: Record): void; + logTrace(message: string, data?: Record): void; + logDebug(message: string, data?: Record): void; + logInfo(message: string, data?: Record): void; + logWarn(message: string, data?: Record): void; + logError(message: string, data?: Record): void; + /** * Log notification at Trace level. * @param method Message type name. */ - logNotification(method: string): void; + logNotification(method: string, data?: Record): void; /** * Log request at Trace level. * @param method Message type name. */ - logRequest(method: string): void; + logRequest(method: string, data?: Record): void; } diff --git a/apps/lsp/src/service/providers/diagnostics.ts b/apps/lsp/src/service/providers/diagnostics.ts index 6cd33afa..ca21ea99 100644 --- a/apps/lsp/src/service/providers/diagnostics.ts +++ b/apps/lsp/src/service/providers/diagnostics.ts @@ -210,7 +210,7 @@ export class DiagnosticComputer { readonly links: readonly MdLink[]; readonly statCache: ResourceMap<{ readonly exists: boolean }>; }> { - this.#logger.log(LogLevel.Debug, 'DiagnosticComputer.compute', { document: doc.uri, version: doc.version }); + this.#logger.logDebug('DiagnosticComputer.compute', { document: doc.uri, version: doc.version }); const { links, definitions } = await this.#linkProvider.getLinks(doc); const statCache = new ResourceMap<{ readonly exists: boolean }>(); @@ -235,7 +235,7 @@ export class DiagnosticComputer { ])).flat()); } - this.#logger.log(LogLevel.Trace, 'DiagnosticComputer.compute finished', { document: doc.uri, version: doc.version, diagnostics }); + this.#logger.logTrace('DiagnosticComputer.compute finished', { document: doc.uri, version: doc.version, diagnostics }); return { links: links, @@ -612,7 +612,7 @@ class FileLinkState extends Disposable { } #onLinkedResourceChanged(resource: URI, exists: boolean) { - this.#logger.log(LogLevel.Trace, 'FileLinkState.onLinkedResourceChanged', { resource, exists }); + this.#logger.logTrace('FileLinkState.onLinkedResourceChanged', { resource, exists }); const entry = this.#linkedToFile.get(resource); if (entry) { @@ -650,7 +650,7 @@ export class DiagnosticsManager extends Disposable implements IPullDiagnosticsMa this.#linkWatcher = this._register(linkWatcher); this._register(this.#linkWatcher.onDidChangeLinkedToFile(e => { - logger.log(LogLevel.Trace, 'DiagnosticsManager.onDidChangeLinkedToFile', { resource: e.changedResource }); + logger.logTrace('DiagnosticsManager.onDidChangeLinkedToFile', { resource: e.changedResource }); this.#onLinkedToFileChanged.fire({ changedResource: e.changedResource, diff --git a/apps/lsp/src/service/providers/document-links.ts b/apps/lsp/src/service/providers/document-links.ts index 54f8d14e..d62869de 100644 --- a/apps/lsp/src/service/providers/document-links.ts +++ b/apps/lsp/src/service/providers/document-links.ts @@ -779,7 +779,7 @@ export class MdLinkProvider extends Disposable { this.#linkComputer = new MdLinkComputer(parser, this.#workspace); this.#linkCache = this._register(new MdDocumentInfoCache(this.#workspace, async (doc, token) => { - logger.log(LogLevel.Debug, 'LinkProvider.compute', { document: doc.uri, version: doc.version }); + logger.logDebug('LinkProvider.compute', { document: doc.uri, version: doc.version }); const links = await this.#linkComputer.getAllLinks(doc, token); return { diff --git a/apps/lsp/src/service/providers/document-symbols.ts b/apps/lsp/src/service/providers/document-symbols.ts index ddd6e047..678a8a1a 100644 --- a/apps/lsp/src/service/providers/document-symbols.ts +++ b/apps/lsp/src/service/providers/document-symbols.ts @@ -48,7 +48,7 @@ export class MdDocumentSymbolProvider { } public async provideDocumentSymbols(document: Document, options: ProvideDocumentSymbolOptions, token: CancellationToken): Promise { - this.#logger.log(LogLevel.Debug, 'DocumentSymbolProvider.provideDocumentSymbols', { document: document.uri, version: document.version }); + this.#logger.logDebug('DocumentSymbolProvider.provideDocumentSymbols', { document: document.uri, version: document.version }); if (token.isCancellationRequested) { return []; diff --git a/apps/lsp/src/service/providers/folding.ts b/apps/lsp/src/service/providers/folding.ts index d6a9e6c2..6add2efa 100644 --- a/apps/lsp/src/service/providers/folding.ts +++ b/apps/lsp/src/service/providers/folding.ts @@ -45,7 +45,7 @@ export class MdFoldingProvider { } public async provideFoldingRanges(document: Document, token: CancellationToken): Promise { - this.#logger.log(LogLevel.Debug, 'MdFoldingProvider.provideFoldingRanges', { document: document.uri, version: document.version }); + this.#logger.logDebug('MdFoldingProvider.provideFoldingRanges', { document: document.uri, version: document.version }); if (token.isCancellationRequested) { return []; diff --git a/apps/lsp/src/service/providers/references.ts b/apps/lsp/src/service/providers/references.ts index d87341ba..0119f439 100644 --- a/apps/lsp/src/service/providers/references.ts +++ b/apps/lsp/src/service/providers/references.ts @@ -119,7 +119,7 @@ export class MdReferencesProvider extends Disposable { } public async getReferencesAtPosition(document: Document, position: lsp.Position, token: CancellationToken): Promise { - this.#logger.log(LogLevel.Debug, 'ReferencesProvider.getReferencesAtPosition', { document: document.uri, version: document.version }); + this.#logger.logDebug('ReferencesProvider.getReferencesAtPosition', { document: document.uri, version: document.version }); const toc = await this.#tocProvider.getForDocument(document); if (token.isCancellationRequested) { @@ -135,7 +135,7 @@ export class MdReferencesProvider extends Disposable { } public async getReferencesToFileInWorkspace(resource: URI, token: CancellationToken): Promise { - this.#logger.log(LogLevel.Debug, 'ReferencesProvider.getAllReferencesToFileInWorkspace', { resource }); + this.#logger.logDebug('ReferencesProvider.getAllReferencesToFileInWorkspace', { resource }); if (token.isCancellationRequested) { return []; diff --git a/apps/lsp/src/service/providers/smart-select.ts b/apps/lsp/src/service/providers/smart-select.ts index ab3b374f..6758beeb 100644 --- a/apps/lsp/src/service/providers/smart-select.ts +++ b/apps/lsp/src/service/providers/smart-select.ts @@ -41,7 +41,7 @@ export class MdSelectionRangeProvider { } public async provideSelectionRanges(document: Document, positions: readonly Position[], token: CancellationToken): Promise { - this.#logger.log(LogLevel.Debug, 'MdSelectionRangeProvider.provideSelectionRanges', { document: document.uri, version: document.version }); + this.#logger.logDebug('MdSelectionRangeProvider.provideSelectionRanges', { document: document.uri, version: document.version }); if (token.isCancellationRequested) { return undefined; diff --git a/apps/lsp/src/service/toc.ts b/apps/lsp/src/service/toc.ts index 8ffe3fd4..1df0f731 100644 --- a/apps/lsp/src/service/toc.ts +++ b/apps/lsp/src/service/toc.ts @@ -299,7 +299,7 @@ export class MdTableOfContentsProvider extends Disposable { this.#logger = logger; this.#cache = this._register(new MdDocumentInfoCache(workspace, (doc, token) => { - this.#logger.log(LogLevel.Debug, 'TableOfContentsProvider.create', { document: doc.uri, version: doc.version }); + this.#logger.logDebug('TableOfContentsProvider.create', { document: doc.uri, version: doc.version }); return TableOfContents.create(parser, doc, token); })); } diff --git a/apps/lsp/src/workspace.ts b/apps/lsp/src/workspace.ts index 69c4b82f..d310fcb0 100644 --- a/apps/lsp/src/workspace.ts +++ b/apps/lsp/src/workspace.ts @@ -33,7 +33,6 @@ import { Document, isQuartoDoc } from "quarto-core"; import { FileStat, ILogger, - LogLevel, LsConfiguration, IWorkspace, IWorkspaceWithWatching, @@ -98,7 +97,7 @@ export function languageServiceWorkspace( const onDidDeleteMarkdownDocument = new Emitter(); const doDeleteDocument = (uri: URI) => { - logger.log(LogLevel.Trace, 'VsCodeClientWorkspace.deleteDocument', { document: uri.toString() }); + logger.logTrace('VsCodeClientWorkspace.deleteDocument', { document: uri.toString() }); documentCache.delete(uri); onDidDeleteMarkdownDocument.fire(uri); } @@ -107,7 +106,7 @@ export function languageServiceWorkspace( if (!isRelevantMarkdownDocument(e.document)) { return; } - logger.log(LogLevel.Trace, 'VsCodeClientWorkspace.TextDocument.onDidOpen', { document: e.document.uri }); + logger.logNotification('onDidOpen', { document: e.document.uri }); const uri = URI.parse(e.document.uri); const doc = documentCache.get(uri); @@ -132,7 +131,7 @@ export function languageServiceWorkspace( return; } - logger.log(LogLevel.Trace, 'VsCodeClientWorkspace.TextDocument.onDidChanceContent', { document: e.document.uri }); + logger.logNotification('onDidChangeContent', { document: e.document.uri }); const uri = URI.parse(e.document.uri); const entry = documentCache.get(uri); @@ -147,7 +146,7 @@ export function languageServiceWorkspace( return; } - logger.log(LogLevel.Trace, 'VsCodeClientWorkspace.TextDocument.onDidClose', { document: e.document.uri }); + logger.logNotification('onDidClose', { document: e.document.uri }); const uri = URI.parse(e.document.uri); const doc = documentCache.get(uri); @@ -254,7 +253,7 @@ export function languageServiceWorkspace( }, async stat(resource: URI): Promise { - logger.log(LogLevel.Trace, 'VsCodeClientWorkspace.stat', { resource: resource.toString() }); + logger.logTrace('VsCodeClientWorkspace.stat', { resource: resource.toString() }); if (documentCache.has(resource)) { return { isDirectory: false }; } @@ -262,7 +261,7 @@ export function languageServiceWorkspace( }, async readDirectory(resource: URI): Promise> { - logger.log(LogLevel.Trace, 'VsCodeClientWorkspace.readDirectory', { resource: resource.toString() }); + logger.logTrace('VsCodeClientWorkspace.readDirectory', { resource: resource.toString() }); const result = await fspromises.readdir(resource.fsPath, { withFileTypes: true }); return result.map(value => [value.name, { isDirectory: value.isDirectory() }]); }, @@ -313,7 +312,7 @@ export function languageServiceWorkspace( // keep document cache up to date and notify clients for (const change of changes) { const resource = URI.parse(change.uri); - logger.log(LogLevel.Trace, 'VsCodeClientWorkspace.onDidChangeWatchedFiles', { type: change.type, resource: resource.toString() }); + logger.logTrace('VsCodeClientWorkspace.onDidChangeWatchedFiles', { type: change.type, resource: resource.toString() }); switch (change.type) { case FileChangeType.Changed: { const entry = documentCache.get(resource); @@ -356,7 +355,7 @@ export function languageServiceWorkspace( const fsWorkspace: IWorkspaceWithWatching = { ...workspace, watchFile(resource, options) { - logger.log(LogLevel.Trace, 'VsCodeClientWorkspace.watchFile', { resource: resource.toString() }); + logger.logTrace('VsCodeClientWorkspace.watchFile', { resource: resource.toString() }); const entry = { resource, @@ -371,7 +370,7 @@ export function languageServiceWorkspace( onDidChange: entry.onDidChange.event, onDidDelete: entry.onDidDelete.event, dispose: () => { - logger.log(LogLevel.Trace, 'VsCodeClientWorkspace.disposeWatcher', { resource: resource.toString() }); + logger.logTrace('VsCodeClientWorkspace.disposeWatcher', { resource: resource.toString() }); watchers.delete(entry.resource.toString()); } }; From 41ced6e905c38f14475e14fd5a2045c416f53472 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 3 Jul 2025 09:06:19 +0200 Subject: [PATCH 09/18] Pass `logLevel` via init options --- apps/lsp/src/config.ts | 14 ++++++++++++++ apps/lsp/src/index.ts | 4 ++++ apps/lsp/src/logging.ts | 3 --- apps/vscode/src/lsp/client.ts | 3 ++- packages/quarto-core/src/lsp.ts | 3 ++- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/lsp/src/config.ts b/apps/lsp/src/config.ts index e3d2ebf1..7d1da492 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -140,6 +140,20 @@ export class ConfigurationManager extends Disposable { this._settings = defaultSettings(); } + public init(logLevel?: string) { + const initLogLevel = LogFunctionLogger.parseLogLevel( + logLevel ?? "warn" + ); + + this._settings = { + ...this._settings, + quarto: { + ...this._settings.quarto, + logLevel: initLogLevel, + } + }; + } + public async update() { this._logger.logTrace('Sending \'configuration\' request'); const settings = await this.connection_.workspace.getConfiguration(); diff --git a/apps/lsp/src/index.ts b/apps/lsp/src/index.ts index 82eafb44..f64db03f 100644 --- a/apps/lsp/src/index.ts +++ b/apps/lsp/src/index.ts @@ -73,6 +73,10 @@ let initializationOptions: LspInitializationOptions | undefined; let mdLs: IMdLanguageService | undefined; connection.onInitialize((params: InitializeParams) => { + // Set log level from initialization options if provided so that we use the + // expected level as soon as possible + configManager.init(params.initializationOptions?.logLevel); + // We're connected, log messages via LSP logger.setConnection(connection); logger.logRequest('initialize'); diff --git a/apps/lsp/src/logging.ts b/apps/lsp/src/logging.ts index 26d67ca7..dbf4a356 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -55,9 +55,6 @@ export class LogFunctionLogger extends Disposable implements ILogger { private readonly _logFn: typeof console.log, ) { super(); - - // Be verbose during init until we have a chance to get the user configuration - this._logLevel = LogLevel.Debug; } setConnection(connection: Connection) { diff --git a/apps/vscode/src/lsp/client.ts b/apps/vscode/src/lsp/client.ts index 74451a53..f70cccd7 100644 --- a/apps/vscode/src/lsp/client.ts +++ b/apps/vscode/src/lsp/client.ts @@ -117,7 +117,8 @@ export async function activateLsp( // create client options const initializationOptions: LspInitializationOptions = { - quartoBinPath: quartoContext.binPath + quartoBinPath: quartoContext.binPath, + logLevel: config.get("server.logLevel"), }; const documentSelectorPattern = semver.gte(quartoContext.version, "1.6.24") ? diff --git a/packages/quarto-core/src/lsp.ts b/packages/quarto-core/src/lsp.ts index 4a7c6009..09b58d65 100644 --- a/packages/quarto-core/src/lsp.ts +++ b/packages/quarto-core/src/lsp.ts @@ -15,4 +15,5 @@ export interface LspInitializationOptions { quartoBinPath?: string; -} \ No newline at end of file + logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error'; +} From dd5d0b50e2b74441ebb1f61a939e576bb4ffb83c Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 3 Jul 2025 09:24:59 +0200 Subject: [PATCH 10/18] Pass log level to client --- apps/lsp/src/logging.ts | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/apps/lsp/src/logging.ts b/apps/lsp/src/logging.ts index dbf4a356..c777d814 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -95,9 +95,12 @@ export class LogFunctionLogger extends Disposable implements ILogger { return; } - this.appendLine(`[lsp-${this.toLevelLabel(level)}] ${message}`); + // Mention log level because until we switch to languageclient 10.x, the + // output channel will use the `info` level for all our messages. + // See https://github.com/microsoft/vscode-languageserver-node/issues/1116. + this.appendLine(level, `[lsp-${this.toLevelLabel(level)}] ${message}`); if (data) { - this.appendLine(LogFunctionLogger.data2String(data)); + this.appendLine(level, LogFunctionLogger.data2String(data)); } } @@ -134,10 +137,28 @@ export class LogFunctionLogger extends Disposable implements ILogger { } } - private appendLine(value: string): void { + private appendLine(level: LogLevel, value: string): void { // If we're connected, send log messages to client as LSP notifications if (this._connection) { - this._connection.console.log(value); + // The log level is not currently forwarded to our `LogOutputChannel` on + // the client side. We'll need to update to languageclient 10.x for this, + // see https://github.com/microsoft/vscode-languageserver-node/issues/1116. + switch (level) { + case LogLevel.Error: + this._connection.console.error(value); + break; + case LogLevel.Warn: + this._connection.console.warn(value); + break; + case LogLevel.Info: + this._connection.console.info(value); + break; + case LogLevel.Debug: + case LogLevel.Trace: + default: + this._connection.console.log(value); + break; + } } else { this._logFn(value); } From 48c8b28135f890e7e99449800e2f5627f834af6c Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 3 Jul 2025 09:34:30 +0200 Subject: [PATCH 11/18] Ignore format commit in git blame --- .git-blame-ignore-revs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..ba1235f3 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,8 @@ +# This file lists revisions of large-scale formatting/style changes so that +# they can be excluded from git blame results. +# +# To set this file as the default ignore file for git blame, run: +# $ git config blame.ignoreRevsFile .git-blame-ignore-revs + +# Format vscode extention and LSP files consistently (#748) +a1513effc913e6e59651256d79295da37134dbbf From 7840fb9baf151a580dc875d9ad516a56cdce0989 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 3 Jul 2025 09:45:42 +0200 Subject: [PATCH 12/18] Fix default setting --- apps/lsp/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/lsp/src/config.ts b/apps/lsp/src/config.ts index 7d1da492..e285fc4b 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -83,7 +83,7 @@ function defaultSettings(): Settings { colorTheme: 'Dark+' }, quarto: { - logLevel: LogLevel.Info, + logLevel: LogLevel.Warn, path: "", mathjax: { scale: 1, From d2a66b333be79e59970a7d437b2c488d9d42f1f3 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 3 Jul 2025 09:51:15 +0200 Subject: [PATCH 13/18] Revert propagation of log level to client --- apps/lsp/src/logging.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/apps/lsp/src/logging.ts b/apps/lsp/src/logging.ts index c777d814..0b5afcde 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -143,18 +143,8 @@ export class LogFunctionLogger extends Disposable implements ILogger { // The log level is not currently forwarded to our `LogOutputChannel` on // the client side. We'll need to update to languageclient 10.x for this, // see https://github.com/microsoft/vscode-languageserver-node/issues/1116. + // So just emit everything via `log` for now. switch (level) { - case LogLevel.Error: - this._connection.console.error(value); - break; - case LogLevel.Warn: - this._connection.console.warn(value); - break; - case LogLevel.Info: - this._connection.console.info(value); - break; - case LogLevel.Debug: - case LogLevel.Trace: default: this._connection.console.log(value); break; From 0c0e710b738ac2366a743530898bce4169a571a0 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 3 Jul 2025 14:03:42 +0200 Subject: [PATCH 14/18] Rename `LogFunctionLogger` to `Logger` --- apps/lsp/src/config.ts | 6 +++--- apps/lsp/src/index.ts | 4 ++-- apps/lsp/src/logging.ts | 9 ++++----- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/lsp/src/config.ts b/apps/lsp/src/config.ts index e285fc4b..cc0e78f7 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -28,7 +28,7 @@ import { ILogger, LogLevel } from './service'; -import { LogFunctionLogger } from './logging'; +import { Logger } from './logging'; export type ValidateEnabled = 'ignore' | 'warning' | 'error' | 'hint'; @@ -141,7 +141,7 @@ export class ConfigurationManager extends Disposable { } public init(logLevel?: string) { - const initLogLevel = LogFunctionLogger.parseLogLevel( + const initLogLevel = Logger.parseLogLevel( logLevel ?? "warn" ); @@ -164,7 +164,7 @@ export class ConfigurationManager extends Disposable { colorTheme: settings.workbench.colorTheme }, quarto: { - logLevel: LogFunctionLogger.parseLogLevel(settings.quarto.server.logLevel), + logLevel: Logger.parseLogLevel(settings.quarto.server.logLevel), path: settings.quarto.path, mathjax: { scale: settings.quarto.mathjax.scale, diff --git a/apps/lsp/src/index.ts b/apps/lsp/src/index.ts index f64db03f..8b9599e7 100644 --- a/apps/lsp/src/index.ts +++ b/apps/lsp/src/index.ts @@ -40,7 +40,7 @@ import { registerCustomMethods } from "./custom"; import { isWindows, LspConnection } from "core-node"; import { initQuartoContext, Document, markdownitParser, LspInitializationOptions } from "quarto-core"; import { ConfigurationManager, lsConfiguration } from "./config"; -import { LogFunctionLogger } from "./logging"; +import { Logger } from "./logging"; import { languageServiceWorkspace } from "./workspace"; import { middlewareCapabilities, middlewareRegister } from "./middleware"; import { createLanguageService, IMdLanguageService } from "./service"; @@ -53,7 +53,7 @@ import { registerDiagnostics } from "./diagnostics"; const connection = createConnection(ProposedFeatures.all); // Initialize logger -const logger = new LogFunctionLogger(console.log.bind(console)); +const logger = new Logger(console.log.bind(console)); // Create text document manager const documents: TextDocuments = new TextDocuments(TextDocument); diff --git a/apps/lsp/src/logging.ts b/apps/lsp/src/logging.ts index 0b5afcde..81812f55 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -25,8 +25,7 @@ import { ILogger, LogLevel } from "./service"; import { ConfigurationManager } from './config'; import { Connection } from 'vscode-languageserver'; -export class LogFunctionLogger extends Disposable implements ILogger { - +export class Logger extends Disposable implements ILogger { private static now(): string { const now = new Date(); return String(now.getUTCHours()).padStart(2, '0') @@ -66,10 +65,10 @@ export class LogFunctionLogger extends Disposable implements ILogger { this._config = config; this._register(this._config.onDidChangeConfiguration(() => { - this._logLevel = LogFunctionLogger.currentLogLevel(this._config!); + this._logLevel = Logger.currentLogLevel(this._config!); })); - this._logLevel = LogFunctionLogger.currentLogLevel(this._config); + this._logLevel = Logger.currentLogLevel(this._config); } private static currentLogLevel(config: ConfigurationManager): LogLevel { @@ -100,7 +99,7 @@ export class LogFunctionLogger extends Disposable implements ILogger { // See https://github.com/microsoft/vscode-languageserver-node/issues/1116. this.appendLine(level, `[lsp-${this.toLevelLabel(level)}] ${message}`); if (data) { - this.appendLine(level, LogFunctionLogger.data2String(data)); + this.appendLine(level, Logger.data2String(data)); } } From b1f1dd7ce9d779123a746d24a7e77bfc2216267c Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 3 Jul 2025 14:16:42 +0200 Subject: [PATCH 15/18] Add comment about default `console.log` handling --- apps/lsp/src/logging.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/lsp/src/logging.ts b/apps/lsp/src/logging.ts index 81812f55..1bf2bc10 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -149,6 +149,13 @@ export class Logger extends Disposable implements ILogger { break; } } else { + // Note that by default, languageserver redirects `console.log` to the + // client. However this is only the case with StdIo connections: + // https://github.com/microsoft/vscode-languageserver-node/blob/df56e720/server/src/node/main.ts#L262-L264 + // While we currently only use StdIo to connect the LSP, and so the branch + // above to explicitly log via our connection object is not strictly + // necessary, it's still better to use our own logger abstraction that we + // are in control of. this._logFn(value); } } From d9dc9d3b8c0c095556df89e9f68d6bb1dd4637dd Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Mon, 7 Jul 2025 11:00:02 +0200 Subject: [PATCH 16/18] Actually pass init option to logger --- apps/lsp/src/config.ts | 8 ++------ apps/lsp/src/index.ts | 6 +++++- apps/lsp/src/logging.ts | 6 +++++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/lsp/src/config.ts b/apps/lsp/src/config.ts index cc0e78f7..e34cf99b 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -140,16 +140,12 @@ export class ConfigurationManager extends Disposable { this._settings = defaultSettings(); } - public init(logLevel?: string) { - const initLogLevel = Logger.parseLogLevel( - logLevel ?? "warn" - ); - + public init(logLevel: LogLevel) { this._settings = { ...this._settings, quarto: { ...this._settings.quarto, - logLevel: initLogLevel, + logLevel, } }; } diff --git a/apps/lsp/src/index.ts b/apps/lsp/src/index.ts index 8b9599e7..bac4465d 100644 --- a/apps/lsp/src/index.ts +++ b/apps/lsp/src/index.ts @@ -75,7 +75,11 @@ let mdLs: IMdLanguageService | undefined; connection.onInitialize((params: InitializeParams) => { // Set log level from initialization options if provided so that we use the // expected level as soon as possible - configManager.init(params.initializationOptions?.logLevel); + const initLogLevel = Logger.parseLogLevel( + params.initializationOptions?.logLevel ?? "warn" + ); + logger.init(initLogLevel); + configManager.init(initLogLevel); // We're connected, log messages via LSP logger.setConnection(connection); diff --git a/apps/lsp/src/logging.ts b/apps/lsp/src/logging.ts index 1bf2bc10..c48fa109 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -46,7 +46,7 @@ export class Logger extends Disposable implements ILogger { return JSON.stringify(data, undefined, 2); } - private _logLevel: LogLevel; + private _logLevel = LogLevel.Warn; private _connection?: Connection; private _config?: ConfigurationManager; @@ -56,6 +56,10 @@ export class Logger extends Disposable implements ILogger { super(); } + init(logLevel: LogLevel): void { + this._logLevel = logLevel; + } + setConnection(connection: Connection) { this._connection = connection; this.logInfo('LSP is now connected'); From b260250243a6ca60fcea890d3b13c181b5882adf Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Mon, 7 Jul 2025 11:05:03 +0200 Subject: [PATCH 17/18] Remove justfile --- apps/vscode/justfile | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 apps/vscode/justfile diff --git a/apps/vscode/justfile b/apps/vscode/justfile deleted file mode 100644 index a5f5e7e4..00000000 --- a/apps/vscode/justfile +++ /dev/null @@ -1,8 +0,0 @@ -install: - rm -rf *.vsix && vsce package && code --install-extension *.vsix && positron --install-extension *.vsix - -install-vscode: - rm -rf *.vsix && vsce package && code --install-extension *.vsix - -install-positron: - rm -rf *.vsix && vsce package && positron --install-extension *.vsix From 11d3479baca651614ad8b520b919710c844f3cda Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Mon, 7 Jul 2025 12:15:22 +0200 Subject: [PATCH 18/18] Remove redundant config watching --- apps/lsp/src/config.ts | 5 +++-- apps/vscode/src/lsp/client.ts | 11 ----------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/apps/lsp/src/config.ts b/apps/lsp/src/config.ts index e34cf99b..7af97ba1 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -172,8 +172,9 @@ export class ConfigurationManager extends Disposable { } public async subscribe() { - // Ignore the settings in parameters, the modern usage is to fetch the settings - // when we get this notification + // Ignore the settings in parameters, the modern usage is to fetch the + // settings when we get this notification. This causes the client to send + // any updates for settings under the `quarto` section. this.connection_.onDidChangeConfiguration((_params) => { this._logger.logNotification('didChangeConfiguration'); this.update(); diff --git a/apps/vscode/src/lsp/client.ts b/apps/vscode/src/lsp/client.ts index f70cccd7..5cf9fca3 100644 --- a/apps/vscode/src/lsp/client.ts +++ b/apps/vscode/src/lsp/client.ts @@ -147,17 +147,6 @@ export async function activateLsp( clientOptions ); - // Notify LSP of quarto setting changes - context.subscriptions.push(workspace.onDidChangeConfiguration(e => { - if (client.state !== State.Running) { - return; - } - - if (e.affectsConfiguration("quarto")) { - client.sendNotification("workspace/didChangeConfiguration", {}); - } - })); - // return once the server is running return new Promise((resolve, reject) => {