Skip to content

Commit

Permalink
Java adapter (#106)
Browse files Browse the repository at this point in the history
* Initial java configuration

* Implement java adapter

* Remove debug output

* Fix Exception on jdtls server

* Resolve classPaths and mainClass

* Show errors

* Fix modulePaths and incompatible class version

* Allow failure when resolving mainClass/classPaths if set in configuration

* Fix for debug-java out-of-specification response

* Update Java adapter to new API

* Update Java adapter to new API

* Java: Move LSP config logic into debugger

* Java: Move source navigation special case to adapter

* Update README

* Java: Format

* Java: Adapt to new installer interface

* Java: Add jdtls connection timeout
  • Loading branch information
LDAP committed Sep 18, 2022
1 parent e6d8d95 commit a0b678c
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 14 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
1 change: 1 addition & 0 deletions modules/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
145 changes: 145 additions & 0 deletions modules/adapters/java.py
Original file line number Diff line number Diff line change
@@ -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)
11 changes: 10 additions & 1 deletion modules/dap/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from ..typecheck import *

from ..import core
from ..import dap
from .transport import Transport

import sublime
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
return json
7 changes: 5 additions & 2 deletions modules/dap/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
27 changes: 17 additions & 10 deletions modules/source_navigation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from __future__ import annotations


from .typecheck import *

from .import core
Expand All @@ -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',
}

Expand Down Expand Up @@ -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():
Expand All @@ -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])
Expand All @@ -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
3 changes: 2 additions & 1 deletion start.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
return True

0 comments on commit a0b678c

Please sign in to comment.