import * as vscode from "vscode" import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from "vscode-languageclient/node" import { formatCdlFile, completionHandlerCdl, hoverCdlHandler, formatDefFile, isFirstLineMachine, diagnosticHandler, cdlDocumentSymbolProvider, defDocumentSymbolProvider, definitionCdlEventHandler } from "./common/handlers" import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging" import { checkVersion, getInterpreterDetails, initializePython, onDidChangePythonInterpreter, resolveInterpreter } from "./common/python" import { disposeServerResources, restartServer } from "./common/server" import { checkIfConfigurationChanged, getInterpreterFromSetting } from "./common/settings" import { loadServerDefaults } from "./common/setup" import { getLSClientTraceLevel } from "./common/utilities" import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi" import { registerNxTclDebugger } from "./debugger/register" let client: LanguageClient | undefined export async function activate(context: vscode.ExtensionContext) { registerNxTclDebugger(context) // This is required to get server name and module. This should be // the first thing that we do in this extension. const serverInfo = loadServerDefaults() const serverName = serverInfo.name const serverId = serverInfo.module const pythonDebugMode = process.env.USE_DEBUGPY?.toLowerCase() === "true" // Setup logging const outputChannel = createOutputChannel(serverName) context.subscriptions.push(outputChannel, registerLogger(outputChannel)) const changeLogLevel = async (c: vscode.LogLevel, g: vscode.LogLevel) => { const level = getLSClientTraceLevel(c, g) await client?.setTrace(level) } context.subscriptions.push( outputChannel.onDidChangeLogLevel(async (e) => { await changeLogLevel(e, vscode.env.logLevel) }), vscode.env.onDidChangeLogLevel(async (e) => { await changeLogLevel(outputChannel.logLevel, e) }) ) // Log Server information traceLog(`Name: ${serverInfo.name}`) traceLog(`Module: ${serverInfo.module}`) traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`) const runServerImpl = async () => { const interpreter = getInterpreterFromSetting(serverId) if (interpreter && interpreter.length > 0) { if (checkVersion(await resolveInterpreter(interpreter))) { traceVerbose( `Using interpreter from ${serverInfo.module}.interpreter: ${interpreter.join(" ")}` ) client = await restartServer(serverId, serverName, outputChannel, client) } return } const interpreterDetails = await getInterpreterDetails() if (interpreterDetails.path) { traceVerbose( `Using interpreter from Python extension: ${interpreterDetails.path.join(" ")}` ) client = await restartServer(serverId, serverName, outputChannel, client) return } traceError( "Python interpreter missing:\r\n" + "[Option 1] Select python interpreter using the ms-python.python.\r\n" + `[Option 2] Set an interpreter using "${serverId}.interpreter" setting.\r\n` + "Please use Python 3.8 or greater." ) } // Serialize server (re)starts. Overlapping triggers (interpreter change, // config change, restart command, initial activation) would otherwise each // read the stale module-level `client`, start a new server and leave the // previous one running orphaned -> hints/hover shown multiple times. let runServerQueue: Promise = Promise.resolve() const runServer = () => { runServerQueue = runServerQueue.catch(() => undefined).then(() => runServerImpl()) return runServerQueue } if (!pythonDebugMode) { context.subscriptions.push( onDidChangePythonInterpreter(async () => { await runServer() }) ) } context.subscriptions.push( onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => { if (checkIfConfigurationChanged(e, serverId)) { await runServer() } }), registerCommand(`${serverId}.restart`, async () => { await runServer() }) ) setImmediate(async () => { if (pythonDebugMode) { // A debugpy listen session is attached to exactly one process. Do not // subscribe to interpreter changes during startup, as the Python // extension can emit a duplicate event and restart that process. traceLog("Python debug mode: starting one stable server session") await runServer() return } const interpreter = getInterpreterFromSetting(serverId) if (interpreter === undefined || interpreter.length === 0) { traceLog(`Python extension loading`) await initializePython(context.subscriptions) traceLog(`Python extension loaded`) } else { await runServer() } }) // Folding ranges for TCL are provided by the language server // (folding_range_provider in lsp_server.py). No client-side provider here to // avoid duplicate folding regions. // const formatCdlProvider = vscode.languages.registerDocumentFormattingEditProvider( { scheme: "file", language: "cdl" }, { provideDocumentFormattingEdits(document: vscode.TextDocument): vscode.TextEdit[] { const text = document.getText() const formattedText = formatCdlFile(text) const fullRange = new vscode.Range( document.positionAt(0), document.positionAt(text.length) ) return [vscode.TextEdit.replace(fullRange, formattedText)] } } ) context.subscriptions.push(formatCdlProvider) const completionCdlProvider = vscode.languages.registerCompletionItemProvider( { scheme: "file", language: "cdl" }, { provideCompletionItems( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext ) { return completionHandlerCdl(document, position) } }, " " // Trigger completion on space ) context.subscriptions.push(completionCdlProvider) const hoverCdlProvider = vscode.languages.registerHoverProvider( { scheme: "file", language: "cdl" }, { provideHover( document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken ) { return hoverCdlHandler(document, position) } } ) context.subscriptions.push(hoverCdlProvider) const definitionCdlEventProvider = vscode.languages.registerDefinitionProvider( { scheme: "file", language: "cdl" }, { provideDefinition(document, position, token) { return definitionCdlEventHandler(document, position, token) } } ) context.subscriptions.push(definitionCdlEventProvider) const formatDefProvider = vscode.languages.registerDocumentFormattingEditProvider( { scheme: "file", language: "def" }, { provideDocumentFormattingEdits(document: vscode.TextDocument): vscode.TextEdit[] { const text = document.getText() const formattedText = formatDefFile(text) const fullRange = new vscode.Range( document.positionAt(0), document.positionAt(text.length) ) return [vscode.TextEdit.replace(fullRange, formattedText)] } } ) context.subscriptions.push(formatDefProvider) const cdlSymbolProvider = vscode.languages.registerDocumentSymbolProvider( { scheme: "file", language: "cdl" }, { provideDocumentSymbols(document: vscode.TextDocument) { return cdlDocumentSymbolProvider(document) } } ) context.subscriptions.push(cdlSymbolProvider) // Outline now provided by the language server (Document Symbols). Removing TS provider. const defSymbolProvider = vscode.languages.registerDocumentSymbolProvider( { scheme: "file", language: "def" }, { provideDocumentSymbols(document: vscode.TextDocument) { return defDocumentSymbolProvider(document) } } ) context.subscriptions.push(defSymbolProvider) // Diagnostics collection const diagnosticCollectionCdl = vscode.languages.createDiagnosticCollection("cdl") const diagnosticCollectionDef = vscode.languages.createDiagnosticCollection("def") context.subscriptions.push(diagnosticCollectionCdl, diagnosticCollectionDef) const diagnosticTimers = new Map>() const updateDiagnostics = (document: vscode.TextDocument) => { if (document.languageId === "cdl") { diagnosticCollectionCdl.set(document.uri, diagnosticHandler(document)) } else if (document.languageId === "def") { diagnosticCollectionDef.set(document.uri, diagnosticHandler(document)) } } const scheduleDiagnostics = (document: vscode.TextDocument) => { const key = document.uri.toString() const previous = diagnosticTimers.get(key) if (previous !== undefined) { clearTimeout(previous) } diagnosticTimers.set( key, setTimeout(() => { diagnosticTimers.delete(key) updateDiagnostics(document) }, 120) ) } // Check if the first line of the CDL file contains "MACHINE" context.subscriptions.push( vscode.workspace.onDidOpenTextDocument((document) => { if (document.languageId === "cdl" || document.languageId === "def") { updateDiagnostics(document) } }), vscode.workspace.onDidChangeTextDocument((event) => { const document = event.document if (document.languageId === "cdl" || document.languageId === "def") { scheduleDiagnostics(document) } }), vscode.workspace.onDidCloseTextDocument((document) => { const key = document.uri.toString() const timer = diagnosticTimers.get(key) if (timer !== undefined) { clearTimeout(timer) diagnosticTimers.delete(key) } diagnosticCollectionCdl.delete(document.uri) diagnosticCollectionDef.delete(document.uri) }), { dispose() { for (const timer of diagnosticTimers.values()) { clearTimeout(timer) } diagnosticTimers.clear() } } ) for (const document of vscode.workspace.textDocuments) { if (document.languageId === "cdl" || document.languageId === "def") { updateDiagnostics(document) } } } export function deactivate(): Thenable | undefined { if (!client) { disposeServerResources() return undefined } return client.stop().finally(disposeServerResources) }