Skip to content

feat: add debugger support #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 83 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -16,13 +16,15 @@
"categories": [
"Programming Languages",
"Linters",
"Formatters"
"Formatters",
"Debuggers"
],
"keywords": [
"jsonnet",
"grafana",
"lsp",
"language"
"language",
"debugger"
],
"activationEvents": [
"onLanguage:jsonnet"
Expand All @@ -44,6 +46,13 @@
"group": "navigation",
"when": "resourceLangId == jsonnet"
}
],
"editor/title/run": [
{
"command": "jsonnet.debugEditorContents",
"when": "resourceLangId == jsonnet",
"group": "navigation@1"
}
]
},
"commands": [
Expand Down Expand Up @@ -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": [
Expand All @@ -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",
Expand Down Expand Up @@ -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"
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions src/debugger.ts
Original file line number Diff line number Diff line change
@@ -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<vscode.DebugAdapterDescriptor> {
return new vscode.DebugAdapterExecutable(this.binPath, ['-d', '-s']);
}
}
63 changes: 61 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,6 +28,7 @@ import {
ServerOptions,
} from 'vscode-languageclient/node';
import { install } from './install';
import { JsonnetDebugAdapterDescriptorFactory } from './debugger';

let extensionContext: ExtensionContext;
let client: LanguageClient;
Expand All @@ -24,7 +39,40 @@ export async function activate(context: ExtensionContext): Promise<void> {
extensionContext = context;

await startClient();
await installDebugger(context);
await didChangeConfigHandler();
context.subscriptions.push(
debug.registerDebugConfigurationProvider(
'jsonnet',
{
provideDebugConfigurations(folder: WorkspaceFolder | undefined): ProviderResult<DebugConfiguration[]> {
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),
Expand Down Expand Up @@ -117,6 +165,14 @@ export function deactivate(): Thenable<void> | undefined {
return client.stop();
}

async function installDebugger(context: ExtensionContext): Promise<void> {
const binPath = await install(extensionContext, channel, 'debugger');
if (!binPath) {
return;
}
debug.registerDebugAdapterDescriptorFactory('jsonnet', new JsonnetDebugAdapterDescriptorFactory(context, binPath));
}

async function startClient(): Promise<void> {
const args: string[] = ['--log-level', workspace.getConfiguration('jsonnet').get('languageServer.logLevel')];
if (workspace.getConfiguration('jsonnet').get('languageServer.tankaMode') === true) {
Expand All @@ -126,7 +182,10 @@ async function startClient(): Promise<void> {
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,
Expand Down
60 changes: 43 additions & 17 deletions src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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<string | null> {
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`;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down