diff --git a/README.md b/README.md index f5023b2..4840f5c 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ You will also need to [install the Vimspector][5] plugin for Vim. The following CocCommand options are provided: - `java.debug.vimspector.start`: Launch Vimspector and connect it to the [Java Debug Server][1]. +- `java.debug.settings.update`: Sync local debug settings to [Java Debug Server][1]. Done automatically when Vimspector is started with `java.debug.vimspector.start` command. ### Command Arguments @@ -55,6 +56,37 @@ The following settings are supported in [CocConfig][9]: - `java.debug.vimspector.profile` : **(Deprecated)** Set to `null` and use `"default":true` in Vimspector.json instead. Specifies the Vimspector [profile][10] to activate when launching. Set to `null` to be prompted if multiple configurations are found and no default is set. Defaults to `Java Attach` - `java.debug.vimspector.substitution.adapterPort` : Specifies the Vimspector [adapter port][11] substitution name in `.vimspector.json`. The actual port number will replace this value in the Vimspector config when the debug server is started. Defaults to `AdapterPort` +- `java.debug.logLevel`: minimum level of debugger logs that are sent to language server, defaults to `warn`. +- `java.debug.settings.showHex`: show numbers in hex format in "Variables" viewlet, defaults to `false`. +- `java.debug.settings.showStaticVariables`: show static variables in "Variables" viewlet, defaults to `false`. +- `java.debug.settings.showQualifiedNames`: show fully qualified class names in "Variables" viewlet, defaults to `false`. +- `java.debug.settings.showLogicalStructure`: show the logical structure for the Collection and Map classes in "Variables" viewlet, defaults to `true`. +- `java.debug.settings.showToString`: show 'toString()' value for all classes that override 'toString' method in "Variables" viewlet, defaults to `true`. +- `java.debug.settings.maxStringLength`: the maximum length of string displayed in "Variables" viewlet, the string longer than this length will be trimmed, defaults to `0` which means no trim is performed. +- `java.debug.settings.numericPrecision`: the precision when formatting doubles in "Variables" viewlet. +- `java.debug.settings.hotCodeReplace`: Reload the changed Java classes during debugging, defaults to `manual`. +- `java.debug.settings.exceptionBreakpoint.exceptionTypes`: Specifies a set of exception types you want to break on, e.g. `java.lang.NullPointerException`. A specific exception type and its subclasses can be selected for caught exceptions, uncaught exceptions, or both can be selected. +- `java.debug.settings.exceptionBreakpoint.allowClasses`: Specifies the allowed locations where the exception breakpoint can break on. Wildcard is supported, e.g. `java.*`, `*.Foo`. +- `java.debug.settings.exceptionBreakpoint.skipClasses`: Skip the specified classes when breaking on exception. + - `$JDK` - Skip the JDK classes from the default system bootstrap classpath, such as rt.jar, jrt-fs.jar. + - `$Libraries` - Skip the classes from application libraries, such as Maven, Gradle dependencies. + - `java.*` - Skip the specified classes. Wildcard is supported. + - `java.lang.ClassLoader` - Skip the classloaders. +- `java.debug.settings.stepping.skipClasses`: Skip the specified classes when stepping. + - `$JDK` - Skip the JDK classes from the default system bootstrap classpath, such as rt.jar, jrt-fs.jar. + - `$Libraries` - Skip the classes from application libraries, such as Maven, Gradle dependencies. + - `java.*` - Skip the specified classes. Wildcard is supported. + - `java.lang.ClassLoader` - Skip the classloaders. +- `java.debug.settings.stepping.skipSynthetics`: Skip synthetic methods when stepping. +- `java.debug.settings.stepping.skipStaticInitializers`: Skip static initializer methods when stepping. +- `java.debug.settings.stepping.skipConstructors`: Skip constructor methods when stepping. +- `java.debug.settings.jdwp.limitOfVariablesPerJdwpRequest`: The maximum number of variables or fields that can be requested in one JDWP request. The higher the value, the less frequently debuggee will be requested when expanding the variable view. Also a large number can cause JDWP request timeout. Defaults to 100. +- `java.debug.settings.jdwp.requestTimeout`: The timeout (ms) of JDWP request when the debugger communicates with the target JVM. Defaults to 3000. +- `java.debug.settings.jdwp.async`: Experimental: Controls whether the debugger is allowed to send JDWP commands asynchronously. Async mode can improve remote debugging response speed on high-latency networks. Defaults to `auto`, and automatically switch to async mode when the latency of a single jdwp request exceeds 15ms during attach debugging. + - `auto` (Default) + - `on` + - `off` +- `java.debug.settings.debugSupportOnDecompiledSource`: [Experimental]: Enable debugging support on the decompiled source code. Be aware that this feature may affect the loading speed of Call Stack Viewlet. You also need [Language Support for Java by Red Hat](https://marketplace.visualstudio.com/items?itemName=redhat.java)@1.20.0 or higher to use this feature. ## Usage and Setup diff --git a/package.json b/package.json index d6d6019..5a533f9 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,186 @@ "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" + }, + "java.debug.logLevel": { + "type": "string", + "default": "warn", + "description": "minimum level of debugger logs that are sent to language server", + "enum": [ + "error", + "warn", + "info", + "verbose" + ], + "scope": "window" + }, + "java.debug.settings.showHex": { + "type": "boolean", + "default": false, + "description": "show numbers in hex format in `Variables` viewlet", + "scope": "window" + }, + "java.debug.settings.showStaticVariables": { + "type": "boolean", + "default": false, + "description": "Show static variables in `Variables` viewlet", + "scope": "window" + }, + "java.debug.settings.showQualifiedNames": { + "type": "boolean", + "default": false, + "description": "show fully qualified class names in `Variables` viewlet", + "scope": "window" + }, + "java.debug.settings.showLogicalStructure": { + "type": "boolean", + "default": true, + "description": "show the logical structure for the Collection and Map classes in `Variables` viewlet", + "scope": "window" + }, + "java.debug.settings.showToString": { + "type": "boolean", + "default": true, + "description": "show 'toString()' value for all classes that override 'toString' method in `Variables` viewlet", + "scope": "window" + }, + "java.debug.settings.maxStringLength": { + "type": "number", + "default": 0, + "description": "the maximum length of string displayed in `Variables` viewlet, the string longer than this length will be trimmed, defaults to 0 which means no trim is performed", + "scope": "window" + }, + "java.debug.settings.numericPrecision": { + "type": "number", + "default": 0, + "description": "the precision when formatting doubles in `Variables` viewlet", + "scope": "window" + }, + "java.debug.settings.hotCodeReplace": { + "type": "string", + "default": "manual", + "description": "Reload the changed Java classes during debugging", + "enum": [ + "auto", + "manual", + "never" + ], + "scope": "window" + }, + "java.debug.settings.exceptionBreakpoint.exceptionTypes": { + "type": "array", + "default": [], + "description": "Specifies a set of exception types you want to break on", + "scope": "window" + }, + "java.debug.settings.exceptionBreakpoint.allowClasses": { + "type": "array", + "default": [], + "description": "Specifies the allowed locations where the exception breakpoint can break on. Wildcard is supported, e.g. java.*, *.Foo", + "scope": "window" + }, + "java.debug.settings.exceptionBreakpoint.skipClasses": { + "type": "array", + "default": [], + "description": "Skip the specified classes when breaking on exception", + "items": { + "anyOf": [ + { + "enum": [ + "$JDK", + "$Libraries", + "java.lang.ClassLoader", + "" + ], + "enumDescriptions": [ + "Skip the JDK classes from the default system bootstrap classpath, such as rt.jar, jrt-fs.jar", + "Skip the classes from application libraries, such as Maven, Gradle dependencies", + "Skip the classloaders", + "Skip the specified classes. Wildcard is supported" + ] + }, + "string" + ] + }, + "scope": "window" + }, + "java.debug.settings.stepping.skipClasses": { + "type": "array", + "default": [], + "description": "Skip the specified classes when stepping", + "items": { + "anyOf": [ + { + "enum": [ + "$JDK", + "$Libraries", + "java.lang.ClassLoader", + "" + ], + "enumDescriptions": [ + "Skip the JDK classes from the default system bootstrap classpath, such as rt.jar, jrt-fs.jar", + "Skip the classes from application libraries, such as Maven, Gradle dependencies", + "Skip the classloaders", + "Skip the specified classes. Wildcard is supported" + ] + }, + "string" + ] + }, + "scope": "window" + }, + "java.debug.settings.stepping.skipSynthetics": { + "type": "boolean", + "default": false, + "description": "Skip synthetic methods when stepping", + "scope": "window" + }, + "java.debug.settings.stepping.skipStaticInitializers": { + "type": "boolean", + "default": false, + "description": "Skip static initializer methods when stepping", + "scope": "window" + }, + "java.debug.settings.stepping.skipConstructors": { + "type": "boolean", + "default": false, + "description": "Skip constructor methods when stepping", + "scope": "window" + }, + "java.debug.settings.jdwp.limitOfVariablesPerJdwpRequest": { + "type": "number", + "default": 100, + "minimum": 1, + "description": "The maximum number of variables or fields that can be requested in one JDWP request", + "scope": "window" + }, + "java.debug.settings.jdwp.requestTimeout": { + "type": "number", + "default": 3000, + "minimum": 100, + "description": "The timeout (ms) of JDWP request when the debugger communicates with the target JVM", + "scope": "window" + }, + "java.debug.settings.jdwp.async": { + "type": "string", + "default": "off", + "description": "Experimental: Controls whether the debugger is allowed to send JDWP commands asynchronously", + "enum": [ + "auto", + "on", + "off" + ], + "scope": "window" + }, + "java.debug.settings.debugSupportOnDecompiledSource": { + "type": "string", + "default": "off", + "description": "Experimental: Enable debugging support on the decompiled source code", + "enum": [ + "on", + "off" + ], + "scope": "window" } } }, @@ -120,6 +300,11 @@ "title": "Launch Vimspector and connect it to the Java Debug Server.", "category": "Java" }, + { + "command": "java.debug.settings.update", + "title": "Update debug settings.", + "category": "Java" + }, { "command": "java.debug.resolveMainMethod", "title": "Show resolved main methods.", diff --git a/src/classFilter.ts b/src/classFilter.ts new file mode 100644 index 0000000..250f8aa --- /dev/null +++ b/src/classFilter.ts @@ -0,0 +1,21 @@ +import { Commands, executeCommand } from './commands'; + +export async function substituteFilterVariables(skipClasses: string[]): Promise { + if (!skipClasses) { + return []; + } + + try { + // Preprocess skipClasses configurations. + if (Array.isArray(skipClasses)) { + const hasReservedName = skipClasses.some((filter) => filter === '$JDK' || filter === '$Libraries'); + return hasReservedName ? await executeCommand(Commands.JAVA_RESOLVE_CLASSFILTERS, skipClasses) : skipClasses; + } else { + console.error('Invalid type for skipClasses config:' + skipClasses); + } + } catch (e) { + console.error(e); + } + + return []; +} diff --git a/src/commands.ts b/src/commands.ts index bc97169..3039363 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -7,8 +7,14 @@ export namespace Commands { export const JAVA_RESOLVE_MAINMETHOD = 'vscode.java.resolveMainMethod'; + export const JAVA_RESOLVE_CLASSFILTERS = 'vscode.java.resolveClassFilters'; + + export const JAVA_UPDATE_DEBUG_SETTINGS = 'vscode.java.updateDebugSettings'; + export const JAVA_DEBUG_VIMSPECTOR_START = 'java.debug.vimspector.start'; + export const JAVA_DEBUG_SETTINGS_UPDATE = 'java.debug.settings.update'; + export const JAVA_DEBUG_RESOLVE_MAINMETHOD = 'java.debug.resolveMainMethod'; export const JAVA_DEBUG_RESOLVE_CLASSPATH = 'java.debug.resolveClasspath'; diff --git a/src/index.ts b/src/index.ts index e77b926..13db3e9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,14 +7,17 @@ import { resolveMainMethodsCurrentFile, } from './debugserver'; import { ISubstitutionVar } from './protocol'; +import { onConfigurationChange, updateDebugSettings } from './settings'; export async function activate(context: ExtensionContext): Promise { registerCommands(context); + context.subscriptions.push(onConfigurationChange()); return Promise.resolve(); } function registerCommands(context: ExtensionContext): void { context.subscriptions.push(commands.registerCommand(Commands.JAVA_DEBUG_VIMSPECTOR_START, startVimspector)); + context.subscriptions.push(commands.registerCommand(Commands.JAVA_DEBUG_SETTINGS_UPDATE, updateDebugSettings)); context.subscriptions.push( commands.registerCommand(Commands.JAVA_DEBUG_RESOLVE_MAINMETHOD, showCommandResult(resolveMainMethodsCurrentFile)), ); @@ -29,6 +32,8 @@ async function startVimspector(...args: any[]): Promise { console.info(msg); window.showInformationMessage(msg); + updateDebugSettings(); + const mainMethod = await resolveMainMethodCurrentFile(); const mainClass = mainMethod?.mainClass; const projectName = mainMethod?.projectName; diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..2808349 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,104 @@ +import { commands, workspace } from 'coc.nvim'; +import { substituteFilterVariables } from './classFilter'; +import { Commands } from './commands'; + +export function onConfigurationChange() { + return workspace.onDidChangeConfiguration((params) => { + if (!params.affectsConfiguration('java.debug.settings') && !params.affectsConfiguration('java.debug.logLevel')) { + return; + } + updateDebugSettings(); + }); +} + +export async function updateDebugSettings() { + const debugSettingsRoot = workspace.getConfiguration('java.debug'); + + if (!debugSettingsRoot) { + return; + } + const logLevel = convertLogLevel(debugSettingsRoot.logLevel || ''); + if (debugSettingsRoot.settings && Object.keys(debugSettingsRoot.settings).length) { + try { + const extraSettings = {}; + if (debugSettingsRoot.settings.stepping && Object.keys(debugSettingsRoot.settings.stepping).length) { + const stepFilters = {}; + if (debugSettingsRoot.settings.stepping.skipClasses) { + stepFilters['skipClasses'] = await substituteFilterVariables(debugSettingsRoot.settings.stepping.skipClasses); + } + if (debugSettingsRoot.settings.stepping.skipSynthetics) { + stepFilters['skipSynthetics'] = debugSettingsRoot.settings.stepping.skipSynthetics; + } + if (debugSettingsRoot.settings.stepping.skipStaticInitializers) { + stepFilters['skipStaticInitializers'] = debugSettingsRoot.settings.stepping.skipStaticInitializers; + } + if (debugSettingsRoot.settings.stepping.skipConstructors) { + stepFilters['skipConstructors'] = debugSettingsRoot.settings.stepping.skipConstructors; + } + extraSettings['stepFilters'] = stepFilters; + } + if ( + debugSettingsRoot.settings.exceptionBreakpoint && + Object.keys(debugSettingsRoot.settings.exceptionBreakpoint).length + ) { + const exceptionFilters = {}; + if (debugSettingsRoot.settings.exceptionBreakpoint.exceptionTypes) { + exceptionFilters['exceptionTypes'] = debugSettingsRoot.settings.exceptionBreakpoint.exceptionTypes; + } + if (debugSettingsRoot.settings.exceptionBreakpoint.allowClasses) { + exceptionFilters['allowClasses'] = debugSettingsRoot.settings.exceptionBreakpoint.allowClasses; + } + if (debugSettingsRoot.settings.exceptionBreakpoint.skipClasses) { + exceptionFilters['skipClasses'] = await substituteFilterVariables( + debugSettingsRoot.settings.exceptionBreakpoint.skipClasses, + ); + } + extraSettings['exceptionFilters'] = exceptionFilters; + extraSettings['exceptionFiltersUpdated'] = true; + } + + if (debugSettingsRoot.settings.jdwp) { + if (debugSettingsRoot.settings.jdwp.async) { + extraSettings['asyncJDWP'] = debugSettingsRoot.settings.jdwp.async; + } + if (debugSettingsRoot.settings.jdwp.limitOfVariablesPerJdwpRequest) { + extraSettings['limitOfVariablesPerJdwpRequest'] = Math.max( + debugSettingsRoot.settings.jdwp.limitOfVariablesPerJdwpRequest, + 1, + ); + } + if (debugSettingsRoot.settings.jdwp.requestTimeout) { + extraSettings['requestTimeout'] = Math.max(debugSettingsRoot.settings.jdwp.requestTimeout, 100); + } + } + const settings = await commands.executeCommand( + Commands.JAVA_UPDATE_DEBUG_SETTINGS, + JSON.stringify({ + ...debugSettingsRoot.settings, + ...extraSettings, + logLevel, + }), + ); + console.debug('settings:', settings); + } catch (err) { + // log a warning message and continue, since update settings failure should not block debug session + console.error('Cannot update debug settings.', err); + } + } +} + +function convertLogLevel(commonLogLevel: string) { + // convert common log level to java log level + switch (commonLogLevel.toLowerCase()) { + case 'verbose': + return 'FINE'; + case 'warn': + return 'WARNING'; + case 'error': + return 'SEVERE'; + case 'info': + return 'INFO'; + default: + return 'FINE'; + } +}