diff --git a/README.md b/README.md index 1df2bfaf..7cb30b68 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ This project comes with some pre-configured debuggers (They can be installed usi ##### PHP - See https://github.com/felixfbecker/vscode-php-debug +##### Java +- Requires [LSP](https://packagecontrol.io/packages/LSP) and [LSP-jdtls](https://packagecontrol.io/packages/LSP-jdtls) +- See https://github.com/redhat-developer/vscode-java + ## Setup - Open the debug panel - from the command palette `Debugger: Open` diff --git a/modules/adapters/__init__.py b/modules/adapters/__init__.py index 5333b4f8..a8cad4f2 100644 --- a/modules/adapters/__init__.py +++ b/modules/adapters/__init__.py @@ -6,6 +6,7 @@ from .python import Python from .go import Go from .php import PHP +from .java import Java from .ruby import Ruby from .elixir import Elixir from .lua import Lua diff --git a/modules/adapters/java.py b/modules/adapters/java.py new file mode 100644 index 00000000..6d040703 --- /dev/null +++ b/modules/adapters/java.py @@ -0,0 +1,145 @@ +from ..typecheck import Optional, Dict, Any, Tuple +from ..import core +from ..import dap +from .import util + +import sublime +import sublime_plugin +import os +import json + + +class Java(dap.AdapterConfiguration): + jdtls_bridge: Dict[int, core.Future] = {} + jdtls_bridge_current_id = 0 + + type = 'java' + docs = 'https://github.com/redhat-developer/vscode-java/blob/master/README.md' + + installer = util.OpenVsxInstaller( + type='java', + repo='vscjava/vscode-java-debug' + ) + + async def start(self, log, configuration): + # Make sure LSP and LSP-JDTLS are installed + pc_settings = sublime.load_settings('Package Control.sublime-settings') + installed_packages = pc_settings.get('installed_packages', []) + if 'LSP-jdtls' not in installed_packages or 'LSP' not in installed_packages: + raise core.Error('LSP and LSP-jdtls required to debug Java!') + + # Get configuration from LSP + lsp_config = await self.get_configuration_from_lsp() + + # Configure debugger + if 'cwd' not in configuration: + configuration['cwd'], _ = os.path.split(sublime.active_window().project_file_name()) + if 'mainClass' not in configuration or not configuration['mainClass']: + configuration['mainClass'] = lsp_config['mainClass'] + if 'classPaths' not in configuration: + configuration['classPaths'] = lsp_config['classPaths'] + if 'modulePaths' not in configuration: + configuration['modulePaths'] = lsp_config['modulePaths'] + if 'console' not in configuration: + configuration['console'] = 'internalConsole' + if lsp_config['enablePreview']: + if 'vmArgs' in configuration: + configuration['vmArgs'] += ' --enable-preview' + else: + configuration['vmArgs'] = '--enable-preview' + + # Start debugging session on the LSP side + port = await self.lsp_execute_command('vscode.java.startDebugSession') + + return dap.SocketTransport(log, 'localhost', port) + + async def on_navigate_to_source(self, source: dap.SourceLocation) -> Optional[Tuple[str, str]]: + if not source.source.path or not source.source.path.startswith('jdt:'): + return None + content = await self.get_class_content_for_uri(source.source.path) + return content, 'text/java' + + async def get_class_content_for_uri(self, uri): + return await self.lsp_request('java/classFileContents', {'uri': uri}) + + async def get_configuration_from_lsp(self) -> dict: + lsp_config = {} + + mainclass_resp = await self.lsp_execute_command('vscode.java.resolveMainClass') + if not mainclass_resp or 'mainClass' not in mainclass_resp[0]: + raise core.Error('Failed to resolve main class') + else: + lsp_config['mainClass'] = mainclass_resp[0]['mainClass'] + lsp_config['projectName'] = mainclass_resp[0].get('projectName', '') + + classpath_response = await self.lsp_execute_command( + 'vscode.java.resolveClasspath', [lsp_config['mainClass'], lsp_config['projectName']] + ) + if not classpath_response[0] and not classpath_response[1]: + raise core.Error('Failed to resolve classpaths/modulepaths') + else: + lsp_config['modulePaths'] = classpath_response[0] + lsp_config['classPaths'] = classpath_response[1] + + # See https://github.com/microsoft/vscode-java-debug/blob/b2a48319952b1af8a4a328fc95d2891de947df94/src/configurationProvider.ts#L297 + lsp_config['enablePreview'] = await self.lsp_execute_command( + 'vscode.java.checkProjectSettings', + [ + json.dumps( + { + 'className': lsp_config['mainClass'], + 'projectName': lsp_config['projectName'], + 'inheritedOptions': True, + 'expectedOptions': { + 'org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures': 'enabled' + }, + } + ) + ] + ) + + return lsp_config + + async def lsp_execute_command(self, command, arguments=None): + request_params = { 'command': command } + if arguments: + request_params['arguments'] = arguments + return await self.lsp_request('workspace/executeCommand', request_params) + + async def lsp_request(self, method, params) -> Any: + ''' + Returns the response or raises an exception. + ''' + future = core.Future() + + id = Java.jdtls_bridge_current_id + Java.jdtls_bridge_current_id += 1 + Java.jdtls_bridge[id] = future + + # Send a request to JDTLS. + # NOTE: the active window might not match the debugger window but generally will + # TODO: a way to get the actual window. + sublime.active_window().run_command( + 'debugger_jdtls_bridge_request', + {'id': id, 'callback_command': 'debugger_jdtls_bridge_response', 'method': method, 'params': params} + ) + sublime.set_timeout(lambda: future.cancel(), 2500) + try: + command_response = await future + except core.CancelledError: + raise core.Error('Unable to connect to LSP-jdtls (timed out)') + + del Java.jdtls_bridge[id] + + if command_response['error']: + raise core.Error(command_response['error']) + return command_response['resp'] + + +class DebuggerJdtlsBridgeResponseCommand(sublime_plugin.WindowCommand): + def run(self, **args): + future = Java.jdtls_bridge.get(args['id']) + if not future: + print('Unable to find a future for this id') + return + future.set_result(args) diff --git a/modules/dap/configuration.py b/modules/dap/configuration.py index 5b371755..eae9b345 100644 --- a/modules/dap/configuration.py +++ b/modules/dap/configuration.py @@ -2,6 +2,7 @@ from ..typecheck import * from ..import core +from ..import dap from .transport import Transport import sublime @@ -95,6 +96,14 @@ def settings(self, debugger: Debugger) -> list[Any]: def ui(self, debugger: Debugger) -> Any|None: ... + async def on_navigate_to_source(self, source: dap.SourceLocation) -> Optional[Tuple[str, str]]: + """ + Allows the adapter to supply content when navigating to source. + Returns: None to keep the default behavior, else a tuple (content, mime_type) + """ + return None + + class Configuration(Dict[str, Any]): def __init__(self, name: str, index: int, type: str, request: str, all: dict[str, Any]): super().__init__(all) @@ -179,4 +188,4 @@ def _expand_variables_and_platform(json: dict[str, Any], variables: dict[str, st if variables := variables: json = sublime.expand_variables(json, variables) - return json \ No newline at end of file + return json diff --git a/modules/dap/session.py b/modules/dap/session.py index 9a153a8f..81422d49 100644 --- a/modules/dap/session.py +++ b/modules/dap/session.py @@ -775,8 +775,11 @@ def set_selected(self, thread: Thread, frame: Optional[dap.StackFrame]): # @NOTE threads_for_id will retain all threads for the entire session even if they are removed @core.schedule async def refresh_threads(self): - response = await self.request('threads', None) - threads: list[dap.Thread] = response['threads'] + # the Java debugger requires an empty object instead of `None` + # See https://github.com/daveleroy/sublime_debugger/pull/106#issuecomment-793802989 + response = await self.request('threads', {}) + # See https://github.com/daveleroy/sublime_debugger/pull/106#issuecomment-795949070 + threads: list[dap.Thread] = response.get('threads', []) self.threads.clear() for thread in threads: diff --git a/modules/source_navigation.py b/modules/source_navigation.py index a9162060..484c7df3 100644 --- a/modules/source_navigation.py +++ b/modules/source_navigation.py @@ -1,4 +1,6 @@ from __future__ import annotations + + from .typecheck import * from .import core @@ -15,6 +17,7 @@ syntax_name_for_mime_type: dict[str|None, str] = { 'text/plain': 'text.plain', 'text/javascript': 'source.js', + 'text/java': 'source.java', 'text/x-lldb.disassembly': 'source.disassembly', } @@ -97,20 +100,27 @@ def clear_generated_view(self): self.generated_view = None async def navigate_to_source(self, source: dap.SourceLocation, move_cursor: bool = False) -> sublime.View: + # Check if adapter want to provide content + if self.debugger.session: + adapter_content = await self.debugger.session.adapter_configuration.on_navigate_to_source(source) + else: + adapter_content = None # if we aren't going to reuse the previous generated view throw away any generated view - if not source.source.sourceReference: + if adapter_content or source.source.sourceReference: self.clear_generated_view() line = (source.line or 1) - 1 column = (source.column or 1) - 1 - if source.source.sourceReference: - session = self.debugger.session - if not session: - raise core.Error('No Active Debug Session') - - content, mime_type = await session.get_source(source.source) + if adapter_content or source.source.sourceReference: + if adapter_content: + content, mime_type = adapter_content + else: + session = self.debugger.session + if not session: + raise core.Error('No Active Debug Session') + content, mime_type = await session.get_source(source.source) # the generated view was closed (no buffer) throw it away if self.generated_view and not self.generated_view.buffer_id(): @@ -121,7 +131,6 @@ async def navigate_to_source(self, source: dap.SourceLocation, move_cursor: bool self.generated_view = view view.set_name(source.source.name or "") view.set_read_only(False) - syntax = syntax_name_for_mime_type.get(mime_type, 'text.plain') view.assign_syntax(sublime.find_syntax_by_scope(syntax)[0]) @@ -135,7 +144,5 @@ async def navigate_to_source(self, source: dap.SourceLocation, move_cursor: bool else: raise core.Error('source has no reference or path') - show_line(view, line, column, move_cursor) - return view diff --git a/start.py b/start.py index faa82472..6cbcb693 100644 --- a/start.py +++ b/start.py @@ -15,6 +15,7 @@ # import all the commands so that sublime sees them from .modules.command import CommandsRegistry, DebuggerExecCommand, DebuggerCommand, DebuggerInputCommand +from .modules.adapters.java import DebuggerJdtlsBridgeResponseCommand from .modules.core.sublime import DebuggerAsyncTextCommand, DebuggerEventsListener from .modules.debugger_output_panel import DebuggerConsoleListener @@ -216,4 +217,4 @@ def on_view_gutter_clicked(self, view: sublime.View, line: int, button: int) -> if source_breakpoints: debugger.breakpoints.source.edit_breakpoints(source_breakpoints) - return True \ No newline at end of file + return True