Highly ambitious ATProtocol AppView service and sdks

Improve intellisense language server #6

This change: - Improves finding & filtering of lexicon files - Adds a logger for better debugging - Unsubscribes from the workspace events when enableValidation is false - Removes duplication between extension.ts and language-server.ts

I'd noticed an issue where if the workspace root was containing the lexicon files, then it was validating the .vscode configuration directory, and also files that were absolutely not lexicon files.

A future improvement may be to add configurable filtering of known "non-lexicon" files, like package.json, tsconfig.json, etc.

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:5w4eqcxzw5jv5qfnmzxcakfy/sh.tangled.repo.pull/3md22rv3upo22
+369 -156
Diff #0
+55 -67
packages/lexicon-intellisense/src/extension.ts
··· 1 - import * as vscode from 'vscode'; 2 - import * as path from 'path'; 3 - import { LexiconLanguageServer } from './language-server'; 1 + import * as vscode from "vscode"; 2 + import * as path from "path"; 3 + import { LexiconLanguageServer } from "./language-server"; 4 4 5 5 let languageServer: LexiconLanguageServer | undefined; 6 6 ··· 12 12 languageServer.start(); 13 13 14 14 // Register commands 15 - const validateCommand = vscode.commands.registerCommand('lexiconIntelliSense.validateFile', async () => { 16 - const activeEditor = vscode.window.activeTextEditor; 17 - if (!activeEditor) { 18 - vscode.window.showErrorMessage('No active editor found'); 19 - return; 20 - } 21 - 22 - const document = activeEditor.document; 23 - if (!isLexiconFile(document)) { 24 - vscode.window.showErrorMessage('Current file is not a lexicon JSON file'); 25 - return; 26 - } 27 - 28 - try { 29 - await languageServer?.validateDocument(document); 30 - vscode.window.showInformationMessage('Lexicon validation completed'); 31 - } catch (error) { 32 - vscode.window.showErrorMessage(`Validation failed: ${error instanceof Error ? error.message : String(error)}`); 33 - } 34 - }); 15 + const validateCommand = vscode.commands.registerCommand( 16 + "lexiconIntelliSense.validateFile", 17 + async () => { 18 + const activeEditor = vscode.window.activeTextEditor; 19 + if (!activeEditor) { 20 + vscode.window.showErrorMessage("No active editor found"); 21 + return; 22 + } 35 23 36 - const validateWorkspaceCommand = vscode.commands.registerCommand('lexiconIntelliSense.validateWorkspace', async () => { 37 - if (!languageServer) { 38 - vscode.window.showErrorMessage('Language server not initialized'); 39 - return; 40 - } 24 + const document = activeEditor.document; 25 + if (!languageServer?.isLexiconDocument(document)) { 26 + vscode.window.showErrorMessage( 27 + "Current file is not a lexicon JSON file", 28 + ); 29 + return; 30 + } 41 31 42 - try { 43 - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; 44 - if (!workspaceFolder) { 45 - vscode.window.showErrorMessage('No workspace folder found'); 32 + try { 33 + await languageServer?.validateDocument(document); 34 + vscode.window.showInformationMessage("Lexicon validation completed"); 35 + } catch (error) { 36 + vscode.window.showErrorMessage( 37 + `Validation failed: ${error instanceof Error ? error.message : String(error)}`, 38 + ); 39 + } 40 + }, 41 + ); 42 + 43 + const validateWorkspaceCommand = vscode.commands.registerCommand( 44 + "lexiconIntelliSense.validateWorkspace", 45 + async () => { 46 + if (!languageServer) { 47 + vscode.window.showErrorMessage("Language server not initialized"); 46 48 return; 47 49 } 48 50 49 - await languageServer.validateWorkspace(workspaceFolder.uri); 50 - vscode.window.showInformationMessage('Workspace lexicon validation completed'); 51 - } catch (error) { 52 - vscode.window.showErrorMessage(`Workspace validation failed: ${error instanceof Error ? error.message : String(error)}`); 53 - } 54 - }); 51 + try { 52 + if ( 53 + !vscode.workspace.workspaceFolders || 54 + vscode.workspace.workspaceFolders.length <= 0 55 + ) { 56 + vscode.window.showErrorMessage("No workspace folders found"); 57 + return; 58 + } 59 + 60 + await languageServer.validateWorkspaceFolders(); 61 + 62 + vscode.window.showInformationMessage( 63 + "Workspace lexicon validation completed", 64 + ); 65 + } catch (error) { 66 + vscode.window.showErrorMessage( 67 + `Workspace validation failed: ${error instanceof Error ? error.message : String(error)}`, 68 + ); 69 + } 70 + }, 71 + ); 55 72 56 73 context.subscriptions.push(validateCommand, validateWorkspaceCommand); 57 - 58 - // Watch for configuration changes 59 - const configWatcher = vscode.workspace.onDidChangeConfiguration((event) => { 60 - if (event.affectsConfiguration('lexiconIntelliSense')) { 61 - languageServer?.updateConfiguration(); 62 - } 63 - }); 64 - 65 - context.subscriptions.push(configWatcher); 66 74 } 67 75 68 76 export function deactivate() { ··· 71 79 languageServer = undefined; 72 80 } 73 81 } 74 - 75 - function isLexiconFile(document: vscode.TextDocument): boolean { 76 - // Check if file is in lexicons directory or has lexicon-like content 77 - const filePath = document.uri.fsPath; 78 - const isInLexiconsDir = filePath.includes('/lexicons/') || filePath.includes('\\lexicons\\'); 79 - 80 - if (isInLexiconsDir && document.languageId === 'json') { 81 - return true; 82 - } 83 - 84 - // Check if JSON content looks like a lexicon 85 - try { 86 - const content = JSON.parse(document.getText()); 87 - return content && typeof content === 'object' && 88 - typeof content.id === 'string' && 89 - content.defs; 90 - } catch { 91 - return false; 92 - } 93 - }
+234 -89
packages/lexicon-intellisense/src/language-server.ts
··· 1 1 import * as vscode from "vscode"; 2 2 import * as path from "path"; 3 - import * as fs from "fs"; 4 - import { 5 - LanguageClient, 6 - LanguageClientOptions, 7 - ServerOptions, 8 - TransportKind, 9 - } from "vscode-languageclient/node"; 10 - import { 11 - LexiconValidator, 12 - ValidationError, 13 - validate, 14 - validateWithDetails, 15 - type LexiconDoc, 16 - } from "./lexicon"; 3 + import * as fs from "fs/promises"; 4 + import { LanguageClient } from "vscode-languageclient/node"; 5 + import { validate, type LexiconDoc } from "./lexicon"; 6 + import Logger from "./logger"; 7 + 8 + type LexiconLanguageServerConfiguration = { 9 + enableValidation: boolean; 10 + lexiconDirectory: string; 11 + }; 12 + 13 + async function pathExists(path: string): Promise<boolean> { 14 + return await fs 15 + .access(path, fs.constants.F_OK) 16 + .then(() => true) 17 + .catch(() => false); 18 + } 19 + 20 + function normalizePath(inputPath: string) { 21 + return inputPath.replace("\\", path.sep).replace("/", path.sep); 22 + } 17 23 18 24 export class LexiconLanguageServer { 19 25 private client: LanguageClient | undefined; 20 26 private context: vscode.ExtensionContext; 21 27 private diagnosticCollection: vscode.DiagnosticCollection; 28 + private configuration: LexiconLanguageServerConfiguration; 29 + private logger: Logger; 30 + private workspaceSubscription?: vscode.Disposable; 22 31 23 32 constructor(context: vscode.ExtensionContext) { 24 33 this.context = context; 34 + this.logger = new Logger("lexicon"); 25 35 this.diagnosticCollection = 26 36 vscode.languages.createDiagnosticCollection("lexicon"); 27 37 context.subscriptions.push(this.diagnosticCollection); 38 + 39 + // Special disposable for disposing of workspace subscriptions if they exist: 40 + context.subscriptions.push({ 41 + dispose: () => { 42 + if (this.workspaceSubscription) { 43 + this.workspaceSubscription.dispose(); 44 + this.workspaceSubscription = undefined; 45 + } 46 + }, 47 + }); 48 + 49 + this.configuration = this.loadConfiguration(); 28 50 } 29 51 30 52 async start() { 31 - // Set up document change listeners for real-time validation 32 - const documentSelector = [ 33 - { scheme: "file", language: "json", pattern: "**/lexicons/**/*.json" }, 34 - ]; 53 + // Setup watcher for the configuration changing: 54 + const configWatcher = vscode.workspace.onDidChangeConfiguration( 55 + async (event) => { 56 + if (event.affectsConfiguration("lexiconIntelliSense")) { 57 + this.logger.debug("Reloading configuration"); 58 + this.loadConfiguration(); 59 + 60 + if (!this.configuration.enableValidation) { 61 + this.logger.info( 62 + "Lexicon validation disabled in config, set lexiconIntelliSense.enableValidation to true to enable", 63 + ); 64 + 65 + if (this.workspaceSubscription) { 66 + this.workspaceSubscription.dispose(); 67 + this.workspaceSubscription = undefined; 68 + } 69 + 70 + return; 71 + } else if (!this.workspaceSubscription) { 72 + this.workspaceSubscription = this.subscribeToWorkspaceEvents(); 73 + } 74 + 75 + // Revalidate all documents based on new configuration: 76 + await this.validateAllOpenDocuments(); 77 + 78 + // Revalidate the entire workspace: 79 + await this.validateWorkspaceFolders(); 80 + } 81 + }, 82 + ); 83 + 84 + this.context.subscriptions.push(configWatcher); 85 + 86 + if (!this.configuration.enableValidation) { 87 + this.logger.info( 88 + "Lexicon validation disabled in config, set lexiconIntelliSense.enableValidation to true to enable", 89 + ); 90 + 91 + return; 92 + } 93 + 94 + this.workspaceSubscription = this.subscribeToWorkspaceEvents(); 35 95 96 + // Validate all open documents: 97 + await this.validateAllOpenDocuments(); 98 + 99 + // Validate all lexicon documents in the workspace 100 + await this.validateWorkspaceFolders(); 101 + } 102 + 103 + stop() { 104 + if (this.client) { 105 + this.client.stop(); 106 + this.client = undefined; 107 + } 108 + } 109 + 110 + subscribeToWorkspaceEvents(): vscode.Disposable { 111 + this.logger.debug("Subscribing to workspace events"); 36 112 // Listen for document changes 37 113 const changeListener = vscode.workspace.onDidChangeTextDocument( 38 114 async (event) => { 39 - if (this.isLexiconDocument(event.document)) { 115 + if (this.canValidateDocument(event.document)) { 40 116 await this.validateDocument(event.document); 41 117 } 42 - } 118 + }, 43 119 ); 44 120 45 121 // Listen for document saves 46 122 const saveListener = vscode.workspace.onDidSaveTextDocument( 47 123 async (document) => { 48 - if (this.isLexiconDocument(document)) { 124 + if (this.canValidateDocument(document)) { 49 125 await this.validateDocument(document); 126 + 50 127 // Re-run workspace validation to check cross-references 51 - if (vscode.workspace.workspaceFolders) { 52 - for (const workspaceFolder of vscode.workspace.workspaceFolders) { 53 - await this.validateWorkspace(workspaceFolder.uri); 54 - } 55 - } 128 + await this.validateWorkspaceFolders(); 56 129 } 57 - } 130 + }, 58 131 ); 59 132 60 - this.context.subscriptions.push(changeListener, saveListener); 61 - 62 - // Validate all lexicon documents in the workspace 63 - if (vscode.workspace.workspaceFolders) { 64 - for (const workspaceFolder of vscode.workspace.workspaceFolders) { 65 - await this.validateWorkspace(workspaceFolder.uri); 66 - } 67 - } 133 + return vscode.Disposable.from(saveListener, changeListener, { 134 + dispose: () => { 135 + this.logger.debug("Unsubscribing from workspace events"); 136 + }, 137 + }); 68 138 } 69 139 70 - stop() { 71 - if (this.client) { 72 - this.client.stop(); 73 - this.client = undefined; 140 + loadConfiguration() { 141 + const config = vscode.workspace.getConfiguration("lexiconIntelliSense"); 142 + const enableValidation = !!config.get<boolean>("enableValidation", true); 143 + let lexiconDirectory = config.get<string>("lexiconDirectory", "/lexicons"); 144 + 145 + if (typeof lexiconDirectory !== "string" || lexiconDirectory.length === 0) { 146 + this.logger.warn( 147 + "Configuration value lexiconDirectory was not a string or was empty, using default: lexicons", 148 + ); 149 + lexiconDirectory = "/lexicons"; 74 150 } 151 + 152 + // Normalise to path configuration to match system path separator: 153 + lexiconDirectory = normalizePath(lexiconDirectory); 154 + 155 + this.configuration = { 156 + enableValidation, 157 + lexiconDirectory, 158 + }; 159 + 160 + return this.configuration; 75 161 } 76 162 77 - updateConfiguration() { 78 - // Revalidate all open documents when configuration changes 163 + async validateAllOpenDocuments() { 164 + this.logger.debug("Validating all open documents"); 165 + // Revalidate all open documents: 79 166 for (const document of vscode.workspace.textDocuments) { 80 - if (this.isLexiconDocument(document)) { 167 + if (this.canValidateDocument(document)) { 81 168 this.validateDocument(document); 82 169 } 83 170 } 84 171 } 85 172 86 - async validateDocument(document: vscode.TextDocument) { 87 - const config = vscode.workspace.getConfiguration("lexiconIntelliSense"); 88 - if (!config.get<boolean>("enableValidation", true)) { 89 - console.log("Validation disabled in config"); 90 - return; 173 + private canValidateDocument(document: vscode.TextDocument) { 174 + // Allow validation of new files that are JSON: 175 + if (document.isUntitled) { 176 + return document.isDirty && document.languageId === "json"; 177 + } else { 178 + // Allow validation of saved files: 179 + return this.isLexiconDocument(document); 91 180 } 181 + } 92 182 183 + async validateDocument(document: vscode.TextDocument) { 93 184 const diagnostics: vscode.Diagnostic[] = []; 94 185 95 186 try { ··· 102 193 this.createDiagnostic( 103 194 new vscode.Range(0, 0, 0, 0), 104 195 "Lexicon must be an object", 105 - vscode.DiagnosticSeverity.Error 106 - ) 196 + vscode.DiagnosticSeverity.Error, 197 + ), 107 198 ); 108 199 } else { 109 200 // Validate required fields ··· 112 203 this.createDiagnostic( 113 204 new vscode.Range(0, 0, 0, 0), 114 205 'Lexicon must have a valid "id" field', 115 - vscode.DiagnosticSeverity.Error 116 - ) 206 + vscode.DiagnosticSeverity.Error, 207 + ), 117 208 ); 118 209 } 119 210 ··· 122 213 this.createDiagnostic( 123 214 new vscode.Range(0, 0, 0, 0), 124 215 'Lexicon must have a "defs" object', 125 - vscode.DiagnosticSeverity.Error 126 - ) 216 + vscode.DiagnosticSeverity.Error, 217 + ), 127 218 ); 128 219 } 129 220 } ··· 136 227 ? parseError.message 137 228 : String(parseError) 138 229 }`, 139 - vscode.DiagnosticSeverity.Error 140 - ) 230 + vscode.DiagnosticSeverity.Error, 231 + ), 141 232 ); 142 233 } 143 234 144 - console.log("Setting diagnostics:", diagnostics.length, "errors found"); 235 + this.logger.debug( 236 + `Setting diagnostics: ${diagnostics.length} errors found`, 237 + ); 145 238 this.diagnosticCollection.set(document.uri, diagnostics); 146 239 } 147 240 241 + async validateWorkspaceFolders() { 242 + if (vscode.workspace.workspaceFolders) { 243 + for (const workspaceFolder of vscode.workspace.workspaceFolders) { 244 + await this.validateWorkspace(workspaceFolder.uri); 245 + } 246 + } 247 + } 248 + 148 249 async validateWorkspace(workspaceUri: vscode.Uri) { 149 - const config = vscode.workspace.getConfiguration("lexiconIntelliSense"); 150 - const lexiconDir = config.get<string>("lexiconDirectory", "lexicons"); 250 + const lexiconPath = path.resolve( 251 + workspaceUri.fsPath, 252 + this.configuration.lexiconDirectory, 253 + ); 151 254 152 - const lexiconPath = path.join(workspaceUri.fsPath, lexiconDir); 153 - if (!fs.existsSync(lexiconPath)) { 255 + if (!(await pathExists(lexiconPath))) { 154 256 // Silently return if lexicons directory doesn't exist - not all projects use lexicons 155 257 return; 156 258 } ··· 164 266 const document = await vscode.workspace.openTextDocument(uri); 165 267 await this.validateDocument(document); 166 268 } catch (error) { 167 - console.warn(`Failed to validate lexicon ${filePath}:`, error); 269 + this.logger.error(`Failed to validate lexicon ${filePath}:`, error); 168 270 } 169 271 } 170 272 ··· 174 276 175 277 for (const filePath of lexiconFiles) { 176 278 try { 177 - const content = fs.readFileSync(filePath, "utf8"); 279 + const content = await fs.readFile(filePath, "utf8"); 178 280 const lexicon = JSON.parse(content); 179 - console.log(lexicon.id, "loaded from", filePath); 281 + this.logger.debug(`${lexicon.id} loaded from ${filePath}`); 180 282 if (lexicon.id && lexicon.defs) { 181 283 lexicons.push(lexicon); 182 284 idToFileMap.set(lexicon.id, filePath); 183 285 } 184 286 } catch (error) { 185 - debugger; 186 - console.warn(`Failed to load lexicon ${filePath}:`, error); 287 + this.logger.error(`Failed to load lexicon ${filePath}`, error); 187 288 } 188 289 } 189 290 ··· 209 310 this.createDiagnostic( 210 311 new vscode.Range(0, 0, 0, 0), 211 312 error, 212 - vscode.DiagnosticSeverity.Error 213 - ) 313 + vscode.DiagnosticSeverity.Error, 314 + ), 214 315 ); 215 316 } 216 317 ··· 222 323 const totalErrors = errorsByLexiconId 223 324 ? Object.values(errorsByLexiconId).reduce( 224 325 (sum: number, errors) => sum + (errors as string[]).length, 225 - 0 326 + 0, 226 327 ) 227 328 : 0; 329 + 228 330 if (totalErrors === 0) { 229 - console.log( 230 - `Successfully validated ${lexicons.length} lexicon files` 331 + this.logger.info( 332 + `Successfully validated ${lexicons.length} lexicon files`, 231 333 ); 232 334 } else { 233 - const errorCount = errorsByLexiconId ? Object.keys(errorsByLexiconId).length : 0; 234 - console.log( 235 - `Validation found ${totalErrors} errors across ${errorCount} lexicons` 335 + const errorCount = errorsByLexiconId 336 + ? Object.keys(errorsByLexiconId).length 337 + : 0; 338 + 339 + this.logger.info( 340 + `Validation found ${totalErrors} errors across ${errorCount} lexicons`, 236 341 ); 237 342 } 238 343 } catch (error) { 239 - console.warn( 240 - `WASM validation failed: ${ 241 - error instanceof Error ? error.message : String(error) 242 - }` 243 - ); 344 + this.logger.error(`WASM validation failed`, error); 244 345 } 245 346 } 246 347 } ··· 248 349 private async findLexiconFiles(directory: string): Promise<string[]> { 249 350 const files: string[] = []; 250 351 251 - const readDir = async (dir: string) => { 252 - const entries = fs.readdirSync(dir, { withFileTypes: true }); 352 + this.logger.debug("finding lexicon files: " + directory); 353 + 354 + const readDir = async (dir: string, isRoot: boolean) => { 355 + const entries = await fs.readdir(dir, { withFileTypes: true }); 253 356 for (const entry of entries) { 254 357 const fullPath = path.join(dir, entry.name); 358 + 359 + // If we can't read the path, skip it: 360 + const isReadable = await pathExists(fullPath); 361 + if (!isReadable) { 362 + continue; 363 + } 364 + 255 365 if (entry.isDirectory()) { 256 - await readDir(fullPath); 257 - } else if (entry.name.endsWith(".json")) { 258 - files.push(fullPath); 366 + // ignore .vscode directory in the root, these are almost certainly 367 + // configuration files: 368 + if (entry.name === ".vscode" && isRoot) { 369 + continue; 370 + } 371 + 372 + // Don't recurse into the node_modules directory: 373 + if (entry.name === "node_modules") { 374 + continue; 375 + } 376 + 377 + await readDir(fullPath, false); 378 + } else if (entry.isFile()) { 379 + if (entry.name.endsWith(".json")) { 380 + files.push(fullPath); 381 + } 259 382 } 260 383 } 261 384 }; 262 385 263 - await readDir(directory); 386 + await readDir(directory, true); 387 + 264 388 return files; 265 389 } 266 390 267 - private isLexiconDocument(document: vscode.TextDocument): boolean { 391 + isLexiconFilePath(filePath: string): boolean { 392 + const normalized = normalizePath(filePath); 393 + if (!filePath.endsWith(".json")) { 394 + return false; 395 + } 396 + 397 + if ( 398 + this.configuration.lexiconDirectory !== "./" && 399 + this.configuration.lexiconDirectory !== "/" 400 + ) { 401 + // normalized is an absolute file path, so we're doing string inclusion, 402 + // rather than startsWith: 403 + return normalized.includes(this.configuration.lexiconDirectory); 404 + } 405 + 406 + return true; 407 + } 408 + 409 + isLexiconDocument(document: vscode.TextDocument): boolean { 268 410 const filePath = document.uri.fsPath; 269 - const isInLexiconsDir = 270 - filePath.includes("/lexicons/") || filePath.includes("\\lexicons\\"); 411 + if (document.languageId !== "json") { 412 + return false; 413 + } 271 414 272 - if (isInLexiconsDir && document.languageId === "json") { 415 + if (this.isLexiconFilePath(filePath)) { 273 416 return true; 274 417 } 275 418 ··· 279 422 return ( 280 423 content && 281 424 typeof content === "object" && 425 + typeof content.lexicon === "number" && 426 + content.lexicon === 1 && 282 427 typeof content.id === "string" && 283 428 content.defs 284 429 ); ··· 290 435 private createDiagnostic( 291 436 range: vscode.Range, 292 437 message: string, 293 - severity: vscode.DiagnosticSeverity 438 + severity: vscode.DiagnosticSeverity, 294 439 ): vscode.Diagnostic { 295 440 const diagnostic = new vscode.Diagnostic(range, message, severity); 296 441 diagnostic.source = "lexicon-intellisense";
+80
packages/lexicon-intellisense/src/logger.ts
··· 1 + // Based on MIT/Apache licensed file from: 2 + // https://github.com/biomejs/biome-vscode/blob/main/src/logger.ts 3 + import { type LogOutputChannel, window } from "vscode"; 4 + 5 + export default class Logger { 6 + /** 7 + * The output channel for logging messages. 8 + */ 9 + private outputChannel: LogOutputChannel; 10 + 11 + /** 12 + * Creates a new logger for the given Biome instance 13 + */ 14 + constructor(private readonly name: string) { 15 + this.outputChannel = window.createOutputChannel(name, { 16 + log: true, 17 + }); 18 + } 19 + 20 + public show(preserveFocus: boolean = false): void { 21 + this.outputChannel.show(preserveFocus); 22 + } 23 + 24 + /** 25 + * Logs a message to the output channel. 26 + * 27 + * @param message The message to log. 28 + */ 29 + public info(message: string): void { 30 + this.outputChannel?.info(` ${message}`); 31 + } 32 + 33 + /** 34 + * Logs an error message to the output channel. 35 + * 36 + * @param message The error message to log. 37 + */ 38 + public error(message?: string, error?: unknown): void { 39 + if (error !== undefined) { 40 + const errorMessage = 41 + error instanceof Error ? error.message : String(error); 42 + 43 + if (errorMessage) { 44 + this.outputChannel.error(` ${message ?? ""}:\n${errorMessage}`); 45 + return; 46 + } 47 + } 48 + 49 + if (message) { 50 + this.outputChannel.error(` ${message}`); 51 + } 52 + } 53 + 54 + /** 55 + * Logs a warning message to the output channel. 56 + * 57 + * @param message The warning message to log. 58 + */ 59 + public warn(message?: string): void { 60 + this.outputChannel.warn(message ?? ""); 61 + } 62 + 63 + /** 64 + * Logs a debug message to the output channel. 65 + * 66 + * @param message The debug message to log. 67 + */ 68 + public debug(message?: string): void { 69 + this.outputChannel.debug(message ?? ""); 70 + } 71 + 72 + /** 73 + * Logs a verbose message to the output channel. 74 + * 75 + * @param message The verbose message to log. 76 + */ 77 + public trace(message?: string): void { 78 + this.outputChannel.trace(message ?? ""); 79 + } 80 + }

History

1 round 0 comments
sign up or login to add to the discussion
1 commit
expand
lexicon-itellisense: Improve language server
no conflicts, ready to merge
expand 0 comments