From a3eca5ac949a3cb5c6ea08ad5d66845d4d80b403 Mon Sep 17 00:00:00 2001 From: Dan Hansen Date: Mon, 19 Feb 2024 20:39:39 -0600 Subject: [PATCH] Add Vimspector config substitutions for convenience The following variables are now available for automatic substitution in Vimspector config based on the currently loaded Java file. Note, these only work if the Java file contains a main method. - ClassPaths - MainClass - ModulePaths - ProjectName The variable substitution names are configurable via the plugin settings. - java.debug.vimspector.substitution.classPaths - java.debug.vimspector.substitution.mainClass - java.debug.vimspector.substitution.modulePaths - java.debug.vimspector.substitution.projectName --- package.json | 34 +++++++++++++++++++++++ src/commands.ts | 27 ++++++++++-------- src/debugserver.ts | 58 +++++++++++++++++++++++++++++++++++++++ src/index.ts | 68 ++++++++++++++++++++++++++++++++++++---------- src/protocol.ts | 27 ++++++++++++++++++ 5 files changed, 189 insertions(+), 25 deletions(-) create mode 100644 src/debugserver.ts create mode 100644 src/protocol.ts diff --git a/package.json b/package.json index 23207be..d6d6019 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,30 @@ "default": "AdapterPort", "description": "Specifies the Vimspector adapter port substitution name in `.vimspector.json`. The actual port number will replace this value in the Vimspector config when the debug server is started.", "scope": "window" + }, + "java.debug.vimspector.substitution.classPaths": { + "type": "string", + "default": "ClassPaths", + "description": "Specifies the Vimspector class paths substitution name in `.vimspector.json`. The actual class paths will replace this value in the Vimspector config when the debug server is started.", + "scope": "window" + }, + "java.debug.vimspector.substitution.mainClass": { + "type": "string", + "default": "MainClass", + "description": "Specifies the Vimspector main class substitution name in `.vimspector.json`. The actual main class will replace this value in the Vimspector config when the debug server is started.", + "scope": "window" + }, + "java.debug.vimspector.substitution.modulePaths": { + "type": "string", + "default": "ModulePaths", + "description": "Specifies the Vimspector module paths substitution name in `.vimspector.json`. The actual module paths will replace this value in the Vimspector config when the debug server is started.", + "scope": "window" + }, + "java.debug.vimspector.substitution.projectName": { + "type": "string", + "default": "ProjectName", + "description": "Specifies the Vimspector project name substitution name in `.vimspector.json`. The actual project name will replace this value in the Vimspector config when the debug server is started.", + "scope": "window" } } }, @@ -95,6 +119,16 @@ "command": "java.debug.vimspector.start", "title": "Launch Vimspector and connect it to the Java Debug Server.", "category": "Java" + }, + { + "command": "java.debug.resolveMainMethod", + "title": "Show resolved main methods.", + "category": "Java" + }, + { + "command": "java.debug.resolveClasspath", + "title": "Show resolved class paths.", + "category": "Java" } ] } diff --git a/src/commands.ts b/src/commands.ts index 702d71e..bc97169 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,16 +1,21 @@ +import { commands } from 'coc.nvim'; + export namespace Commands { - /** - * vscode-java-debug command to start a debug session. - */ - export const JAVA_START_DEBUG_SESSION = 'vscode.java.startDebugSession'; - - /** - * coc command to start a java debugger session and connect vimspector to it. - */ + export const JAVA_START_DEBUGSESSION = 'vscode.java.startDebugSession'; + + export const JAVA_RESOLVE_CLASSPATH = 'vscode.java.resolveClasspath'; + + export const JAVA_RESOLVE_MAINMETHOD = 'vscode.java.resolveMainMethod'; + export const JAVA_DEBUG_VIMSPECTOR_START = 'java.debug.vimspector.start'; - /** - * Execute Workspace Command - */ + export const JAVA_DEBUG_RESOLVE_MAINMETHOD = 'java.debug.resolveMainMethod'; + + export const JAVA_DEBUG_RESOLVE_CLASSPATH = 'java.debug.resolveClasspath'; + export const EXECUTE_WORKSPACE_COMMAND = 'java.execute.workspaceCommand'; } + +export async function executeCommand(...rest: any[]) { + return commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, ...rest); +} diff --git a/src/debugserver.ts b/src/debugserver.ts new file mode 100644 index 0000000..e8dd771 --- /dev/null +++ b/src/debugserver.ts @@ -0,0 +1,58 @@ +import { TextDocument, Uri, window, workspace } from 'coc.nvim'; +import { Commands, executeCommand } from './commands'; +import { IClassPath, IMainClassOption, MainMethodResult } from './protocol'; + +export async function resolveMainMethodCurrentFile(): Promise { + const mainMethods = await resolveMainMethodsCurrentFile(); + if (mainMethods.length === 1) { + return mainMethods[0]; + } else if (mainMethods.length > 1) { + return await pickMainMethod(mainMethods); + } + return undefined; +} + +export async function resolveMainMethodsCurrentFile(): Promise { + const { document } = await workspace.getCurrentState(); + return resolveMainMethod(document); +} + +async function resolveMainMethod(document: TextDocument): Promise { + const resourcePath = getJavaResourcePath(document); + return await executeCommand(Commands.JAVA_RESOLVE_MAINMETHOD, resourcePath); +} + +export async function resolveClassPathCurrentFile(): Promise { + const mainMethod = await resolveMainMethodCurrentFile(); + if (mainMethod) { + return resolveClassPathMainMethod(mainMethod); + } + return { modulePaths: [], classPaths: [] }; +} + +export async function resolveClassPathMainMethod(mainMethod: IMainClassOption): Promise { + const classPath: any[] = await resolveClasspath(mainMethod.mainClass, mainMethod.projectName || ''); + const [modulePaths, classPaths] = classPath; + return { modulePaths, classPaths }; +} + +async function resolveClasspath(mainClass: string, projectName: string, scope?: string): Promise { + return executeCommand(Commands.JAVA_RESOLVE_CLASSPATH, mainClass, projectName, scope); +} + +function getJavaResourcePath(document: TextDocument): string | undefined { + const resource = Uri.parse(document.uri); + if (resource.scheme === 'file' && resource.fsPath.endsWith('.java')) { + return resource.toString(); + } + return undefined; +} + +export async function pickMainMethod(mainMethods: MainMethodResult): Promise { + const items = mainMethods.map((method) => { + return method.mainClass; + }); + const selected = await window.showQuickpick(items, 'Choose a main method.'); + // Choose the first one if none is selected. + return mainMethods[selected >= 0 ? selected : 0]; +} diff --git a/src/index.ts b/src/index.ts index 45c3a1c..686e8d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,12 @@ -import { commands, ExtensionContext, workspace, window } from 'coc.nvim'; -import { Commands } from './commands'; +import { ExtensionContext, commands, window, workspace } from 'coc.nvim'; +import { Commands, executeCommand } from './commands'; +import { + resolveClassPathCurrentFile, + resolveClassPathMainMethod, + resolveMainMethodCurrentFile, + resolveMainMethodsCurrentFile, +} from './debugserver'; +import { ISubstitutionVar } from './protocol'; export async function activate(context: ExtensionContext): Promise { registerCommands(context); @@ -8,36 +15,60 @@ export async function activate(context: ExtensionContext): Promise { function registerCommands(context: ExtensionContext): void { context.subscriptions.push(commands.registerCommand(Commands.JAVA_DEBUG_VIMSPECTOR_START, startVimspector)); + context.subscriptions.push( + commands.registerCommand(Commands.JAVA_DEBUG_RESOLVE_MAINMETHOD, showCommandResult(resolveMainMethodsCurrentFile)), + ); + context.subscriptions.push( + commands.registerCommand(Commands.JAVA_DEBUG_RESOLVE_CLASSPATH, showCommandResult(resolveClassPathCurrentFile)), + ); } async function startVimspector(...args: any[]): Promise { - window.showMessage('Starting Java debug server...'); + const debugPort: string = await executeCommand(Commands.JAVA_START_DEBUGSESSION); + const msg = `Java debug server started on port: ${debugPort}`; + console.info(msg); + window.showInformationMessage(msg); - const debugPort: string = await commands.executeCommand( - Commands.EXECUTE_WORKSPACE_COMMAND, - Commands.JAVA_START_DEBUG_SESSION, - ); + const mainMethod = await resolveMainMethodCurrentFile(); + if (!mainMethod) { + window.showErrorMessage(`A Java file must be active for :CocCommand ${Commands.JAVA_DEBUG_VIMSPECTOR_START}`); + return Promise.resolve(); + } + const mainClass = mainMethod.mainClass; + const projectName = mainMethod.projectName; + const classPathMainMethod = await resolveClassPathMainMethod(mainMethod); - window.showMessage(`Java debug server started on port: ${debugPort}`); + // See https://puremourning.github.io/vimspector/configuration.html#the-splat-operator + const modulePaths = classPathMainMethod.modulePaths.join(' '); + const classPaths = classPathMainMethod.classPaths.join(' '); const debugConfig = workspace.getConfiguration('java.debug'); + // See package.json#configuration.properties + const vars = debugConfig.get('vimspector.substitution'); + + // DEPRECATED Vimspector supports choosing a default now. const profile = debugConfig.get('vimspector.profile'); - const adapterPort = debugConfig.get('vimspector.substitution.adapterPort'); - const overrides = getOverrides(args); const defaults = {}; if (profile) { defaults['configuration'] = profile; } + + const overrides = getOverrides(args); + const settings = { - [adapterPort as string]: debugPort, + [vars?.adapterPort as string]: debugPort, + [vars?.classPaths as string]: classPaths, + [vars?.mainClass as string]: mainClass, + [vars?.modulePaths as string]: modulePaths, + [vars?.projectName as string]: projectName, ...defaults, ...overrides, }; const vimspectorSettings = JSON.stringify(settings); - // See https://github.com/puremourning/vimspector#launch-with-options - window.showMessage(`Launching Vimspector with settings: ${vimspectorSettings}`); + // View logs with :CocOpenLog + console.info(`Launching Vimspector with settings: ${vimspectorSettings}`); return workspace.nvim.eval(`vimspector#LaunchWithSettings(${vimspectorSettings})`); } @@ -68,8 +99,17 @@ function parseOverrides(args: string): any { try { overrides = JSON.parse(args); } catch (e) { - window.showMessage(`Expected valid JSON for Vimspector settings, but got: ${args}`, 'error'); + window.showErrorMessage(`Expected valid JSON for Vimspector settings, but got: ${args}`, 'error'); } } return overrides; } + +function showCommandResult(func: () => Promise): (...args: any[]) => Promise { + return async () => { + const result = await func(); + const json = JSON.stringify(result, null, 2); + window.showInformationMessage(json); + return Promise.resolve(); + }; +} diff --git a/src/protocol.ts b/src/protocol.ts new file mode 100644 index 0000000..b3fcf1f --- /dev/null +++ b/src/protocol.ts @@ -0,0 +1,27 @@ +import { Range } from 'coc.nvim'; + +export interface IMainClassOption { + readonly mainClass: string; + readonly projectName?: string; + readonly filePath?: string; +} + +export interface IMainMethod extends IMainClassOption { + readonly range: Range; +} + +export type MainMethodResult = Array; + +// See https://github.com/microsoft/vscode-java-debug#options +export interface ISubstitutionVar { + readonly adapterPort: string; + readonly classPaths: string; + readonly mainClass: string; + readonly modulePaths: string; + readonly projectName: string; +} + +export interface IClassPath { + readonly modulePaths: string[]; + readonly classPaths: string[]; +}