diff --git a/package.json b/package.json index ff66019..7bb2cb5 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "name": "vscode-jsonnet", "icon": "icon.png", "displayName": "Jsonnet Language Server", - "description": "Full code support (formatting, highlighting, navigation, etc) for Jsonnet", + "description": "Full code support (formatting, highlighting, navigation, debugging etc) for Jsonnet", "license": "Apache License Version 2.0", "publisher": "Grafana", - "version": "0.5.1", + "version": "0.6.0", "repository": { "type": "git", "url": "https://github.com/grafana/vscode-jsonnet" @@ -16,13 +16,15 @@ "categories": [ "Programming Languages", "Linters", - "Formatters" + "Formatters", + "Debuggers" ], "keywords": [ "jsonnet", "grafana", "lsp", - "language" + "language", + "debugger" ], "activationEvents": [ "onLanguage:jsonnet" @@ -44,6 +46,13 @@ "group": "navigation", "when": "resourceLangId == jsonnet" } + ], + "editor/title/run": [ + { + "command": "jsonnet.debugEditorContents", + "when": "resourceLangId == jsonnet", + "group": "navigation@1" + } ] }, "commands": [ @@ -77,6 +86,13 @@ { "command": "jsonnet.restartLanguageServer", "title": "Jsonnet: Restart Language Server" + }, + { + "command": "jsonnet.debugEditorContents", + "title": "Jsonnet: Debug File", + "category": "Jsonnet", + "enablement": "!inDebugMode", + "icon": "$(debug-alt)" } ], "languages": [ @@ -100,6 +116,54 @@ "path": "./language/jsonnet.tmLanguage.json" } ], + "breakpoints": [ + { + "language": "jsonnet" + } + ], + "debuggers": [ + { + "type": "jsonnet", + "languages": [ + "jsonnet" + ], + "label": "Jsonnet Debugger", + "configurationAttributes": { + "launch": { + "required": [ + "program", + "jpaths" + ], + "properties": { + "program": { + "type": "string", + "description": "jsonnet script to run" + }, + "jpaths": { + "type": "array", + "description": "jsonnet search paths", + "items": { + "type": "string" + } + } + } + } + }, + "initialConfigurations": [], + "configurationSnippets": [ + { + "label": "Jsonnet: Debug current file", + "description": "A new configuration for debugging a Jsonnet file.", + "body": { + "type": "jsonnet", + "request": "launch", + "name": "Debug current JSONNET file", + "program": "^\"\\${file}\"" + } + } + ] + } + ], "configuration": { "type": "object", "title": "Jsonnet Language Server", @@ -221,6 +285,21 @@ ], "default": "info", "description": "Log level for the language server" + }, + "jsonnet.debugger.releaseRepository": { + "type": "string", + "default": "grafana/jsonnet-debugger", + "description": "Github repository to download the debugger server from" + }, + "jsonnet.debugger.enableAutoUpdate": { + "scope": "resource", + "type": "boolean", + "default": true + }, + "jsonnet.debugger.pathToBinary": { + "scope": "resource", + "type": "string", + "description": "Path to debugger" } } } diff --git a/src/debugger.ts b/src/debugger.ts new file mode 100644 index 0000000..b868471 --- /dev/null +++ b/src/debugger.ts @@ -0,0 +1,18 @@ +import * as vscode from 'vscode'; + +export class JsonnetDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { + context: vscode.ExtensionContext; + binPath: string; + + constructor(context: vscode.ExtensionContext, binPath: string) { + this.context = context; + this.binPath = binPath; + } + + createDebugAdapterDescriptor( + session: vscode.DebugSession, + executable: vscode.DebugAdapterExecutable | undefined + ): vscode.ProviderResult { + return new vscode.DebugAdapterExecutable(this.binPath, ['-d', '-s']); + } +} diff --git a/src/extension.ts b/src/extension.ts index 36e5cc3..f55463d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,19 @@ import * as path from 'path'; -import { commands, window, workspace, ExtensionContext, Uri, OutputChannel, TextEditor, ViewColumn } from 'vscode'; +import { + commands, + debug, + window, + workspace, + ExtensionContext, + Uri, + OutputChannel, + TextEditor, + ViewColumn, + ProviderResult, + WorkspaceFolder, + DebugConfiguration, + DebugConfigurationProviderTriggerKind, +} from 'vscode'; import * as fs from 'fs'; import * as os from 'os'; import { stringify as stringifyYaml } from 'yaml'; @@ -14,6 +28,7 @@ import { ServerOptions, } from 'vscode-languageclient/node'; import { install } from './install'; +import { JsonnetDebugAdapterDescriptorFactory } from './debugger'; let extensionContext: ExtensionContext; let client: LanguageClient; @@ -24,7 +39,40 @@ export async function activate(context: ExtensionContext): Promise { extensionContext = context; await startClient(); + await installDebugger(context); await didChangeConfigHandler(); + context.subscriptions.push( + debug.registerDebugConfigurationProvider( + 'jsonnet', + { + provideDebugConfigurations(folder: WorkspaceFolder | undefined): ProviderResult { + return [ + { + name: 'Debug current Jsonnet file', + request: 'launch', + type: 'jsonnet', + program: '${file}', + }, + ]; + }, + }, + DebugConfigurationProviderTriggerKind.Dynamic + ), + commands.registerCommand('jsonnet.debugEditorContents', (resource: Uri) => { + let targetResource = resource; + if (!targetResource && window.activeTextEditor) { + targetResource = window.activeTextEditor.document.uri; + } + if (targetResource) { + debug.startDebugging(undefined, { + type: 'jsonnet', + name: 'Debug File', + request: 'launch', + program: targetResource.fsPath, + }); + } + }) + ); context.subscriptions.push( workspace.onDidChangeConfiguration(didChangeConfigHandler), @@ -117,6 +165,14 @@ export function deactivate(): Thenable | undefined { return client.stop(); } +async function installDebugger(context: ExtensionContext): Promise { + const binPath = await install(extensionContext, channel, 'debugger'); + if (!binPath) { + return; + } + debug.registerDebugAdapterDescriptorFactory('jsonnet', new JsonnetDebugAdapterDescriptorFactory(context, binPath)); +} + async function startClient(): Promise { const args: string[] = ['--log-level', workspace.getConfiguration('jsonnet').get('languageServer.logLevel')]; if (workspace.getConfiguration('jsonnet').get('languageServer.tankaMode') === true) { @@ -126,7 +182,10 @@ async function startClient(): Promise { args.push('--lint'); } - const binPath = await install(extensionContext, channel); + const binPath = await install(extensionContext, channel, 'languageServer'); + if (!binPath) { + return; + } const executable: Executable = { command: binPath, args: args, diff --git a/src/install.ts b/src/install.ts index 800b8b7..e1e9b4c 100644 --- a/src/install.ts +++ b/src/install.ts @@ -4,15 +4,36 @@ import * as fs from 'fs'; import { execFileSync } from 'child_process'; import * as path from 'path'; -export async function install(context: ExtensionContext, channel: OutputChannel): Promise { - let binPath: string = workspace.getConfiguration('jsonnet').get('languageServer.pathToBinary'); - const releaseRepository: string = workspace.getConfiguration('jsonnet').get('languageServer.releaseRepository'); +export type Component = 'languageServer' | 'debugger'; - // If the binPath is undefined, use a default path +const ComponentDetails: Record< + Component, + { + binaryName: string; + displayName: string; + } +> = { + languageServer: { + binaryName: 'jsonnet-language-server', + displayName: 'language server', + }, + debugger: { + binaryName: 'jsonnet-debugger', + displayName: 'debugger', + }, +}; + +export async function install( + context: ExtensionContext, + channel: OutputChannel, + component: Component +): Promise { + const { binaryName, displayName } = ComponentDetails[component]; + let binPath: string = workspace.getConfiguration('jsonnet').get(`${component}.pathToBinary`); const isCustomBinPath = binPath !== undefined && binPath !== null && binPath !== ''; if (!isCustomBinPath) { - channel.appendLine(`Not using custom binary path. Using default path`); - binPath = path.join(context.globalStorageUri.fsPath, 'bin', 'jsonnet-language-server'); + channel.appendLine(`Not using custom binary path. Using default path for ${component}`); + binPath = path.join(context.globalStorageUri.fsPath, 'bin', binaryName); if (process.platform.toString() === 'win32') { binPath = `${binPath}.exe`; } @@ -27,18 +48,20 @@ export async function install(context: ExtensionContext, channel: OutputChannel) throw new Error(msg); } } + + const releaseRepository: string = workspace.getConfiguration('jsonnet').get(`${component}.releaseRepository`); + const binPathExists = fs.existsSync(binPath); channel.appendLine(`Binary path is ${binPath} (exists: ${binPathExists})`); // Without auto-update, the process ends here. - const enableAutoUpdate: boolean = workspace.getConfiguration('jsonnet').get('languageServer.enableAutoUpdate'); + const enableAutoUpdate: boolean = workspace.getConfiguration('jsonnet').get(`${component}.enableAutoUpdate`); if (!enableAutoUpdate) { if (!binPathExists) { - const msg = - "The language server binary does not exist, please set either 'jsonnet.languageServer.pathToBinary' or 'jsonnet.languageServer.enableAutoUpdate'"; + const msg = `The jsonnet ${displayName} binary does not exist, please set either 'jsonnet.${component}.pathToBinary' or 'jsonnet.${component}.enableAutoUpdate'`; channel.appendLine(msg); window.showErrorMessage(msg); - throw new Error(msg); + return null; } return binPath; } @@ -76,18 +99,21 @@ export async function install(context: ExtensionContext, channel: OutputChannel) if (!binPathExists) { // The binary does not exist. Only install if the user says yes. const value = await window.showInformationMessage( - `The language server does not seem to be installed. Do you wish to install the latest version?`, + `The jsonnet ${displayName} does not seem to be installed. Do you wish to install the latest version?`, 'Yes', 'No' ); - doUpdate = value === 'Yes'; + if (value === 'No') { + return null; + } + doUpdate = true; } else { // The binary exists try { // Check the version let currentVersion = ''; const result = execFileSync(binPath, ['--version']); - const prefix = 'jsonnet-language-server version '; + const prefix = `${binaryName} version `; if (result.toString().startsWith(prefix)) { currentVersion = result.toString().substring(prefix.length).trim(); } else { @@ -131,7 +157,7 @@ export async function install(context: ExtensionContext, channel: OutputChannel) suffix = '.exe'; } - const url = `https://github.com/${releaseRepository}/releases/download/v${latestVersion}/jsonnet-language-server_${latestVersion}_${platform}_${arch}${suffix}`; + const url = `https://github.com/${releaseRepository}/releases/download/v${latestVersion}/${binaryName}_${latestVersion}_${platform}_${arch}${suffix}`; channel.appendLine(`Downloading ${url}`); try { @@ -145,10 +171,10 @@ export async function install(context: ExtensionContext, channel: OutputChannel) throw new Error(msg); } - channel.appendLine(`Successfully downloaded the language server version ${latestVersion}`); - window.showInformationMessage(`Successfully installed the language server version ${latestVersion}`); + channel.appendLine(`Successfully downloaded the ${displayName} version ${latestVersion}`); + window.showInformationMessage(`Successfully installed the ${displayName} version ${latestVersion}`); } else { - channel.appendLine(`Not updating the language server.`); + channel.appendLine(`Not updating the ${displayName}.`); } return binPath;