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 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 a51cf1a2..7af97ba1 100644 --- a/apps/lsp/src/config.ts +++ b/apps/lsp/src/config.ts @@ -24,8 +24,11 @@ import { IncludeWorkspaceHeaderCompletions, LsConfiguration, defaultLsConfiguration, - PreferredMdPathExtensionStyle + PreferredMdPathExtensionStyle, + ILogger, + LogLevel } from './service'; +import { Logger } from './logging'; export type ValidateEnabled = 'ignore' | 'warning' | 'error' | 'hint'; @@ -34,6 +37,7 @@ export interface Settings { readonly colorTheme: string; }; readonly quarto: { + readonly logLevel: LogLevel; readonly path: string; readonly mathjax: { readonly scale: number; @@ -41,10 +45,6 @@ export interface Settings { } }; readonly markdown: { - readonly server: { - readonly log: 'off' | 'debug' | 'trace'; - }; - readonly preferredMdPathExtensionStyle: 'auto' | 'includeExtension' | 'removeExtension'; readonly suggest: { @@ -83,6 +83,7 @@ function defaultSettings(): Settings { colorTheme: 'Dark+' }, quarto: { + logLevel: LogLevel.Warn, path: "", mathjax: { scale: 1, @@ -90,9 +91,6 @@ function defaultSettings(): Settings { } }, markdown: { - server: { - log: 'off' - }, preferredMdPathExtensionStyle: 'auto', suggest: { paths: { @@ -131,13 +129,29 @@ 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(); } + public init(logLevel: LogLevel) { + this._settings = { + ...this._settings, + quarto: { + ...this._settings.quarto, + logLevel, + } + }; + } + public async update() { + this._logger.logTrace('Sending \'configuration\' request'); const settings = await this.connection_.workspace.getConfiguration(); this._settings = { @@ -146,6 +160,7 @@ export class ConfigurationManager extends Disposable { colorTheme: settings.workbench.colorTheme }, quarto: { + logLevel: Logger.parseLogLevel(settings.quarto.server.logLevel), path: settings.quarto.path, mathjax: { scale: settings.quarto.mathjax.scale, @@ -157,14 +172,22 @@ 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 causes the client to send + // any updates for settings under the `quarto` section. + 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.update(); - }); + + // Retrieve initial values for settings of interest + await this.update(); } public getSettings(): 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/index.ts b/apps/lsp/src/index.ts index 19cf7448..bac4465d 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"; @@ -52,12 +52,15 @@ import { registerDiagnostics } from "./diagnostics"; // Also include all preview / proposed LSP features. const connection = createConnection(ProposedFeatures.all); +// Initialize logger +const logger = new Logger(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 @@ -66,16 +69,29 @@ let capabilities: ClientCapabilities | undefined; // Initialization options let initializationOptions: LspInitializationOptions | undefined; -// Markdowdn language service +// Markdown language service 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 + const initLogLevel = Logger.parseLogLevel( + params.initializationOptions?.logLevel ?? "warn" + ); + logger.init(initLogLevel); + configManager.init(initLogLevel); + + // We're connected, log messages via LSP + logger.setConnection(connection); + logger.logRequest('initialize'); // alias options and capabilities initializationOptions = params.initializationOptions; capabilities = params.capabilities; connection.onCompletion(async (params, token): Promise => { + logger.logRequest('completion'); + const document = documents.get(params.textDocument.uri); if (!document) { return []; @@ -85,6 +101,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 +112,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 +122,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 +137,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 +147,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 +157,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 +172,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 +214,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 +241,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 +283,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..c48fa109 100644 --- a/apps/lsp/src/logging.ts +++ b/apps/lsp/src/logging.ts @@ -1,98 +1,166 @@ -/* - * 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'; +import { Connection } from 'vscode-languageserver'; + +export class Logger 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.Warn; + private _connection?: Connection; + private _config?: ConfigurationManager; + + constructor( + private readonly _logFn: typeof console.log, + ) { + super(); + } + + init(logLevel: LogLevel): void { + this._logLevel = logLevel; + } + + setConnection(connection: Connection) { + this._connection = connection; + this.logInfo('LSP is now connected'); + } + + setConfigurationManager(config: ConfigurationManager) { + this._config = config; + + this._register(this._config.onDidChangeConfiguration(() => { + this._logLevel = Logger.currentLogLevel(this._config!); + })); + + this._logLevel = Logger.currentLogLevel(this._config); + } + + 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 'info': return LogLevel.Info; + case 'warn': return LogLevel.Warn; + case 'error': return LogLevel.Error; + default: + return LogLevel.Warn; + } + } + + get level(): LogLevel { return this._logLevel; } + + public log(level: LogLevel, message: string, data?: unknown): void { + if (level < this.level) { + return; + } + + // 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(level, Logger.data2String(data)); + } + } + + 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 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 { + switch (level) { + 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'; + } + } + + private appendLine(level: LogLevel, value: string): void { + // If we're connected, send log messages to client as LSP notifications + if (this._connection) { + // 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) { + default: + this._connection.console.log(value); + 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); + } + } +} 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/lsp/src/service/logging.ts b/apps/lsp/src/service/logging.ts index a44b47ef..973ca2a9 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, } /** @@ -45,4 +51,22 @@ export interface ILogger { * @param data Additional information about what is being logged. */ 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, data?: Record): void; + + /** + * Log request at Trace level. + * @param method Message type name. + */ + 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()); } }; diff --git a/apps/vscode/package.json b/apps/vscode/package.json index 83c30344..1a69d34e 100644 --- a/apps/vscode/package.json +++ b/apps/vscode/package.json @@ -1306,6 +1306,19 @@ "type": "string" }, "uniqueItems": true + }, + "quarto.server.logLevel": { + "scope": "window", + "type": "string", + "default": "warn", + "enum": [ + "trace", + "debug", + "info", + "warn", + "error" + ], + "markdownDescription": "Log level for the Quarto language server." } } }, diff --git a/apps/vscode/src/lsp/client.ts b/apps/vscode/src/lsp/client.ts index 0799d3d8..5cf9fca3 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'; +}