diff --git a/client/package-lock.json b/client/package-lock.json index 20c97f8..f2fa6f6 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "@vscode/python-extension": "^1.0.5", + "fs-extra": "^11.3.0", "vscode-languageclient": "^9.0.1" }, "devDependencies": { @@ -36,6 +38,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@vscode/python-extension": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@vscode/python-extension/-/python-extension-1.0.5.tgz", + "integrity": "sha512-uYhXUrL/gn92mfqhjAwH2+yGOpjloBxj9ekoL4BhUsKcyJMpEg6WlNf3S3si+5x9zlbHHe7FYQNjZEbz1ymI9Q==", + "license": "MIT", + "engines": { + "node": ">=16.17.1", + "vscode": "^1.78.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -43,14 +55,46 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/minimatch": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", @@ -82,6 +126,15 @@ "dev": true, "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", diff --git a/client/package.json b/client/package.json index ef34fc0..4db362e 100644 --- a/client/package.json +++ b/client/package.json @@ -7,6 +7,8 @@ "vscode": "^1.96.0" }, "dependencies": { + "@vscode/python-extension": "^1.0.5", + "fs-extra": "^11.3.0", "vscode-languageclient": "^9.0.1" }, "devDependencies": { diff --git a/client/src/common/constants.ts b/client/src/common/constants.ts new file mode 100644 index 0000000..10f812f --- /dev/null +++ b/client/src/common/constants.ts @@ -0,0 +1,14 @@ +import * as path from "path" + +const folderName = path.basename(__dirname) +export const EXTENSION_ROOT_DIR = + folderName === "common" + ? path.dirname(path.dirname(path.dirname(__dirname))) + : path.dirname(path.dirname(__dirname)) +export const BUNDLED_PYTHON_SCRIPTS_DIR = path.join(EXTENSION_ROOT_DIR, "server") +export const SERVER_SCRIPT_PATH = path.join(BUNDLED_PYTHON_SCRIPTS_DIR, "src", `lsp_server.py`) +export const DEBUG_SERVER_SCRIPT_PATH = path.join( + BUNDLED_PYTHON_SCRIPTS_DIR, + "src", + `_debug_server.py` +) diff --git a/client/src/common/log/logging.ts b/client/src/common/log/logging.ts new file mode 100644 index 0000000..3885c9f --- /dev/null +++ b/client/src/common/log/logging.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as util from "util" +import { Disposable, LogOutputChannel } from "vscode" + +type Arguments = unknown[] +class OutputChannelLogger { + constructor(private readonly channel: LogOutputChannel) {} + + public traceLog(...data: Arguments): void { + this.channel.appendLine(util.format(...data)) + } + + public traceError(...data: Arguments): void { + this.channel.error(util.format(...data)) + } + + public traceWarn(...data: Arguments): void { + this.channel.warn(util.format(...data)) + } + + public traceInfo(...data: Arguments): void { + this.channel.info(util.format(...data)) + } + + public traceVerbose(...data: Arguments): void { + this.channel.debug(util.format(...data)) + } +} + +let channel: OutputChannelLogger | undefined +export function registerLogger(logChannel: LogOutputChannel): Disposable { + channel = new OutputChannelLogger(logChannel) + return { + dispose: () => { + channel = undefined + } + } +} + +export function traceLog(...args: Arguments): void { + channel?.traceLog(...args) +} + +export function traceError(...args: Arguments): void { + channel?.traceError(...args) +} + +export function traceWarn(...args: Arguments): void { + channel?.traceWarn(...args) +} + +export function traceInfo(...args: Arguments): void { + channel?.traceInfo(...args) +} + +export function traceVerbose(...args: Arguments): void { + channel?.traceVerbose(...args) +} diff --git a/client/src/common/python.ts b/client/src/common/python.ts new file mode 100644 index 0000000..f300201 --- /dev/null +++ b/client/src/common/python.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/naming-convention */ +import { commands, Disposable, Event, EventEmitter, Uri } from "vscode" +import { traceError, traceLog } from "./log/logging" +import { PythonExtension, ResolvedEnvironment } from "@vscode/python-extension" + +export interface IInterpreterDetails { + path?: string[] + resource?: Uri +} + +const onDidChangePythonInterpreterEvent = new EventEmitter() +export const onDidChangePythonInterpreter: Event = + onDidChangePythonInterpreterEvent.event + +let _api: PythonExtension | undefined +async function getPythonExtensionAPI(): Promise { + if (_api) { + return _api + } + _api = await PythonExtension.api() + return _api +} + +export async function initializePython(disposables: Disposable[]): Promise { + try { + const api = await getPythonExtensionAPI() + + if (api) { + disposables.push( + api.environments.onDidChangeActiveEnvironmentPath((e) => { + onDidChangePythonInterpreterEvent.fire({ + path: [e.path], + resource: e.resource?.uri + }) + }) + ) + + traceLog("Waiting for interpreter from python extension.") + onDidChangePythonInterpreterEvent.fire(await getInterpreterDetails()) + } + } catch (error) { + traceError("Error initializing python: ", error) + } +} + +export async function resolveInterpreter( + interpreter: string[] +): Promise { + const api = await getPythonExtensionAPI() + return api?.environments.resolveEnvironment(interpreter[0]) +} + +export async function getInterpreterDetails(resource?: Uri): Promise { + const api = await getPythonExtensionAPI() + const environment = await api?.environments.resolveEnvironment( + api?.environments.getActiveEnvironmentPath(resource) + ) + if (environment?.executable.uri && checkVersion(environment)) { + return { path: [environment?.executable.uri.fsPath], resource } + } + return { path: undefined, resource } +} + +export async function getDebuggerPath(): Promise { + const api = await getPythonExtensionAPI() + return api?.debug.getDebuggerPackagePath() +} + +export async function runPythonExtensionCommand(command: string, ...rest: any[]) { + await getPythonExtensionAPI() + return await commands.executeCommand(command, ...rest) +} + +export function checkVersion(resolved: ResolvedEnvironment | undefined): boolean { + const version = resolved?.version + if (version?.major === 3 && version?.minor >= 11) { + return true + } + traceError(`Python version ${version?.major}.${version?.minor} is not supported.`) + traceError(`Selected python path: ${resolved?.executable.uri?.fsPath}`) + traceError("Supported versions are 3.11 and above.") + return false +} diff --git a/client/src/common/server.ts b/client/src/common/server.ts new file mode 100644 index 0000000..a3dffc6 --- /dev/null +++ b/client/src/common/server.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fsapi from "fs-extra" +import { Disposable, env, LogOutputChannel } from "vscode" +import { State } from "vscode-languageclient" +import { + LanguageClient, + LanguageClientOptions, + RevealOutputChannelOn, + ServerOptions +} from "vscode-languageclient/node" +import { DEBUG_SERVER_SCRIPT_PATH, SERVER_SCRIPT_PATH } from "./constants" +import { traceError, traceInfo, traceVerbose } from "./log/logging" +import { getDebuggerPath } from "./python" +import { + getExtensionSettings, + getGlobalSettings, + getWorkspaceSettings, + ISettings +} from "./settings" +import { getLSClientTraceLevel, getProjectRoot } from "./utilities" +import { isVirtualWorkspace } from "./vscodeapi" + +export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings } + +async function createServer( + settings: ISettings, + serverId: string, + serverName: string, + outputChannel: LogOutputChannel, + initializationOptions: IInitOptions +): Promise { + const command = settings.interpreter[0] + const cwd = settings.cwd + + // Set debugger path needed for debugging python code. + const newEnv = { ...process.env } + const debuggerPath = await getDebuggerPath() + const isDebugScript = await fsapi.pathExists(DEBUG_SERVER_SCRIPT_PATH) + if (newEnv.USE_DEBUGPY && debuggerPath) { + newEnv.DEBUGPY_PATH = debuggerPath + } else { + newEnv.USE_DEBUGPY = "False" + } + + // Set import strategy + newEnv.LS_IMPORT_STRATEGY = settings.importStrategy + + // Set notification type + newEnv.LS_SHOW_NOTIFICATION = settings.showNotifications + + const args = + newEnv.USE_DEBUGPY === "False" || !isDebugScript + ? settings.interpreter.slice(1).concat([SERVER_SCRIPT_PATH]) + : settings.interpreter.slice(1).concat([DEBUG_SERVER_SCRIPT_PATH]) + traceInfo(`Server run command: ${[command, ...args].join(" ")}`) + + const serverOptions: ServerOptions = { + command, + args, + options: { cwd, env: newEnv } + } + + // Options to control the language client + const clientOptions: LanguageClientOptions = { + // Register the server for python documents + documentSelector: isVirtualWorkspace() + ? [{ language: "tcl" }] + : [ + { scheme: "file", language: "tcl" }, + { scheme: "untitled", language: "tcl" }, + { scheme: "vscode-notebook", language: "tcl" }, + { scheme: "vscode-notebook-cell", language: "tcl" } + ], + outputChannel: outputChannel, + traceOutputChannel: outputChannel, + revealOutputChannelOn: RevealOutputChannelOn.Never, + initializationOptions + } + + return new LanguageClient(serverId, serverName, serverOptions, clientOptions) +} + +let _disposables: Disposable[] = [] +export async function restartServer( + serverId: string, + serverName: string, + outputChannel: LogOutputChannel, + lsClient?: LanguageClient +): Promise { + if (lsClient) { + traceInfo(`Server: Stop requested`) + await lsClient.stop() + _disposables.forEach((d) => d.dispose()) + _disposables = [] + } + const projectRoot = await getProjectRoot() + const workspaceSetting = await getWorkspaceSettings(serverId, projectRoot, true) + + const newLSClient = await createServer(workspaceSetting, serverId, serverName, outputChannel, { + settings: await getExtensionSettings(serverId, true), + globalSettings: await getGlobalSettings(serverId, false) + }) + traceInfo(`Server: Start requested.`) + _disposables.push( + newLSClient.onDidChangeState((e) => { + switch (e.newState) { + case State.Stopped: + traceVerbose(`Server State: Stopped`) + break + case State.Starting: + traceVerbose(`Server State: Starting`) + break + case State.Running: + traceVerbose(`Server State: Running`) + break + } + }) + ) + try { + await newLSClient.start() + } catch (ex) { + traceError(`Server: Start failed: ${ex}`) + return undefined + } + + const level = getLSClientTraceLevel(outputChannel.logLevel, env.logLevel) + await newLSClient.setTrace(level) + return newLSClient +} diff --git a/client/src/common/settings.ts b/client/src/common/settings.ts new file mode 100644 index 0000000..b10e7ca --- /dev/null +++ b/client/src/common/settings.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + ConfigurationChangeEvent, + ConfigurationScope, + WorkspaceConfiguration, + WorkspaceFolder +} from "vscode" +import { getInterpreterDetails } from "./python" +import { getConfiguration, getWorkspaceFolders } from "./vscodeapi" + +export interface ISettings { + cwd: string + workspace: string + args: string[] + path: string[] + interpreter: string[] + importStrategy: string + showNotifications: string +} + +export function getExtensionSettings( + namespace: string, + includeInterpreter?: boolean +): Promise { + return Promise.all( + getWorkspaceFolders().map((w) => getWorkspaceSettings(namespace, w, includeInterpreter)) + ) +} + +function resolveVariables(value: string[], workspace?: WorkspaceFolder): string[] { + const substitutions = new Map() + const home = process.env.HOME || process.env.USERPROFILE + if (home) { + substitutions.set("${userHome}", home) + } + if (workspace) { + substitutions.set("${workspaceFolder}", workspace.uri.fsPath) + } + substitutions.set("${cwd}", process.cwd()) + getWorkspaceFolders().forEach((w) => { + substitutions.set("${workspaceFolder:" + w.name + "}", w.uri.fsPath) + }) + + return value.map((s) => { + for (const [key, value] of substitutions) { + s = s.replace(key, value) + } + return s + }) +} + +export function getInterpreterFromSetting(namespace: string, scope?: ConfigurationScope) { + const config = getConfiguration(namespace, scope) + return config.get("interpreter") +} + +export async function getWorkspaceSettings( + namespace: string, + workspace: WorkspaceFolder, + includeInterpreter?: boolean +): Promise { + const config = getConfiguration(namespace, workspace.uri) + + let interpreter: string[] = [] + if (includeInterpreter) { + interpreter = getInterpreterFromSetting(namespace, workspace) ?? [] + if (interpreter.length === 0) { + interpreter = (await getInterpreterDetails(workspace.uri)).path ?? [] + } + } + + const workspaceSetting = { + cwd: workspace.uri.fsPath, + workspace: workspace.uri.toString(), + args: resolveVariables(config.get(`args`) ?? [], workspace), + path: resolveVariables(config.get(`path`) ?? [], workspace), + interpreter: resolveVariables(interpreter, workspace), + importStrategy: config.get(`importStrategy`) ?? "useBundled", + showNotifications: config.get(`showNotifications`) ?? "off" + } + return workspaceSetting +} + +function getGlobalValue(config: WorkspaceConfiguration, key: string, defaultValue: T): T { + const inspect = config.inspect(key) + return inspect?.globalValue ?? inspect?.defaultValue ?? defaultValue +} + +export async function getGlobalSettings( + namespace: string, + includeInterpreter?: boolean +): Promise { + const config = getConfiguration(namespace) + + let interpreter: string[] = [] + if (includeInterpreter) { + interpreter = getGlobalValue(config, "interpreter", []) + if (interpreter === undefined || interpreter.length === 0) { + interpreter = (await getInterpreterDetails()).path ?? [] + } + } + + const setting = { + cwd: process.cwd(), + workspace: process.cwd(), + args: getGlobalValue(config, "args", []), + path: getGlobalValue(config, "path", []), + interpreter: interpreter, + importStrategy: getGlobalValue(config, "importStrategy", "useBundled"), + showNotifications: getGlobalValue(config, "showNotifications", "off") + } + return setting +} + +export function checkIfConfigurationChanged( + e: ConfigurationChangeEvent, + namespace: string +): boolean { + const settings = [ + `${namespace}.args`, + `${namespace}.path`, + `${namespace}.interpreter`, + `${namespace}.importStrategy`, + `${namespace}.showNotifications` + ] + const changed = settings.map((s) => e.affectsConfiguration(s)) + return changed.includes(true) +} diff --git a/client/src/common/setup.ts b/client/src/common/setup.ts new file mode 100644 index 0000000..d6c32e8 --- /dev/null +++ b/client/src/common/setup.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from "path" +import * as fs from "fs-extra" +import { EXTENSION_ROOT_DIR } from "./constants" + +export interface IServerInfo { + name: string + module: string +} + +export function loadServerDefaults(): IServerInfo { + const packageJson = path.join(EXTENSION_ROOT_DIR, "package.json") + const content = fs.readFileSync(packageJson).toString() + const config = JSON.parse(content) + return config.serverInfo as IServerInfo +} diff --git a/client/src/common/utilities.ts b/client/src/common/utilities.ts new file mode 100644 index 0000000..74d4e37 --- /dev/null +++ b/client/src/common/utilities.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from "fs-extra" +import * as path from "path" +import { LogLevel, Uri, WorkspaceFolder } from "vscode" +import { Trace } from "vscode-jsonrpc/node" +import { getWorkspaceFolders } from "./vscodeapi" + +function logLevelToTrace(logLevel: LogLevel): Trace { + switch (logLevel) { + case LogLevel.Error: + case LogLevel.Warning: + case LogLevel.Info: + return Trace.Messages + + case LogLevel.Debug: + case LogLevel.Trace: + return Trace.Verbose + + case LogLevel.Off: + default: + return Trace.Off + } +} + +export function getLSClientTraceLevel(channelLogLevel: LogLevel, globalLogLevel: LogLevel): Trace { + if (channelLogLevel === LogLevel.Off) { + return logLevelToTrace(globalLogLevel) + } + if (globalLogLevel === LogLevel.Off) { + return logLevelToTrace(channelLogLevel) + } + const level = logLevelToTrace( + channelLogLevel <= globalLogLevel ? channelLogLevel : globalLogLevel + ) + return level +} + +export async function getProjectRoot(): Promise { + const workspaces: readonly WorkspaceFolder[] = getWorkspaceFolders() + if (workspaces.length === 0) { + return { + uri: Uri.file(process.cwd()), + name: path.basename(process.cwd()), + index: 0 + } + } else if (workspaces.length === 1) { + return workspaces[0] + } else { + let rootWorkspace = workspaces[0] + let root = undefined + for (const w of workspaces) { + if (await fs.pathExists(w.uri.fsPath)) { + root = w.uri.fsPath + rootWorkspace = w + break + } + } + + for (const w of workspaces) { + if (root && root.length > w.uri.fsPath.length && (await fs.pathExists(w.uri.fsPath))) { + root = w.uri.fsPath + rootWorkspace = w + } + } + return rootWorkspace + } +} diff --git a/client/src/common/vscodeapi.ts b/client/src/common/vscodeapi.ts new file mode 100644 index 0000000..8304022 --- /dev/null +++ b/client/src/common/vscodeapi.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + commands, + ConfigurationScope, + Disposable, + LogOutputChannel, + Uri, + window, + workspace, + WorkspaceConfiguration, + WorkspaceFolder +} from "vscode" + +export function createOutputChannel(name: string): LogOutputChannel { + return window.createOutputChannel(name, { log: true }) +} + +export function getConfiguration( + config: string, + scope?: ConfigurationScope +): WorkspaceConfiguration { + return workspace.getConfiguration(config, scope) +} + +export function registerCommand( + command: string, + callback: (...args: any[]) => any, + thisArg?: any +): Disposable { + return commands.registerCommand(command, callback, thisArg) +} + +export const { onDidChangeConfiguration } = workspace + +export function isVirtualWorkspace(): boolean { + const isVirtual = + workspace.workspaceFolders && + workspace.workspaceFolders.every((f) => f.uri.scheme !== "file") + return !!isVirtual +} + +export function getWorkspaceFolders(): readonly WorkspaceFolder[] { + return workspace.workspaceFolders ?? [] +} + +export function getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined { + return workspace.getWorkspaceFolder(uri) +} diff --git a/client/src/extension.ts b/client/src/extension.ts index ec5d8a1..0474159 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -14,43 +14,105 @@ import { diagnosticHandler, tclDocumentSymbolProvider } from "./common/handlers" -import * as path from "path" +import { registerLogger, traceError, traceLog, traceVerbose } from "./common/log/logging" +import { + checkVersion, + getInterpreterDetails, + initializePython, + onDidChangePythonInterpreter, + resolveInterpreter +} from "./common/python" +import { 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" let client: LanguageClient -export function activate(context: vscode.ExtensionContext) { - // The server is implemented in node - const serverModule = context.asAbsolutePath(path.join("out", "server", "src", "server.js")) - console.log(context.asAbsolutePath(path.join("server", "src", "server.js"))) - // If the extension is launched in debug mode then the debug server options are used - // Otherwise the run options are used - const serverOptions: ServerOptions = { - run: { module: serverModule, transport: TransportKind.ipc }, - debug: { - module: serverModule, - transport: TransportKind.ipc - } - } - // Options to control the language client - const clientOptions: LanguageClientOptions = { - // Register the server for plain text documents - documentSelector: [{ scheme: "file", language: "tcl" }], - synchronize: { - // Notify the server about file changes to '.clientrc files contained in the workspace - fileEvents: vscode.workspace.createFileSystemWatcher("**/.clientrc") - } +export async function activate(context: vscode.ExtensionContext) { + // 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 + + // 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) } - // Create the language client and start the client. - client = new LanguageClient( - "languageServerExample", - "Language Server Example", - serverOptions, - clientOptions + context.subscriptions.push( + outputChannel.onDidChangeLogLevel(async (e) => { + await changeLogLevel(e, vscode.env.logLevel) + }), + vscode.env.onDidChangeLogLevel(async (e) => { + await changeLogLevel(outputChannel.logLevel, e) + }) ) - // Start the client. This will also launch the server - client.start() + // Log Server information + traceLog(`Name: ${serverInfo.name}`) + traceLog(`Module: ${serverInfo.module}`) + traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`) + + const runServer = 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." + ) + } + + context.subscriptions.push( + onDidChangePythonInterpreter(async () => { + await runServer() + }), + onDidChangeConfiguration(async (e: vscode.ConfigurationChangeEvent) => { + if (checkIfConfigurationChanged(e, serverId)) { + await runServer() + } + }), + registerCommand(`${serverId}.restart`, async () => { + await runServer() + }) + ) + + setImmediate(async () => { + 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() + } + }) // const formatCdlProvider = vscode.languages.registerDocumentFormattingEditProvider( diff --git a/server/.python-version b/server/.python-version new file mode 100644 index 0000000..2c07333 --- /dev/null +++ b/server/.python-version @@ -0,0 +1 @@ +3.11 diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..e69de29 diff --git a/server/noxfile.py b/server/noxfile.py new file mode 100644 index 0000000..82f0645 --- /dev/null +++ b/server/noxfile.py @@ -0,0 +1,26 @@ +"""All the action we need during build""" + +import json +import os +import pathlib +import urllib.request as url_lib +from typing import List + +import nox # pylint: disable=import-error + + +@nox.session() +def setup(session: nox.Session) -> None: + """Sets up the template for development.""" + + session.install( + "-t", + "./bundled/libs", + "--no-cache-dir", + "--implementation", + "py", + "--no-deps", + "--upgrade", + "-r", + "./requirements.txt", + ) diff --git a/server/package-lock.json b/server/package-lock.json deleted file mode 100644 index 69c1324..0000000 --- a/server/package-lock.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "name": "lsp-sample-server", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "lsp-sample-server", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "chevrotain": "^11.0.3", - "vscode-languageserver": "^9.0.1", - "vscode-languageserver-textdocument": "^1.0.12" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", - "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "11.0.3", - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/@chevrotain/gast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", - "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/types": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", - "license": "Apache-2.0" - }, - "node_modules/chevrotain": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", - "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/cst-dts-gen": "11.0.3", - "@chevrotain/gast": "11.0.3", - "@chevrotain/regexp-to-ast": "11.0.3", - "@chevrotain/types": "11.0.3", - "@chevrotain/utils": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageserver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "license": "MIT" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT" - } - } -} diff --git a/server/package.json b/server/package.json deleted file mode 100644 index 700c1e7..0000000 --- a/server/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "lsp-sample-server", - "description": "Example implementation of a language server in node.", - "version": "1.0.0", - "author": "Microsoft Corporation", - "license": "MIT", - "engines": { - "node": "*" - }, - "dependencies": { - "chevrotain": "^11.0.3", - "vscode-languageserver": "^9.0.1", - "vscode-languageserver-textdocument": "^1.0.12" - }, - "scripts": {} -} diff --git a/server/pyproject.toml b/server/pyproject.toml new file mode 100644 index 0000000..b54b66b --- /dev/null +++ b/server/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "server" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "pygls","ply" +] diff --git a/server/src/_debug_server.py b/server/src/_debug_server.py new file mode 100644 index 0000000..c4c6852 --- /dev/null +++ b/server/src/_debug_server.py @@ -0,0 +1,39 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +"""Debugging support for LSP.""" + +import os +import pathlib +import runpy +import sys + + +def update_sys_path(path_to_add: str) -> None: + """Add given path to `sys.path`.""" + if path_to_add not in sys.path and os.path.isdir(path_to_add): + sys.path.append(path_to_add) + + +# Ensure debugger is loaded before we load anything else, to debug initialization. +debugger_path = os.getenv("DEBUGPY_PATH", None) +if debugger_path: + if debugger_path.endswith("debugpy"): + debugger_path = os.fspath(pathlib.Path(debugger_path).parent) + + update_sys_path(debugger_path) + + # pylint: disable=wrong-import-position,import-error + import debugpy + + # 5678 is the default port, If you need to change it update it here + # and in launch.json. + debugpy.connect(5678) + + # This will ensure that execution is paused as soon as the debugger + # connects to VS Code. If you don't want to pause here comment this + # line and set breakpoints as appropriate. + debugpy.breakpoint() + +SERVER_PATH = os.fspath(pathlib.Path(__file__).parent / "lsp_server.py") +# NOTE: Set breakpoint in `lsp_server.py` before continuing. +runpy.run_path(SERVER_PATH, run_name="__main__") diff --git a/server/src/lsp_server.py b/server/src/lsp_server.py new file mode 100644 index 0000000..074d769 --- /dev/null +++ b/server/src/lsp_server.py @@ -0,0 +1,32 @@ +"""Implementation of tool support over LSP.""" + +from __future__ import annotations + +import copy +import json +import os +import pathlib +import re +import sys +import sysconfig +import traceback +from typing import Any, Optional, Sequence + + +# ********************************************************** +# Update sys.path before importing any bundled libraries. +# ********************************************************** +def update_sys_path(path_to_add: str, strategy: str) -> None: + """Add given path to `sys.path`.""" + if path_to_add not in sys.path and os.path.isdir(path_to_add): + if strategy == "useBundled": + sys.path.insert(0, path_to_add) + elif strategy == "fromEnvironment": + sys.path.append(path_to_add) + + +# Ensure that we can import LSP libraries, and other bundled libraries. +update_sys_path( + os.fspath(pathlib.Path(__file__).parent.parent / "libs"), + os.getenv("LS_IMPORT_STRATEGY", "useBundled"), +) diff --git a/server/src/parser.ts b/server/src/parser.ts deleted file mode 100644 index 8b13789..0000000 --- a/server/src/parser.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/server/src/server.ts b/server/src/server.ts deleted file mode 100644 index 1be5387..0000000 --- a/server/src/server.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - createConnection, - TextDocuments, - ProposedFeatures, - TextDocumentSyncKind, - InitializeParams, - InitializeResult, - CompletionItemKind, - TextDocumentPositionParams, - CompletionItem, - TextEdit, - Range, - Position -} from "vscode-languageserver/node" -import { TextDocument } from "vscode-languageserver-textdocument" - -// Create a connection for the server, using Node's IPC as a transport. -// Also include all preview / proposed LSP features. -let connection = createConnection(ProposedFeatures.all) - -// Create a simple text document manager. -let documents: TextDocuments = new TextDocuments(TextDocument) - -connection.onInitialize((params: InitializeParams) => { - let capabilities = params.capabilities - - // Does the client support the `workspace/configuration` request? - // If not, we fall back using global settings. - const result: InitializeResult = { - capabilities: { - textDocumentSync: TextDocumentSyncKind.Incremental, - // Tell the client that this server supports code completion. - completionProvider: { - resolveProvider: false - } - } - } - return result -}) - -// This handler provides the initial list of the completion items. -connection.onCompletion((params: TextDocumentPositionParams): CompletionItem[] => { - const doc = documents.get(params.textDocument.uri) - if (!doc) return [] - - const text = doc.getText() - - return [] -}) - -connection.onDocumentFormatting(async (params, token) => { - const document = documents.get(params.textDocument.uri) - if (!document) return [] - - const originalText = document.getText() - - return [] -}) - -documents.onDidChangeContent(async (change) => { - connection.console.log("Document has changed") -}) - -// Make the text document manager listen on the connection -// for open, change and close text document events -documents.listen(connection) - -// Listen on the connection -connection.listen() diff --git a/server/tsconfig.json b/server/tsconfig.json deleted file mode 100644 index eb314f7..0000000 --- a/server/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "target": "es2020", - "lib": ["es2020"], - "module": "commonjs", - "moduleResolution": "node", - "sourceMap": true, - "strict": true, - "outDir": "out", - "rootDir": "src" - }, - "include": ["src"], - "exclude": ["node_modules", ".vscode-test"] -} diff --git a/server/uv.lock b/server/uv.lock new file mode 100644 index 0000000..4af21f5 --- /dev/null +++ b/server/uv.lock @@ -0,0 +1,83 @@ +version = 1 +requires-python = ">=3.11" + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "cattrs" +version = "25.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/2b/561d78f488dcc303da4639e02021311728fb7fda8006dd2835550cddd9ed/cattrs-25.1.1.tar.gz", hash = "sha256:c914b734e0f2d59e5b720d145ee010f1fd9a13ee93900922a2f3f9d593b8382c", size = 435016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/b0/215274ef0d835bbc1056392a367646648b6084e39d489099959aefcca2af/cattrs-25.1.1-py3-none-any.whl", hash = "sha256:1b40b2d3402af7be79a7e7e097a9b4cd16d4c06e6d526644b0b26a063a1cc064", size = 69386 }, +] + +[[package]] +name = "lsprotocol" +version = "2023.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/f6/6e80484ec078d0b50699ceb1833597b792a6c695f90c645fbaf54b947e6f/lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d", size = 69434 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/37/2351e48cb3309673492d3a8c59d407b75fb6630e560eb27ecd4da03adc9a/lsprotocol-2023.0.1-py3-none-any.whl", hash = "sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2", size = 70826 }, +] + +[[package]] +name = "ply" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567 }, +] + +[[package]] +name = "pygls" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cattrs" }, + { name = "lsprotocol" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/b9/41d173dad9eaa9db9c785a85671fc3d68961f08d67706dc2e79011e10b5c/pygls-1.3.1.tar.gz", hash = "sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018", size = 45527 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/19/b74a10dd24548e96e8c80226cbacb28b021bc3a168a7d2709fb0d0185348/pygls-1.3.1-py3-none-any.whl", hash = "sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e", size = 56031 }, +] + +[[package]] +name = "server" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "ply" }, + { name = "pygls" }, +] + +[package.metadata] +requires-dist = [ + { name = "ply" }, + { name = "pygls" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, +]