diff --git a/examples/servers/.vscode/launch.json b/examples/servers/.vscode/launch.json new file mode 100644 index 00000000..f1241865 --- /dev/null +++ b/examples/servers/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + "configurations": [ + { + "name": "pygls: Debug Server", + "type": "python", + "request": "attach", + "connect": { + "host": "${config:pygls.server.debugHost}", + "port": "${config:pygls.server.debugPort}" + }, + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ], + "justMyCode": false + } + ] +} diff --git a/examples/servers/.vscode/settings.json b/examples/servers/.vscode/settings.json index 7064c02c..c2b0d23b 100644 --- a/examples/servers/.vscode/settings.json +++ b/examples/servers/.vscode/settings.json @@ -1,6 +1,9 @@ { // Uncomment to override Python interpreter used. // "pygls.server.pythonPath": "/path/to/python", + "pygls.server.debug": false, + // "pygls.server.debugHost": "localhost", + // "pygls.server.debugPort": 5678, "pygls.server.launchScript": "json_server.py", "pygls.trace.server": "off", "pygls.client.documentSelector": [ diff --git a/examples/vscode-playground/README.md b/examples/vscode-playground/README.md index 986f2b01..e73ed513 100644 --- a/examples/vscode-playground/README.md +++ b/examples/vscode-playground/README.md @@ -75,3 +75,10 @@ The `code_actions.py` example is intended to be used with text files (e.g. the p ``` You can find the full list of known language identifiers [here](https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers). + +#### Debugging the server + +To debug the language server set the `pygls.server.debug` option to `true`. +The server should be restarted and the debugger connect automatically. + +You can control the host and port that the debugger uses through the `pygls.server.debugHost` and `pygls.server.debugPort` options. diff --git a/examples/vscode-playground/package.json b/examples/vscode-playground/package.json index 64188334..9c6827d2 100644 --- a/examples/vscode-playground/package.json +++ b/examples/vscode-playground/package.json @@ -54,6 +54,24 @@ "description": "The working directory from which to launch the server.", "markdownDescription": "The working directory from which to launch the server.\nIf blank, this will default to the `examples/servers` directory." }, + "pygls.server.debug": { + "scope": "resource", + "default": false, + "type": "boolean", + "description": "Enable debugging of the server process." + }, + "pygls.server.debugHost": { + "scope": "resource", + "default": "localhost", + "type": "string", + "description": "The host on which the server process to debug is running." + }, + "pygls.server.debugPort": { + "scope": "resource", + "default": 5678, + "type": "integer", + "description": "The port number on which the server process to debug is listening." + }, "pygls.server.launchScript": { "scope": "resource", "type": "string", diff --git a/examples/vscode-playground/src/extension.ts b/examples/vscode-playground/src/extension.ts index 8a4c23f2..8952f39c 100644 --- a/examples/vscode-playground/src/extension.ts +++ b/examples/vscode-playground/src/extension.ts @@ -24,13 +24,12 @@ import * as vscode from "vscode"; import * as semver from "semver"; import { PythonExtension } from "@vscode/python-extension"; -import { LanguageClient, LanguageClientOptions, ServerOptions, State } from "vscode-languageclient/node"; +import { LanguageClient, LanguageClientOptions, ServerOptions, State, integer } from "vscode-languageclient/node"; const MIN_PYTHON = semver.parse("3.7.9") // Some other nice to haves. // TODO: Check selected env satisfies pygls' requirements - if not offer to run the select env command. -// TODO: Start a debug session for the currently configured server. // TODO: TCP Transport // TODO: WS Transport // TODO: Web Extension support (requires WASM-WASI!) @@ -144,7 +143,7 @@ async function startLangServer() { if (client) { await stopLangServer() } - + const config = vscode.workspace.getConfiguration("pygls.server") const cwd = getCwd() const serverPath = getServerPath() @@ -152,25 +151,33 @@ async function startLangServer() { logger.info(`server: '${serverPath}'`) const resource = vscode.Uri.joinPath(vscode.Uri.file(cwd), serverPath) - const pythonPath = await getPythonPath(resource) - if (!pythonPath) { + const pythonCommand = await getPythonCommand(resource) + if (!pythonCommand) { clientStarting = false return } + logger.debug(`python: ${pythonCommand.join(" ")}`) const serverOptions: ServerOptions = { - command: pythonPath, - args: [serverPath], + command: pythonCommand[0], + args: [...pythonCommand.slice(1), serverPath], options: { cwd }, }; client = new LanguageClient('pygls', serverOptions, getClientOptions()); - try { - await client.start() - clientStarting = false - } catch (err) { - clientStarting = false - logger.error(`Unable to start server: ${err}`) + const promises = [client.start()] + + if (config.get("debug")) { + promises.push(startDebugging()) + } + + const results = await Promise.allSettled(promises) + clientStarting = false + + for (const result of results) { + if (result.status === "rejected") { + logger.error(`There was a error starting the server: ${result.reason}`) + } } } @@ -187,6 +194,17 @@ async function stopLangServer(): Promise { client = undefined } +function startDebugging(): Promise { + if (!vscode.workspace.workspaceFolders) { + logger.error("Unable to start debugging, there is no workspace.") + return Promise.reject("Unable to start debugging, there is no workspace.") + } + // TODO: Is there a more reliable way to ensure the debug adapter is ready? + setTimeout(async () => { + await vscode.debug.startDebugging(vscode.workspace.workspaceFolders[0], "pygls: Debug Server") + }, 2000) +} + function getClientOptions(): LanguageClientOptions { const config = vscode.workspace.getConfiguration('pygls.client') const options = { @@ -270,7 +288,7 @@ function getCwd(): string { /** * - * @returns The python script to launch the server with + * @returns The python script that implements the server. */ function getServerPath(): string { const config = vscode.workspace.getConfiguration("pygls.server") @@ -279,13 +297,49 @@ function getServerPath(): string { } /** + * Return the python command to use when starting the server. + * + * If debugging is enabled, this will also included the arguments to required + * to wrap the server in a debug adapter. + * + * @returns The full python command needed in order to start the server. + */ +async function getPythonCommand(resource?: vscode.Uri): Promise { + const config = vscode.workspace.getConfiguration("pygls.server", resource) + const pythonPath = await getPythonInterpreter(resource) + if (!pythonPath) { + return + } + const command = [pythonPath] + const enableDebugger = config.get('debug') + + if (!enableDebugger) { + return command + } + + const debugHost = config.get('debugHost') + const debugPort = config.get('debugPort') + try { + const debugArgs = await python.debug.getRemoteLauncherCommand(debugHost, debugPort, true) + // Debugpy recommends we disable frozen modules + command.push("-Xfrozen_modules=off", ...debugArgs) + } catch (err) { + logger.error(`Unable to get debugger command: ${err}`) + logger.error("Debugger will not be available.") + } + + return command +} + +/** + * Return the python interpreter to use when starting the server. + * * This uses the official python extension to grab the user's currently * configured environment. * * @returns The python interpreter to use to launch the server */ -async function getPythonPath(resource?: vscode.Uri): Promise { - +async function getPythonInterpreter(resource?: vscode.Uri): Promise { const config = vscode.workspace.getConfiguration("pygls.server", resource) const pythonPath = config.get('pythonPath') if (pythonPath) {