Skip to content
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

Java adapter #106

Merged
merged 22 commits into from
Sep 18, 2022
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
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()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might change that next week to only request if the user did not provide certain config option, by splitting in multiple methods. I should also use the correct main class in the checkProjectSettings command below.


# 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