From 47c7a5c8fe07ee6290aa8b62a9f3a7dc62ef29bc Mon Sep 17 00:00:00 2001 From: David <2889367+daveleroy@users.noreply.github.com> Date: Tue, 24 Sep 2019 22:53:06 -0700 Subject: [PATCH] Moved a bunch of logic out of debugger_interface such as the navigating/showing selected stack frame file/line. Toggle breakpoint etc. Generate sublime-commands and sublime-menu files. Adds run to cursor logic and command. --- Commands/Commands.sublime-commands | 185 +++++++----- Commands/Context.sublime-menu | 28 +- Commands/Main.sublime-menu | 179 ++++++++---- debug_adapters/php.json | 1 - modules/commands/debugger.py | 96 ------ modules/commands/install_adapters_menu.py | 15 +- modules/components/breakpoints_panel.py | 11 +- modules/components/callstack_panel.py | 10 +- modules/components/debugger_panel.py | 45 ++- modules/components/selected_line.py | 7 +- modules/components/variables_panel.py | 13 +- modules/core/__init__.py | 3 +- modules/core/core.py | 24 +- modules/core/dispose.py | 24 ++ modules/core/error.py | 4 + modules/dap/__init__.py | 4 +- modules/dap/transport.py | 116 +------- modules/debugger/breakpoint_commands.py | 55 ++++ modules/debugger/breakpoints.py | 1 + modules/debugger/commands.py | 243 ++++++++++++++++ modules/debugger/debugger.py | 97 ++---- modules/debugger/debugger_interface.py | 340 ++++++++-------------- modules/debugger/debugger_project.py | 16 + modules/debugger/debugger_terminal.py | 9 +- modules/debugger/thread.py | 12 +- modules/debugger/view_hover.py | 66 +++++ modules/debugger/view_selected_source.py | 99 +++++++ setup.cfg | 4 +- start.py | 1 + 29 files changed, 981 insertions(+), 727 deletions(-) mode change 100755 => 100644 Commands/Commands.sublime-commands mode change 100755 => 100644 Commands/Main.sublime-menu create mode 100644 modules/core/dispose.py create mode 100644 modules/core/error.py create mode 100644 modules/debugger/breakpoint_commands.py create mode 100644 modules/debugger/commands.py create mode 100644 modules/debugger/debugger_project.py create mode 100644 modules/debugger/view_hover.py create mode 100644 modules/debugger/view_selected_source.py diff --git a/Commands/Commands.sublime-commands b/Commands/Commands.sublime-commands old mode 100755 new mode 100644 index 01227e46..c9c816aa --- a/Commands/Commands.sublime-commands +++ b/Commands/Commands.sublime-commands @@ -1,69 +1,120 @@ [ - { - "caption": "Preferences: Debugger Settings", - "command": "edit_settings", - "args": { - "base_file": "${packages}/Debugger/debugger.sublime-settings" - } - }, - { - "caption" : "Debugger: Open in project", - "command" : "debugger_open" - }, - { - "caption" : "Debugger: Refresh phantoms", - "command" : "debugger_refresh_phantoms" - }, - { - "caption" : "Debugger: Quit", - "command" : "debugger_quit" - }, - { - "caption" : "Debugger: Start", - "command" : "debugger_start" - }, - { - "caption" : "Debugger: Stop", - "command" : "debugger_stop" - }, - { - "caption" : "Debugger: Step Over", - "command" : "debugger_step_over" - }, - { - "caption" : "Debugger: Step Out", - "command" : "debugger_step_out" - }, - { - "caption" : "Debugger: Step In", - "command" : "debugger_step_in" - }, - { - "caption" : "Debugger: Pause", - "command" : "debugger_pause" - }, - { - "caption" : "Debugger: Resume", - "command" : "debugger_resume" - }, - { - "caption" : "Debugger: Run Command", - "command" : "debugger_run_command" - }, - { - "caption" : "Debugger: Toggle Breakpoint", - "command" : "debugger_toggle_breakpoint" - }, - { - "caption" : "Debugger: Configurations", - "command" : "debugger_change_configuration" - }, - { - "caption" : "Debugger: Install Adapters", - "command" : "debugger_install_adapter" - }, - { - "caption" : "_", - "command" : "debugger_input" - }, + { + "command": "edit_settings", + "args": { + "base_file": "${packages}/Debugger/debugger.sublime-settings" + }, + "caption": "Preferences: Debugger Settings" + }, + { + "command": "debugger", + "args": { + "action": "open" + }, + "caption": "Debugger: Open" + }, + { + "command": "debugger", + "args": { + "action": "quit" + }, + "caption": "Debugger: Quit" + }, + { + "id": "debugger", + "caption": "-" + }, + { + "command": "debugger", + "args": { + "action": "install_adapters" + }, + "caption": "Debugger: Install Adapters" + }, + { + "command": "debugger", + "args": { + "action": "change_configuration" + }, + "caption": "Debugger: Change Configuration" + }, + { + "id": "debugger", + "caption": "-" + }, + { + "command": "debugger", + "args": { + "action": "start" + }, + "caption": "Debugger: Start" + }, + { + "command": "debugger", + "args": { + "action": "stop" + }, + "caption": "Debugger: Stop" + }, + { + "id": "debugger", + "caption": "-" + }, + { + "command": "debugger", + "args": { + "action": "pause" + }, + "caption": "Debugger: Pause" + }, + { + "command": "debugger", + "args": { + "action": "continue" + }, + "caption": "Debugger: Continue" + }, + { + "command": "debugger", + "args": { + "action": "step_over" + }, + "caption": "Debugger: Step Over" + }, + { + "command": "debugger", + "args": { + "action": "step_in" + }, + "caption": "Debugger: Step In" + }, + { + "command": "debugger", + "args": { + "action": "step_out" + }, + "caption": "Debugger: Step Out" + }, + { + "command": "debugger", + "args": { + "action": "run_command" + }, + "caption": "Debugger: Run Command" + }, + { + "id": "debugger", + "caption": "-" + }, + { + "command": "debugger", + "args": { + "action": "refresh_phantoms" + }, + "caption": "Debugger: Refresh Phantoms" + }, + { + "command": "debugger_input", + "caption": "_" + } ] \ No newline at end of file diff --git a/Commands/Context.sublime-menu b/Commands/Context.sublime-menu index 5d82a305..fe3c6e70 100755 --- a/Commands/Context.sublime-menu +++ b/Commands/Context.sublime-menu @@ -1,8 +1,24 @@ [ - { "caption": "-", "id": "debugger_start" }, - { - "caption": "Toggle breakpoint", - "command": "debugger_toggle_breakpoint" - }, - { "caption": "-", "id": "debugger_end" } + { + "id": "debugger", + "caption": "-" + }, + { + "command": "debugger", + "args": { + "action": "toggle_breakpoint" + }, + "caption": "Toggle Breakpoint" + }, + { + "command": "debugger", + "args": { + "action": "run_to_current_line" + }, + "caption": "Run To Cursor" + }, + { + "id": "debugger", + "caption": "-" + } ] \ No newline at end of file diff --git a/Commands/Main.sublime-menu b/Commands/Main.sublime-menu old mode 100755 new mode 100644 index d294eaa7..dbd78767 --- a/Commands/Main.sublime-menu +++ b/Commands/Main.sublime-menu @@ -1,61 +1,122 @@ [ - { "caption": "Debugger", "id": "debugger", - "children" : [ - { - "caption" : "Open (In project)", - "command" : "debugger_open", - }, - { - "caption" : "Quit", - "command" : "debugger_quit", - }, - { - "caption": "Settings", - "command": "edit_settings", - "args": { - "base_file": "${packages}/Debugger/debugger.sublime-settings" - } - }, - { "caption": "-"}, - { - "caption" : "Configurations", - "command" : "debugger_change_configuration" - }, - { - "caption" : "Install Adapters", - "command" : "debugger_install_adapter", - }, - { "caption": "-"}, - { - "caption" : "Start", - "command" : "debugger_start", - }, - { - "caption" : "Stop", - "command" : "debugger_stop" - }, - { "caption": "-"}, - { - "caption" : "Step Over", - "command" : "debugger_step_over" - }, - { - "caption" : "Step Out", - "command" : "debugger_step_out" - }, - { - "caption" : "Step In", - "command" : "debugger_step_in" - }, - { - "caption" : "Pause", - "command" : "debugger_pause" - }, - { - "caption" : "Resume", - "command" : "debugger_resume" - }, - { "caption": "-", "id": "debugger_end" }, - ], - }, + { + "children": [ + { + "command": "debugger", + "args": { + "action": "open" + }, + "caption": "Open" + }, + { + "command": "debugger", + "args": { + "action": "quit" + }, + "caption": "Quit" + }, + { + "command": "edit_settings", + "args": { + "base_file": "${packages}/Debugger/debugger.sublime-settings" + }, + "caption": "Settings" + }, + { + "id": "debugger", + "caption": "-" + }, + { + "command": "debugger", + "args": { + "action": "install_adapters" + }, + "caption": "Install Adapters" + }, + { + "command": "debugger", + "args": { + "action": "change_configuration" + }, + "caption": "Change Configuration" + }, + { + "id": "debugger", + "caption": "-" + }, + { + "command": "debugger", + "args": { + "action": "start" + }, + "caption": "Start" + }, + { + "command": "debugger", + "args": { + "action": "stop" + }, + "caption": "Stop" + }, + { + "id": "debugger", + "caption": "-" + }, + { + "command": "debugger", + "args": { + "action": "pause" + }, + "caption": "Pause" + }, + { + "command": "debugger", + "args": { + "action": "continue" + }, + "caption": "Continue" + }, + { + "command": "debugger", + "args": { + "action": "step_over" + }, + "caption": "Step Over" + }, + { + "command": "debugger", + "args": { + "action": "step_in" + }, + "caption": "Step In" + }, + { + "command": "debugger", + "args": { + "action": "step_out" + }, + "caption": "Step Out" + }, + { + "command": "debugger", + "args": { + "action": "run_command" + }, + "caption": "Run Command" + }, + { + "id": "debugger", + "caption": "-" + }, + { + "command": "debugger", + "args": { + "action": "refresh_phantoms" + }, + "caption": "Refresh Phantoms" + } + ], + "id": "debugger", + "caption": "Debugger" + } ] \ No newline at end of file diff --git a/debug_adapters/php.json b/debug_adapters/php.json index 09bf606d..0253cad8 100644 --- a/debug_adapters/php.json +++ b/debug_adapters/php.json @@ -3,7 +3,6 @@ "node", "${package}/data/debug_adapters/vscode-php/extension/out/phpDebug.js" ], - "hover_word_regex_match" : "\\$[a-zA-Z0-9_]*", "hover_word_seperators" : "./\\()\"'-:,.;<>~!@#%^&*|+=[]{}`~?.", "installation": { diff --git a/modules/commands/debugger.py b/modules/commands/debugger.py index e615049d..18a0d437 100644 --- a/modules/commands/debugger.py +++ b/modules/commands/debugger.py @@ -36,102 +36,6 @@ def run_main(self) -> None: def on_main(self, main: DebuggerInterface) -> None: pass -class DebuggerCommand(RunDebuggerInterfaceCommand): - def is_visible(self) -> bool: - return DebuggerInterface.for_window(self.window) is not None - - -class DebuggerOpenCommand(RunDebuggerInterfaceCommand): - def run_main(self) -> None: - main = DebuggerInterface.for_window(self.window, True) - assert main - main.show() - - -class DebuggerToggleBreakpointCommand(DebuggerCommand): - def on_main(self, main: DebuggerInterface) -> None: - view = self.window.active_view() - x, y = view.rowcol(view.sel()[0].begin()) - line = x + 1 - file = view.file_name() - breakpoint = main.breakpoints.get_breakpoint(file, line) - if breakpoint is not None: - main.breakpoints.remove_breakpoint(breakpoint) - else: - main.breakpoints.add_breakpoint(file, line) - - -class DebuggerQuitCommand(RunDebuggerInterfaceCommand): - def on_main(self, main: DebuggerInterface) -> None: - main.dispose() - -class DebuggerStartCommand(RunDebuggerInterfaceCommand): - def on_main(self, main: DebuggerInterface) -> None: - main.on_play() - -class DebuggerStopCommand(DebuggerCommand): - def on_main(self, main: DebuggerInterface) -> None: - main.on_stop() - - def is_enabled(self) -> bool: - return not DebuggerInState(self.window, DebuggerStateful.stopped) - - -class DebuggerPauseCommand(DebuggerCommand): - def on_main(self, main: DebuggerInterface) -> None: - main.on_pause() - - def is_enabled(self) -> bool: - return DebuggerInState(self.window, DebuggerStateful.running) - - -class DebuggerStepOverCommand(DebuggerCommand): - def on_main(self, main: DebuggerInterface) -> None: - main.on_step_over() - - def is_enabled(self) -> bool: - return DebuggerInState(self.window, DebuggerStateful.paused) - - -class DebuggerStepInCommand(DebuggerCommand): - def on_main(self, main: DebuggerInterface) -> None: - main.on_step_in() - - def is_enabled(self) -> bool: - return DebuggerInState(self.window, DebuggerStateful.paused) - - -class DebuggerStepOutCommand(DebuggerCommand): - def on_main(self, main: DebuggerInterface) -> None: - main.on_step_out() - - def is_enabled(self) -> bool: - return DebuggerInState(self.window, DebuggerStateful.paused) - - -class DebuggerResumeCommand(DebuggerCommand): - def on_main(self, main: DebuggerInterface) -> None: - main.on_resume() - - def is_enabled(self) -> bool: - return DebuggerInState(self.window, DebuggerStateful.paused) - - -class DebuggerRunCommandCommand(DebuggerCommand): - def on_main(self, main: DebuggerInterface) -> None: - main.open_repl_console() - - -class DebuggerRefreshPhantomsCommand(RunDebuggerInterfaceCommand): - def on_main(self, main: DebuggerInterface) -> None: - print("Refresh phantoms") - main.refresh_phantoms() - -class DebuggerChangeConfigurationCommand(DebuggerCommand): - def on_main(self, debugger: 'DebuggerInterface') -> None: - select_configuration.run(debugger) - - class DebuggerShowLineCommand(sublime_plugin.TextCommand): def run(self, edit, line: int, move_cursor: bool): a = self.view.text_point(line, 0) diff --git a/modules/commands/install_adapters_menu.py b/modules/commands/install_adapters_menu.py index 083b2137..3d3282e6 100644 --- a/modules/commands/install_adapters_menu.py +++ b/modules/commands/install_adapters_menu.py @@ -7,7 +7,6 @@ from ..debugger.adapter_configuration import AdapterConfiguration, install_adapter from ..debugger.debugger_interface import DebuggerInterface -from .debugger import DebuggerCommand def open_install_adapter_menu(debugger: DebuggerInterface, selected_index = 0): values = [] @@ -28,16 +27,15 @@ def input (selected_index): @core.async def run_async(list, adapter): - - debugger.console_panel.Add("installing debug adapter...") + debugger.terminal.log_info("installing debug adapter...") try: yield from install_adapter(adapter) except Exception as e: - debugger.console_panel.Add(str(e)) - debugger.console_panel.Add("... debug adapter installed failed") + debugger.terminal.log_error(str(e)) + debugger.terminal.log_error("... debug adapter installed failed") finally: adapter.installing = False - debugger.console_panel.Add("... debug adapter installed") + debugger.terminal.log_info("... debug adapter installed") open_install_adapter_menu(debugger, list) @@ -52,8 +50,3 @@ def run_not_main(list): ui.run_input_command(input(selected_index), run) ui.run_input_command(input(selected_index), run, run_not_main=run_not_main) - - -class DebuggerInstallAdapter(DebuggerCommand): - def on_main(self, debugger: DebuggerInterface) -> None: - open_install_adapter_menu(debugger) diff --git a/modules/components/breakpoints_panel.py b/modules/components/breakpoints_panel.py index e32234f5..1849b28e 100644 --- a/modules/components/breakpoints_panel.py +++ b/modules/components/breakpoints_panel.py @@ -39,7 +39,7 @@ class BreakpointsPanel(ui.Block): def __init__(self, breakpoints: Breakpoints, on_expand: Callable[[Breakpoint], None]) -> None: super().__init__() self.breakpoints = breakpoints - self.selected = None + self.selected = None #type: Optional[Breakpoint] # FIXME put in on activate/deactivate self.breakpoints.onUpdatedFunctionBreakpoint.add(self._updated) self.breakpoints.onUpdatedBreakpoint.add(self._updated) @@ -91,10 +91,11 @@ def on_click(filter=filter): ]), ui.Label(filter.name, color=color, padding_left=0.25, width=13.6, align=0) )) - for breakpoint in self.breakpoints.functionBreakpoints: - color = colors[breakpoint == self.selected] - name = breakpoint.name - i = self.item(breakpoint, breakpoint.image(), name, "ƒn", breakpoint.enabled) + + for function_breakpoint in self.breakpoints.functionBreakpoints: + color = colors[function_breakpoint == self.selected] + name = function_breakpoint.name + i = self.item(function_breakpoint, function_breakpoint.image(), name, "ƒn", function_breakpoint.enabled) items.append(i) for breakpoint in self.breakpoints.breakpoints: diff --git a/modules/components/callstack_panel.py b/modules/components/callstack_panel.py index 97178261..27f47d5a 100644 --- a/modules/components/callstack_panel.py +++ b/modules/components/callstack_panel.py @@ -2,13 +2,9 @@ import os -from .. import ui -from .. import core +from .. import core, ui, dap from ..debugger.debugger import ( - Thread, - StackFrame, - DebugAdapterClient, DebuggerStateful, ThreadStateful ) @@ -109,7 +105,7 @@ def on_click(index=index): class StackFrameComponent (ui.Block): - def __init__(self, frame: StackFrame, on_click: Callable[[], None]) -> None: + def __init__(self, frame: dap.StackFrame, on_click: Callable[[], None]) -> None: super().__init__() self.frame = frame self.on_click = on_click @@ -117,7 +113,7 @@ def __init__(self, frame: StackFrame, on_click: Callable[[], None]) -> None: def render(self) -> ui.Block.Children: frame = self.frame name = os.path.basename(frame.file) - if frame.presentation == StackFrame.subtle: + if frame.presentation == dap.StackFrame.subtle: color = "secondary" else: color = "primary" diff --git a/modules/components/debugger_panel.py b/modules/components/debugger_panel.py index b5b72bd6..87c99c30 100644 --- a/modules/components/debugger_panel.py +++ b/modules/components/debugger_panel.py @@ -38,14 +38,11 @@ def __init__(self, callbacks: DebuggerPanelCallbacks, breakpoints: ui.Block) -> self.callbacks = callbacks self.name = '' self.breakpoints = breakpoints + def setState(self, state: int) -> None: self.state = state self.dirty() - def set_name(self, name: str) -> None: - self.name = name - self.dirty() - def render(self) -> ui.Block.Children: buttons = [] #type: List[ui.Block] @@ -86,14 +83,10 @@ def render(self) -> ui.Block.Children: items.append( DebuggerItem(self.callbacks.on_play, ui.Img(ui.Images.shared.play)) ) - if stop: - items.append( - DebuggerItem(self.callbacks.on_stop, ui.Img(ui.Images.shared.stop)) - ) - else: - items.append( - DebuggerItem(self.callbacks.on_stop, ui.Img(ui.Images.shared.stop_disable)) - ) + + items.append( + DebuggerItem(self.callbacks.on_stop, ui.Img(ui.Images.shared.stop), ui.Img(ui.Images.shared.stop_disable)) + ) if not controls: items.append( @@ -108,18 +101,12 @@ def render(self) -> ui.Block.Children: DebuggerItem(self.callbacks.on_resume, ui.Img(ui.Images.shared.resume)) ) - if controls: - items.extend([ - DebuggerItem(self.callbacks.on_step_over, ui.Img(ui.Images.shared.down)), - DebuggerItem(self.callbacks.on_step_out, ui.Img(ui.Images.shared.left)), - DebuggerItem(self.callbacks.on_step_in, ui.Img(ui.Images.shared.right)), - ]) - else: - items.extend([ - DebuggerItem(self.callbacks.on_step_over, ui.Img(ui.Images.shared.down_disable)), - DebuggerItem(self.callbacks.on_step_out, ui.Img(ui.Images.shared.left_disable)), - DebuggerItem(self.callbacks.on_step_in, ui.Img(ui.Images.shared.right_disable)), - ]) + items.extend([ + DebuggerItem(self.callbacks.on_step_over, ui.Img(ui.Images.shared.down), ui.Img(ui.Images.shared.down_disable)), + DebuggerItem(self.callbacks.on_step_out, ui.Img(ui.Images.shared.left), ui.Img(ui.Images.shared.left_disable)), + DebuggerItem(self.callbacks.on_step_in, ui.Img(ui.Images.shared.right), ui.Img(ui.Images.shared.right_disable)), + ]) + items_new = [] for item in items: @@ -131,11 +118,17 @@ def render(self) -> ui.Block.Children: class DebuggerItem (ui.Inline): - def __init__(self, callback: Callable[[], None], image: ui.Img) -> None: + def __init__(self, callback: Callable[[], None], enabled_image: ui.Img, disabled_image: Optional[ui.Img] = None) -> None: super().__init__() - self.image = image + + if not callback.enabled() and disabled_image: + self.image = disabled_image + else: + self.image = enabled_image + self.callback = callback + def render(self) -> ui.Inline.Children: return [ ui.Padding(ui.Button(self.callback, items=[self.image]), left=0.6, right=0.6, top=0.0, bottom=0.0) diff --git a/modules/components/selected_line.py b/modules/components/selected_line.py index 3a25b708..05be8ff2 100644 --- a/modules/components/selected_line.py +++ b/modules/components/selected_line.py @@ -31,9 +31,10 @@ def render(self) -> ui.Block.Children: class SelectedLine: def __init__(self, view: sublime.View, line: int, text: str): - pt_current_line = view.text_point(line, 0) - pt_prev_line = view.text_point(line - 1, 0) - pt_next_line = view.text_point(line + 1, 0) + # note sublime lines are 0 based not 1 based + pt_current_line = view.text_point(line - 1, 0) + pt_prev_line = view.text_point(line - 2, 0) + pt_next_line = view.text_point(line, 0) line_prev = view.line(pt_current_line) line_current = view.line(pt_prev_line) diff --git a/modules/components/variables_panel.py b/modules/components/variables_panel.py index 09a4f111..9f6902d7 100644 --- a/modules/components/variables_panel.py +++ b/modules/components/variables_panel.py @@ -1,13 +1,6 @@ from ..typecheck import * -from .. import ui -from .. import core - -from ..debugger.debugger import ( - Scope, - Thread, - DebugAdapterClient -) +from .. import core, ui, dap from .variable_component import Variable, VariableStateful, VariableStatefulComponent from .layout import variables_panel_width @@ -16,13 +9,13 @@ class VariablesPanel (ui.Block): def __init__(self) -> None: super().__init__() - self.scopes = [] #type: List[Scope] + self.scopes = [] #type: List[dap.Scope] def clear(self) -> None: self.scopes = [] self.dirty() - def set_scopes(self, scopes: List[Scope]) -> None: + def set_scopes(self, scopes: List[dap.Scope]) -> None: self.scopes = scopes self.dirty() diff --git a/modules/core/__init__.py b/modules/core/__init__.py index f1124de5..b1fcd811 100644 --- a/modules/core/__init__.py +++ b/modules/core/__init__.py @@ -4,7 +4,8 @@ from .log import * from .event import Handle, Event, EventDispatchMain from . import platform - +from .error import Error +from .dispose import Disposables _current_package = "" def current_package() -> str: diff --git a/modules/core/core.py b/modules/core/core.py index d38cc3f0..09ff33f7 100644 --- a/modules/core/core.py +++ b/modules/core/core.py @@ -15,7 +15,7 @@ CancelledError = asyncio.CancelledError _main_executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) -_main_loop = None +_main_loop = None #type: asyncio.BaseDefaultEventLoopPolicy _main_thread = None def _create_main_loop(): @@ -75,23 +75,23 @@ def stop_event_loop() -> None: def all_methods(decorator): - def decorate(cls): - for attribute in cls.__dict__: - if callable(getattr(cls, attribute)): - setattr(cls, attribute, decorator(getattr(cls, attribute))) - return cls - return decorate + def decorate(cls): + for attribute in cls.__dict__: + if callable(getattr(cls, attribute)): + setattr(cls, attribute, decorator(getattr(cls, attribute))) + return cls + return decorate '''decorator for requiring that a function must be run in the background''' def require_main_thread(function): - def wrapper(*args, **kwargs): - assert_main_thread() - return function(*args, **kwargs) - return wrapper + def wrapper(*args, **kwargs): + assert_main_thread() + return function(*args, **kwargs) + return wrapper -def run(awaitable: awaitable[T], on_done: Callable[[T], None] = None, on_error: Callable[[Exception], None] = None) -> None: +def run(awaitable: awaitable[T], on_done: Callable[[T], None] = None, on_error: Callable[[Exception], None] = None) -> asyncio.Future: task = asyncio.ensure_future(awaitable, loop=_main_loop) def done(task) -> None: diff --git a/modules/core/dispose.py b/modules/core/dispose.py new file mode 100644 index 00000000..ef0d1cae --- /dev/null +++ b/modules/core/dispose.py @@ -0,0 +1,24 @@ +from ..typecheck import * +from .log import log_exception + +class Disposables: + def __init__(self): + self.disposables = {} #type: Dict[int, Any] + + def __iadd__(self, disposable) -> Any: + try: + disposable.dispose + except AttributeError: + log_exception("expected dispose() function") + + self.disposables[id(disposable)] = disposable + return self + + def __isub__(self, disposable) -> Any: + self.disposables[id(disposable)].dispose() + return self + + def dispose(self): + for value in self.disposables.values(): + value.dispose() + self.disposables.clear() \ No newline at end of file diff --git a/modules/core/error.py b/modules/core/error.py new file mode 100644 index 00000000..3f5a6b19 --- /dev/null +++ b/modules/core/error.py @@ -0,0 +1,4 @@ + +class Error(Exception): + def __init__(self, format: str): + super().__init__(format) diff --git a/modules/dap/__init__.py b/modules/dap/__init__.py index cc91e6ce..c6c0fa9c 100644 --- a/modules/dap/__init__.py +++ b/modules/dap/__init__.py @@ -1,4 +1,4 @@ -from .client import DebugAdapterClient -from .transport import Process, TCPTransport, StdioTransport +from .client import DebugAdapterClient as Client +from .transport import Process, Transport, StdioTransport from .types import * \ No newline at end of file diff --git a/modules/dap/transport.py b/modules/dap/transport.py index eddddfe8..cc39c6c5 100644 --- a/modules/dap/transport.py +++ b/modules/dap/transport.py @@ -46,8 +46,8 @@ def __init__(self, command: List[str], on_stdout: Optional[Callable[[str], None] # Hide the console window on Windows startupinfo = None if os.name == "nt": - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo = subprocess.STARTUPINFO() #type: ignore + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW #type: ignore self.process = subprocess.Popen(command, stdout=subprocess.PIPE, @@ -135,120 +135,8 @@ def dispose(self) -> None: assert False -STATE_HEADERS = 0 -STATE_CONTENT = 1 -TCP_CONNECT_TIMEOUT = 5 CONTENT_HEADER = b"Content-Length: " - -class TCPTransport(Transport): - def __init__(self, s: socket.socket) -> None: - self.socket = s # type: 'Optional[socket.socket]' - self.send_queue = Queue() # type: Queue[Optional[str]] - - def start(self, on_receive: Callable[[str], None], on_closed: Callable[[], None]) -> None: - self.on_receive = on_receive - self.on_closed = on_closed - self.read_thread = threading.Thread(target=self.read_socket) - self.read_thread.start() - self.write_thread = threading.Thread(target=self.write_socket) - self.write_thread.start() - - def close(self) -> None: - if self.socket == None: - return - self.send_queue.put(None) # kill the write thread as it's blocked on send_queue - self.socket = None - core.call_soon_threadsafe(self.on_closed) - - def dispose(self) -> None: - self.close() - - def read_socket(self) -> None: - remaining_data = b"" - is_incomplete = False - read_state = STATE_HEADERS - content_length = 0 - while self.socket: - is_incomplete = False - try: - received_data = self.socket.recv(4096) - except Exception as err: - print("Failure reading from socket", err) - self.close() - break - - if not received_data: - print("no data received, closing") - self.close() - break - - data = remaining_data + received_data - remaining_data = b"" - while len(data) > 0 and not is_incomplete: - if read_state == STATE_HEADERS: - headers, _sep, rest = data.partition(b"\r\n\r\n") - if len(_sep) < 1: - is_incomplete = True - remaining_data = data - else: - for header in headers.split(b"\r\n"): - if header.startswith(CONTENT_HEADER): - header_value = header[len(CONTENT_HEADER):] - content_length = int(header_value) - read_state = STATE_CONTENT - data = rest - - if read_state == STATE_CONTENT: - # read content bytes - if len(data) >= content_length: - content = data[:content_length] - message = content.decode("UTF-8") - core.call_soon_threadsafe(self.on_receive, message) - data = data[content_length:] - read_state = STATE_HEADERS - else: - is_incomplete = True - remaining_data = data - - def send(self, message: str) -> None: - self.send_queue.put(message) - - def write_socket(self) -> None: - while self.socket: - message = self.send_queue.get() - if message is None: - break - else: - try: - self.socket.sendall(bytes('Content-Length: {}\r\n\r\n'.format(len(message)), 'UTF-8')) - self.socket.sendall(bytes(message, 'UTF-8')) - core.log_info(' << ', message) - except Exception as err: - print("Failure writing to socket", err) - self.close() - - -# starts the tcp connection in a none blocking fashion -@core.async -def start_tcp_transport(host: str, port: int) -> core.awaitable[TCPTransport]: - def start_tcp_transport_inner() -> TCPTransport: - print('connecting to {}:{}'.format(host, port)) - start_time = time.time() - while time.time() - start_time < TCP_CONNECT_TIMEOUT: - try: - sock = socket.create_connection((host, port)) - transport = TCPTransport(sock) - return transport - except ConnectionRefusedError as e: - pass - raise Exception("Timeout connecting to socket") - - print('connecting to {}:{}'.format(host, port)) - transport = yield from core.run_in_executor(start_tcp_transport_inner) - return transport - - class StdioTransport(Transport): def __init__(self, process: Process) -> None: assert process.on_stdout == None, 'expected process to not read stdout' diff --git a/modules/debugger/breakpoint_commands.py b/modules/debugger/breakpoint_commands.py new file mode 100644 index 00000000..c8b80e5a --- /dev/null +++ b/modules/debugger/breakpoint_commands.py @@ -0,0 +1,55 @@ +from ..typecheck import * +from ..import core, ui, dap + +import sublime + +from .debugger import DebuggerStateful +from .debugger_project import DebuggerProject +from .breakpoints import Breakpoints + +class BreakpointCommandsProvider(core.Disposables): + def __init__(self, project: DebuggerProject, debugger: DebuggerStateful, breakpoints: Breakpoints): + super().__init__() + + self.run_to_line_breakpoint = None + self.breakpoints = breakpoints + self.debugger = debugger + self.project = project + + self += self.debugger.state_changed.add(self.on_debugger_state_change) + + def current_file_line(self) -> Tuple[str, int]: + view = self.project.window.active_view() + x, y = view.rowcol(view.sel()[0].begin()) + line = x + 1 + file = self.project.source_file(view) + if not file: + raise core.Error("No source file selected, either no selection in current window or file is not saved") + + return file, line + + def clear_run_to_line(self): + if self.run_to_line_breakpoint: + self.breakpoints.remove_breakpoint(self.run_to_line_breakpoint) + self.run_to_line_breakpoint = None + + def run_to_current_line(self): + file, line = self.current_file_line() + self.clear_run_to_line() + if self.debugger.state != DebuggerStateful.paused: + raise core.Error("Debugger not paused") + + self.run_to_line_breakpoint = self.breakpoints.add_breakpoint(file, line) + core.run(self.debugger.resume()) + + def on_debugger_state_change(self): + if self.debugger.state != DebuggerStateful.running: + self.clear_run_to_line() + + def toggle_current_line(self): + file, line = self.current_file_line() + bp = self.breakpoints.get_breakpoint(file, line) + if bp: + self.breakpoints.remove_breakpoint(bp) + else: + self.breakpoints.add_breakpoint(file, line) \ No newline at end of file diff --git a/modules/debugger/breakpoints.py b/modules/debugger/breakpoints.py index f435c445..b80e0232 100644 --- a/modules/debugger/breakpoints.py +++ b/modules/debugger/breakpoints.py @@ -323,6 +323,7 @@ def get_breakpoint(self, file: str, line: int) -> Optional[Breakpoint]: def add_breakpoint(self, file: str, line: int): b = Breakpoint(file, line, True) self.add(b) + return b def add(self, breakpoint: Breakpoint): self.breakpoints.append(breakpoint) diff --git a/modules/debugger/commands.py b/modules/debugger/commands.py new file mode 100644 index 00000000..3de9b4b2 --- /dev/null +++ b/modules/debugger/commands.py @@ -0,0 +1,243 @@ +from ..typecheck import * + +import sublime +import sublime_plugin +import json + +from .. import core +from .debugger_interface import DebuggerInterface +from .debugger import DebuggerStateful +from ..commands.install_adapters_menu import open_install_adapter_menu +from ..commands import select_configuration + + +# commands look like... + +""" +"command": "debugger_command", +"args": { + action": "..." +} +""" + +# window.run_command("debugger_run", {"action": "generate_commands"}) + +actions_window = [ + { + "action": "open", + "caption": "Open", + "opens": True, + }, + { + "action": "quit", + "caption": "Quit", + "run": lambda window, debugger: debugger.dispose(), + }, + { "caption": "-" }, + { + "action": "install_adapters", + "caption": "Install Adapters", + "run": lambda window, debugger: open_install_adapter_menu(debugger), + "opens": True, + }, + { + "action": "change_configuration", + "caption": "Change Configuration", + "run": lambda window, debugger: select_configuration.run(debugger), + }, + { "caption": "-" }, + { + "action": "start", + "caption": "Start", + "command": lambda window, debugger: debugger.on_play, + "opens": True + }, + { + "action": "stop", + "caption": "Stop", + "command": lambda window, debugger: debugger.on_stop, + }, + { "caption": "-" }, + { + "action": "pause", + "caption": "Pause", + "command": lambda window, debugger: debugger.on_pause, + }, + { + "action": "continue", + "caption": "Continue", + "command": lambda window, debugger: debugger.on_resume, + }, + { + "action": "step_over", + "caption": "Step Over", + "command": lambda window, debugger: debugger.on_step_over, + }, + { + "action": "step_in", + "caption": "Step In", + "command": lambda window, debugger: debugger.on_step_in, + }, + { + "action": "step_out", + "caption": "Step Out", + "command": lambda window, debugger: debugger.on_step_out, + }, + { + "action": "run_command", + "caption": "Run Command", + "command": lambda window, debugger: debugger.on_run_command, + }, + { "caption": "-" }, + { + "action": "refresh_phantoms", + "caption": "Refresh Phantoms", + "run": lambda window, debugger: debugger.refresh_phantoms(), + }, +] #type: List[Dict[str, Any]] + + +actions_context = [ + { "caption": "-" }, + { + "action": "toggle_breakpoint", + "caption": "Toggle Breakpoint", + "command": lambda window, debugger: debugger.toggle_breakpoint, + "opens": True, + }, + { + "action": "run_to_current_line", + "caption": "Run To Cursor", + "command": lambda window, debugger: debugger.run_to_current_line, + }, + { "caption": "-" }, +] + +actions_window_map = {} #type: Dict[str, Dict[str, Any]] +for actions in (actions_window, actions_context): + for action in actions: + action_name = action.get('action') + if action_name: + actions_window_map[action_name] = action + +class DebuggerCommand (sublime_plugin.WindowCommand): + def run(self, action: str): #type: ignore + if action == "generate_commands": + generate_commands_and_menus() + return + + core.call_soon_threadsafe(self.run_main, actions_window_map[action]) + + def run_main(self, action: dict): + debugger_interface = DebuggerInterface.for_window(self.window, create=action.get('opens', False)) + + command = action.get('command') + if command: + result = command(self.window, debugger_interface) + result() + + run = action.get('run') + if run: + run(self.window, debugger_interface) + + def is_enabled(self, action: str): #type: ignore + if action == "generate_commands": + return True + action_item = actions_window_map[action] + + + opens = action_item.get('opens', False) + if opens: + return True + + debugger_interface = DebuggerInterface.for_window(self.window) + if not debugger_interface: + return False + + command = action_item.get('command') + if command: + result = command(self.window, debugger_interface) + return result.enabled() + + return True + + def is_visible(self, action: str): #type: ignore + if action == "generate_commands": + return True + + action_item = actions_window_map[action] + opens = action_item.get('opens', False) + return opens or DebuggerInterface.for_window(self.window) != None + + +def generate_commands_and_menus(): + current_package = core.current_package() + + preferences = { + "caption": "Preferences: Debugger Settings", + "command": "edit_settings", + "args": { + "base_file": "${packages}/Debugger/debugger.sublime-settings" + } + } + settings = { + "caption": "Settings", + "command": "edit_settings", + "args": { + "base_file": "${packages}/Debugger/debugger.sublime-settings" + } + } + + + def generate_commands(actions, commands, prefix =""): + for action in actions: + if action['caption'] == '-': + commands.append({"caption" : action['caption'], "id": "debugger"}) + continue + + commands.append( + { + "caption" : prefix + action['caption'], + "command" : "debugger", + "args": { + "action": action['action'], + } + } + ) + + commands_palette = [] + generate_commands(actions_window, commands_palette, prefix="Debugger: ") + + commands_palette.insert(0, preferences) + + # hidden command used for gathering input from the command palette + input = { + "caption" : "_", + "command" : "debugger_input" + } + commands_palette.append(input) + + with open(current_package + '/Commands/Commands.sublime-commands', 'w') as file: + json.dump(commands_palette, file, indent=4, separators=(',', ': ')) + + + commands_menu = [] + generate_commands(actions_window, commands_menu) + commands_menu.insert(2, settings) + + main = [{ + "caption": "Debugger", + "id": "debugger", + "children" : commands_menu} + ] + with open(current_package + '/Commands/Main.sublime-menu', 'w') as file: + json.dump(main, file, indent=4, separators=(',', ': ')) + + print('Generating commands') + + + commands_context = [] + generate_commands(actions_context, commands_context) + + with open(current_package + '/Commands/Context.sublime-menu', 'w') as file: + json.dump(commands_context, file, indent=4, separators=(',', ': ')) diff --git a/modules/debugger/debugger.py b/modules/debugger/debugger.py index f9e68f5c..aac5ad73 100644 --- a/modules/debugger/debugger.py +++ b/modules/debugger/debugger.py @@ -2,38 +2,15 @@ import sublime -from .. import core +from .. import core, dap -from ..dap.client import ( - DebugAdapterClient, - StoppedEvent, - ContinuedEvent, - OutputEvent -) from ..dap.transport import ( - start_tcp_transport, Process, - TCPTransport, StdioTransport ) -from ..dap.types import ( - StackFrame, - EvaluateResponse, - Thread, - Scope, - Variable, - CompletionItem, - Error, - Capabilities, - ExceptionBreakpointsFilter, - Source, - ThreadEvent -) from .terminal import Terminal, TerminalProcess, TerminalStandard -from .. import dap - from .adapter_configuration import ( ConfigurationExpanded, AdapterConfiguration @@ -61,9 +38,9 @@ class DebuggerStateful: def __init__(self, breakpoints: Breakpoints, on_state_changed: Callable[[int], None], - on_scopes: Callable[[List[Scope]], None], - on_output: Callable[[OutputEvent], None], - on_selected_frame: Callable[[Optional[Thread], Optional[StackFrame]], None], + on_scopes: Callable[[List[dap.Scope]], None], + on_output: Callable[[dap.OutputEvent], None], + on_selected_frame: Callable[[Optional[dap.Thread], Optional[dap.StackFrame]], None], on_threads_stateful: Callable[[List[ThreadStateful]], None], on_terminals: Callable[[List[Terminal]], None], ) -> None: @@ -74,19 +51,19 @@ def __init__(self, self.on_selected_frame = on_selected_frame self.on_terminals = on_terminals - self.adapter = None #type: Optional[DebugAdapterClient] + self.adapter = None #type: Optional[dap.Client] self.process = None #type: Optional[Process] self.launching_async = None self.launch_request = True self.supports_terminate_request = True - self.selected_frame = None #type: Optional[StackFrame] + self.selected_frame = None #type: Optional[Union[dap.StackFrame, ThreadStateful]] self.selected_thread_explicitly = False - self.selected_threadstateful = None #type: Optional[Thread] + self.selected_threadstateful = None #type: Optional[ThreadStateful] - self.threads_stateful = [] - self.threads_stateful_from_id = {} + self.threads_stateful = [] #type: List[ThreadStateful] + self.threads_stateful_from_id = {} #type: Dict[int, ThreadStateful] self.terminals = [] #type: List[Terminal] self._state = DebuggerStateful.stopped @@ -97,6 +74,7 @@ def __init__(self, breakpoints.onSendBreakpointToDebugger.add(self.onSendBreakpointToDebugger) breakpoints.onSendFunctionBreakpointToDebugger.add(self.onSendFunctionBreakpointToDebugger) + self.state_changed = core.Event() #type: Event def dispose_terminals(self): for terminal in self.terminals: @@ -122,6 +100,7 @@ def state(self, state: int) -> None: self._state = state self.on_state_changed(state) + self.state_changed() def launch(self, adapter_configuration: AdapterConfiguration, configuration: ConfigurationExpanded) -> core.awaitable[None]: if self.launching_async: @@ -162,30 +141,9 @@ def _launch(self, adapter_configuration: AdapterConfiguration, configuration: Co yield from build.run(sublime.active_window(), terminal.write_stdout, configuration.all['sublime_debugger_build']) self.log_info('... finished running build') - # If there is a command to run for this debugger run it now - if adapter_configuration.tcp_port: - self.log_info('starting debug adapter...') - try: - self.process = Process(adapter_configuration.command, on_stdout=self.log_info, on_stderr=self.log_info) - except Exception as e: - self.log_error('Failed to start debug adapter process: {}'.format(e)) - self.log_error('Command in question: {}'.format(adapter_configuration.command)) - core.display('Failed to start debug adapter process: Check the Event Log for more details') - raise Exception("failed to start debug adapter process") - tcp_address = adapter_configuration.tcp_address or 'localhost' - try: - transport = yield from start_tcp_transport(tcp_address, adapter_configuration.tcp_port) - except Exception as e: - self.log_error('Failed to connect to debug adapter: {}'.format(e)) - self.log_error('address: {} port: {}'.format(tcp_address, adapter_configuration.tcp_port)) - core.display('Failed to connect to debug adapter: Check the Event Log for more details and messages from the debug adapter process?') - raise Exception("failed to start debug adapter process") - - self.log_info('... debug adapter started') - else: - # dont monitor stdout the StdioTransport users it - self.process = Process(adapter_configuration.command, on_stdout=None, on_stderr=self.log_info) - transport = StdioTransport(self.process) + # dont monitor stdout the StdioTransport uses it + self.process = Process(adapter_configuration.command, on_stdout=None, on_stderr=self.log_info) + transport = StdioTransport(self.process) def on_run_in_terminal(request: dap.RunInTerminalRequest) -> int: terminal = TerminalProcess(request.cwd, request.args) @@ -193,7 +151,7 @@ def on_run_in_terminal(request: dap.RunInTerminalRequest) -> int: self.on_terminals(self.terminals) return terminal.pid() - adapter = DebugAdapterClient(transport, + adapter = dap.Client(transport, on_run_in_terminal = on_run_in_terminal ) adapter.onThreads.add(self._on_threads_event) @@ -290,7 +248,7 @@ def stop(self) -> core.awaitable[None]: if self.supports_terminate_request: try: yield from self.adapter.Terminate() - except Error as e: + except dap.Error as e: yield from self.adapter.Disconnect() else: yield from self.adapter.Disconnect() @@ -310,6 +268,7 @@ def force_stop_adapter(self) -> None: self.threads_stateful_from_id = {} self.threads_stateful = [] + self.on_threads_stateful(self.threads_stateful) self.on_scopes([]) self.on_selected_frame(None, None) @@ -360,19 +319,19 @@ def evaluate(self, command: str) -> core.awaitable[None]: adapter = self.adapter response = yield from adapter.Evaluate(command, self.selected_frame, "repl") - event = OutputEvent("console", response.result, response.variablesReference) + event = dap.OutputEvent("console", response.result, response.variablesReference) self.on_output(event) def log_info(self, string: str) -> None: - output = OutputEvent("debugger.info", string + '\n', 0) + output = dap.OutputEvent("debugger.info", string + '\n', 0) self.on_output(output) def log_output(self, string: str) -> None: - output = OutputEvent("debugger.output", string + '\n', 0) + output = dap.OutputEvent("debugger.output", string + '\n', 0) self.on_output(output) def log_error(self, string: str) -> None: - output = OutputEvent("debugger.error", string + '\n', 0) + output = dap.OutputEvent("debugger.error", string + '\n', 0) self.on_output(output) # after a successfull launch/attach, stopped event, thread event we request all threads @@ -384,7 +343,7 @@ def async() -> core.awaitable[None]: self._update_threads(threads) core.run(async()) - def _update_threads(self, threads: Optional[List[Thread]]) -> None: + def _update_threads(self, threads: Optional[List[dap.Thread]]) -> None: self.threads_stateful = [] threads_stateful_from_id = {} @@ -405,10 +364,10 @@ def _update_threads(self, threads: Optional[List[Thread]]) -> None: self.update_selection_if_needed() self.on_threads_stateful(self.threads_stateful) - def _on_threads_event(self, event: ThreadEvent) -> None: + def _on_threads_event(self, event: dap.ThreadEvent) -> None: self.refresh_threads() - def _on_output_event(self, event: OutputEvent) -> None: + def _on_output_event(self, event: dap.OutputEvent) -> None: self.on_output(event) def _thread_for_command(self) -> ThreadStateful: @@ -448,7 +407,7 @@ def update_selection_if_needed(self, reload_scopes=True) -> None: def reload_scopes(self): frame = None - if isinstance(self.selected_frame, StackFrame): + if isinstance(self.selected_frame, dap.StackFrame): frame = self.selected_frame elif isinstance(self.selected_frame, ThreadStateful) and self.selected_frame.frames: @@ -460,7 +419,7 @@ def reload_scopes(self): core.run(self.adapter.GetScopes(frame), self.on_scopes) self.on_selected_frame(self.selected_threadstateful, frame) - def select_threadstateful(self, thread: ThreadStateful, frame: Optional[StackFrame]): + def select_threadstateful(self, thread: ThreadStateful, frame: Optional[dap.StackFrame]): self.selected_threadstateful = thread self.selected_threadstateful.fetch_if_needed() if frame: @@ -479,7 +438,7 @@ def _threadstateful_for_id(self, id: int) -> ThreadStateful: thread = ThreadStateful(self, id, None, self.adapter.allThreadsStopped) return thread - def _on_continued_event(self, event: ContinuedEvent) -> None: + def _on_continued_event(self, event: dap.ContinuedEvent) -> None: if not event: return @@ -499,7 +458,7 @@ def _on_continued_event(self, event: ContinuedEvent) -> None: self._refresh_state() - def _on_stopped_event(self, event: StoppedEvent) -> None: + def _on_stopped_event(self, event: dap.StoppedEvent) -> None: self.refresh_threads() event_thread = self._threadstateful_for_id(event.threadId) diff --git a/modules/debugger/debugger_interface.py b/modules/debugger/debugger_interface.py index 69792382..0500213c 100644 --- a/modules/debugger/debugger_interface.py +++ b/modules/debugger/debugger_interface.py @@ -6,9 +6,9 @@ import subprocess import re import json +import types -from .. import ui -from .. import core +from .. import core, ui, dap from ..components.variable_component import VariableStateful, VariableStatefulComponent, Variable from ..components.debugger_panel import DebuggerPanel, DebuggerPanelCallbacks, STOPPED, PAUSED, RUNNING, LOADING @@ -27,15 +27,12 @@ from ..debugger.debugger import ( DebuggerStateful, - OutputEvent, - StackFrame, - Scope, - Thread, - EvaluateResponse, - DebugAdapterClient, - Error, - Source ) + +from .debugger_project import ( + DebuggerProject +) + from .breakpoints import ( Breakpoints, Breakpoint, @@ -54,6 +51,10 @@ from .debugger_terminal import DebuggerTerminal from .. components.pages_panel import TabbedPanelItem +from .view_hover import ViewHoverProvider +from .view_selected_source import ViewSelectedSourceProvider +from .breakpoint_commands import BreakpointCommandsProvider + class Panels: def __init__(self, view: sublime.View, phantom_location: int, columns: int): self.panels = [] @@ -67,7 +68,7 @@ def __init__(self, view: sublime.View, phantom_location: int, columns: int): def add(self, panels: [TabbedPanelItem]): self.panels.extend(panels) self.layout() - + def modified(self, panel: TabbedPanelItem): column = panel.column row = panel.row @@ -87,7 +88,6 @@ def show(self, id: int): column = panel.column row = panel.row self.pages[column].show(row) - def layout(self): items = [] @@ -102,7 +102,6 @@ def layout(self): for i in range(0, self.columns): self.pages[i].update(items[i]) - class DebuggerInterface (DebuggerPanelCallbacks): instances = {} #type: Dict[int, DebuggerInterface] @@ -125,8 +124,10 @@ def for_window(window: sublime.Window, create: bool = False) -> 'Optional[Debugg main = DebuggerInterface(window) DebuggerInterface.instances[window.id()] = main return main - except Error as e: + except dap.Error as e: core.log_exception() + if create: + instance.show() return instance @staticmethod @@ -171,27 +172,20 @@ def __init__(self, window: sublime.Window) -> None: data.setdefault('settings', {}).setdefault('debug.configurations', []) window.set_project_data(data) break + self.project = DebuggerProject(window) autocomplete = Autocomplete.create_for_window(window) self.input_open = False self.window = window self.disposeables = [] #type: List[Any] - self.breakpoints = Breakpoints() - + + self.breakpoints = Breakpoints(); self.variables_panel = VariablesPanel() self.callstack_panel = CallStackPanel() - self.breakpoints_panel = BreakpointsPanel(self.breakpoints, self.onSelectedBreakpoint) - - def on_breakpoint_more(): - show_breakpoint_options(self.breakpoints) - - + self.breakpoints_panel = BreakpointsPanel(self.breakpoints, self.on_selected_breakpoint) self.debugger_panel = DebuggerPanel(self, self.breakpoints_panel) - self.selected_frame_line = None #type: Optional[SelectedLine] - self.selected_frame_generated_view = None #type: Optional[sublime.View] - def on_state_changed(state: int) -> None: if state == DebuggerStateful.stopped: self.breakpoints.clear_breakpoint_results() @@ -217,16 +211,16 @@ def on_state_changed(state: int) -> None: elif state == DebuggerStateful.stopping or state == DebuggerStateful.starting: self.debugger_panel.setState(LOADING) - def on_scopes(scopes: List[Scope]) -> None: + def on_scopes(scopes: List[dap.Scope]) -> None: self.variables_panel.set_scopes(scopes) - def on_selected_frame(thread: Optional[Thread], frame: Optional[StackFrame]) -> None: - if frame and thread: - self.run_async(self.navigate_to_frame(thread, frame)) + def on_selected_frame(thread: Optional[dap.Thread], frame: Optional[dap.StackFrame]) -> None: + if frame and thread and frame.source: + self.source_provider.select(frame.source, frame.line, thread.stopped_text) else: - self.dispose_selected_frame() + self.source_provider.clear() - def on_output(event: OutputEvent) -> None: + def on_output(event: dap.OutputEvent) -> None: self.terminal.program_output(self.debugger.adapter, event) def on_threads_stateful(threads: Any): @@ -272,15 +266,10 @@ def on_terminals(list: Any): self.load_configurations() - - - print('Creating a window: h') - self.disposeables.extend([ self.panel, ui.view_gutter_hovered.add(self.on_gutter_hovered), - ui.view_text_hovered.add(self.on_text_hovered), - self.breakpoints.onSelectedBreakpoint.add(self.onSelectedBreakpoint) + self.breakpoints.onSelectedBreakpoint.add(self.on_selected_breakpoint) ]) @@ -311,8 +300,14 @@ def on_terminals(list: Any): terminal_panel_item ]) - def update_panels(self): - pass + view_hover = ViewHoverProvider(self.project, self.debugger) + self.disposeables.append(view_hover) + + self.source_provider = ViewSelectedSourceProvider(self.project, self.debugger) + self.disposeables.append(self.source_provider) + + self.breakpoints_provider = BreakpointCommandsProvider(self.project, self.debugger, self.breakpoints) + self.disposeables.append(self.breakpoints_provider) def load_configurations(self) -> None: variables = extract_variables(self.window) @@ -359,11 +354,6 @@ def load_adapter(adapter_name, adapter_json): self.configurations = configurations self.configuration = self.persistance.load_configuration_option(configurations) - if self.configuration: - self.debugger_panel.set_name(self.configuration.name) - else: - self.debugger_panel.set_name('select config') - assert self.view self.view.settings().set('font_size', get_setting(self.view, 'ui_scale', 12)) @@ -377,106 +367,23 @@ def show(self) -> None: def changeConfiguration(self, configuration: Configuration): self.configuration = configuration self.persistance.save_configuration_option(configuration) - self.debugger_panel.set_name(configuration.name) - - @core.async - def LaunchDebugger(self) -> core.awaitable[None]: - self.show_console_panel() - self.terminal.clear() - self.terminal.log_info('Console cleared...') - try: - if not self.configuration: - self.terminal.log_error("Add or select a configuration to begin debugging") - select_configuration.run(self) - return - - configuration = self.configuration - - adapter_configuration = self.adapters.get(configuration.type) - if not adapter_configuration: - raise Exception('Unable to find debug adapter with the type name "{}"'.format(configuration.type)) - - except Exception as e: - core.log_exception() - core.display(e) - return - - variables = extract_variables(self.window) - configuration_expanded = ConfigurationExpanded(configuration, variables) - yield from self.debugger.launch(adapter_configuration, configuration_expanded) - - # TODO this could be made better - def is_source_file(self, view: sublime.View) -> bool: - return bool(view.file_name()) - - @core.async - def async_on_text_hovered(self, event: ui.HoverEvent) -> core.awaitable[None]: - if not self.is_source_file(event.view): - return - - if not self.debugger.adapter: - return - - hover_word_seperators = self.debugger.adapter_configuration.hover_word_seperators - hover_word_regex_match = self.debugger.adapter_configuration.hover_word_regex_match - - if hover_word_seperators: - word = event.view.expand_by_class(event.point, sublime.CLASS_WORD_START | sublime.CLASS_WORD_END, separators=hover_word_seperators) - else: - word = event.view.word(event.point) - - word_string = event.view.substr(word) - if not word_string: - return - - if hover_word_regex_match: - x = re.search(hover_word_regex_match, word_string) - if not x: - print("hover match discarded because it failed matching the hover pattern, ", word_string) - return - word_string = x.group() - - try: - response = yield from self.debugger.adapter.Evaluate(word_string, self.debugger.selected_frame, 'hover') - variable = Variable(self.debugger.adapter, "", response.result, response.variablesReference) - event.view.add_regions('selected_hover', [word], scope="comment", flags=sublime.DRAW_NO_OUTLINE) - - def on_close() -> None: - event.view.erase_regions('selected_hover') - - variableState = VariableStateful(variable, None) - component = VariableStatefulComponent(variableState) - variableState.on_dirty = component.dirty - variableState.expand() - ui.Popup(component, event.view, word.a, on_close=on_close) - - except Error as e: - pass # errors trying to evaluate a hover expression should be ignored - - def on_text_hovered(self, event: ui.HoverEvent) -> None: - core.run(self.async_on_text_hovered(event)) def on_gutter_hovered(self, event: ui.GutterEvent) -> None: - if self.window.active_view() != event.view: - return - if not self.is_source_file(event.view): + if not self.project.is_source_file(event.view): return + file = event.view.file_name() - if not file: - return + at = event.view.text_point(event.line, 0) line = event.line + 1 breakpoint = self.breakpoints.get_breakpoint(file, line) if breakpoint: breakpoint_menus.edit_breakpoint(self.breakpoints, breakpoint) - def dispose(self) -> None: self.persistance.save_breakpoints(self.breakpoints) self.persistance.save_to_file() - self.dispose_selected_frame() - for d in self.disposeables: d.dispose() @@ -485,129 +392,116 @@ def dispose(self) -> None: self.debugger.dispose() del DebuggerInterface.instances[self.window.id()] - def onSelectedBreakpoint(self, breakpoint: Optional[Breakpoint]) -> None: + def on_selected_breakpoint(self, breakpoint: Optional[Breakpoint]) -> None: if breakpoint: - self.OnExpandBreakpoint(breakpoint) + breakpoint_menus.edit_breakpoint(self.breakpoints, breakpoint) def show_console_panel(self) -> None: self.panels.show(id(self.terminal)) - def show_breakpoints_panel(self) -> None: - pass - def show_call_stack_panel(self) -> None: self.panels.show(id(self.callstack_panel)) + def run_async(self, awaitable: core.awaitable[None]): + def on_error(e: Exception) -> None: + self.terminal.log_error(str(e)) + core.run(awaitable, on_error=on_error) + + def on_navigate_to_source(self, source: dap.Source, line: int): + self.source_provider.navigate(source, line) + + def command(enabled=None, disabled=None): + def wrap(f): + @property + def wrapper(self): + class command: + def __init__(self, debugger, enabled, disabled, f): + self.f = f + self.debugger = debugger + self._enabled = enabled + self._disabled = disabled + + def __call__(self, *args, **kw): + return self.f(self.debugger) + + def enabled(self): + if self._enabled is not None: + if self.debugger.debugger.state != self._enabled: + return False + if self._disabled is not None: + if self.debugger.debugger.state == self._disabled: + return False + + return True + + return command(self, enabled, disabled, f) + return wrapper + return wrap + + @command() def on_play(self) -> None: self.panel.show() - self.run_async(self.LaunchDebugger()) + @core.async + def on_play_async() -> core.awaitable[None]: + self.show_console_panel() + self.terminal.clear() + self.terminal.log_info('Console cleared...') + try: + if not self.configuration: + self.terminal.log_error("Add or select a configuration to begin debugging") + select_configuration.run(self) + return + + configuration = self.configuration + + adapter_configuration = self.adapters.get(configuration.type) + if not adapter_configuration: + raise Exception('Unable to find debug adapter with the type name "{}"'.format(configuration.type)) + + except Exception as e: + core.log_exception() + core.display(e) + return + + variables = extract_variables(self.window) + configuration_expanded = ConfigurationExpanded(configuration, variables) + yield from self.debugger.launch(adapter_configuration, configuration_expanded) + + self.run_async(on_play_async()) + + @command(disabled=DebuggerStateful.stopped) def on_stop(self) -> None: self.run_async(self.debugger.stop()) + @command(enabled=DebuggerStateful.paused) def on_resume(self) -> None: self.run_async(self.debugger.resume()) + @command(enabled=DebuggerStateful.running) def on_pause(self) -> None: self.run_async(self.debugger.pause()) + @command(enabled=DebuggerStateful.paused) def on_step_over(self) -> None: self.run_async(self.debugger.step_over()) + @command(enabled=DebuggerStateful.paused) def on_step_in(self) -> None: self.run_async(self.debugger.step_in()) + @command(enabled=DebuggerStateful.paused) def on_step_out(self) -> None: self.run_async(self.debugger.step_out()) - def run_async(self, awaitable: core.awaitable[None]): - def on_error(e: Exception) -> None: - self.terminal.log_error(str(e)) - core.run(awaitable, on_error=on_error) - + @command(disabled=DebuggerStateful.stopped) def on_run_command(self, command: str) -> None: self.run_async(self.debugger.evaluate(command)) - def on_navigate_to_source(self, source: Source, line: int): - core.run(self.navigate_to_source(source, line, True)) - - @core.async - def navigate_to_source(self, source: Source, line: int, move_cursor: bool = False): - self.navigate_soure = source - self.navigate_line = line - - # sublime lines are 0 based - - selected_frame_generated_view = None #type: Optional[sublime.View] - - if source.sourceReference: - if not self.debugger.adapter: - return - - content = yield from self.debugger.adapter.GetSource(source) - - # throw out the view if it doesn't have a buffer since it was closed - if self.selected_frame_generated_view and not self.selected_frame_generated_view.buffer_id(): - self.selected_frame_generated_view = None - - view = self.selected_frame_generated_view or self.window.new_file() - self.selected_frame_generated_view = None - view.set_name(source.name) - view.set_read_only(False) - view.run_command('debugger_replace_contents', { - 'characters': content - }) - view.set_read_only(True) - view.set_scratch(True) - selected_frame_generated_view = view - elif source.path: - view = yield from core.sublime_open_file_async(self.window, source.path) - else: - return None - - view.run_command("debugger_show_line", { - 'line' : line -1, - 'move_cursor' : move_cursor - }) - - # We seem to have already selected a differn't frame in the time we loaded the view - if source != self.navigate_soure: - # if we generated a view close it - if selected_frame_generated_view: - selected_frame_generated_view.close() - return None - - self.selected_frame_generated_view = selected_frame_generated_view - return view - - - @core.async - def navigate_to_frame(self, thread: Thread, frame: StackFrame) -> core.awaitable[None]: - print("Navigating to frame") - - source = frame.source - - if not source: - self.dispose_selected_frame() - return - - # sublime lines are 0 based - line = frame.line - 1 - - view = yield from self.navigate_to_source(source, frame.line) - self.dispose_selected_frame() - if view: - self.selected_frame_line = SelectedLine(view, line, thread.stopped_text) - - def dispose_selected_frame(self) -> None: - if self.selected_frame_generated_view: - self.selected_frame_generated_view.close() - self.selected_frame_generated_view = None - if self.selected_frame_line: - self.selected_frame_line.dispose() - self.selected_frame_line = None - - def OnExpandBreakpoint(self, breakpoint: Breakpoint) -> None: - breakpoint_menus.edit_breakpoint(self.breakpoints, breakpoint) - + @command('toggle_breakpoint') + def toggle_breakpoint(self): + self.breakpoints_provider.toggle_current_line() + @command(enabled=DebuggerStateful.paused) + def run_to_current_line(self) -> None: + self.breakpoints_provider.run_to_current_line() diff --git a/modules/debugger/debugger_project.py b/modules/debugger/debugger_project.py new file mode 100644 index 00000000..06b9a92e --- /dev/null +++ b/modules/debugger/debugger_project.py @@ -0,0 +1,16 @@ +from ..typecheck import * + +import sublime + +class DebuggerProject: + def __init__(self, window: sublime.Window): + self.window = window + + def is_source_file(self, view: sublime.View) -> bool: + return bool(self.source_file(view)) + + def source_file(self, view: sublime.View) -> Optional[str]: + if view.window() != self.window: + return None + + return view.file_name() \ No newline at end of file diff --git a/modules/debugger/debugger_terminal.py b/modules/debugger/debugger_terminal.py index c7702263..a36c6709 100644 --- a/modules/debugger/debugger_terminal.py +++ b/modules/debugger/debugger_terminal.py @@ -1,8 +1,9 @@ - from ..typecheck import * -from .terminal import TerminalStandard, Line, LineSourceComponent -from .. import dap, ui, core + +from ..import dap, ui, core + from ..components.variable_component import VariableStatefulComponent, VariableStateful +from .terminal import TerminalStandard, Line, LineSourceComponent class VariableLine(Line): def __init__(self, variable: dap.Variable, source: Optional[dap.Source], line: Optional[int], on_clicked_source: Callable[[], None]) -> None: @@ -41,7 +42,7 @@ def writeable_prompt(self) -> str: def write(self, text: str): self.on_run_command(text) - def program_output(self, client: dap.DebugAdapterClient, event: dap.OutputEvent): + def program_output(self, client: dap.Client, event: dap.OutputEvent): variablesReference = event.variablesReference if variablesReference: # this seems to be what vscode does it ignores the actual message here. diff --git a/modules/debugger/thread.py b/modules/debugger/thread.py index ef275a52..6d654431 100644 --- a/modules/debugger/thread.py +++ b/modules/debugger/thread.py @@ -2,13 +2,7 @@ if TYPE_CHECKING: from .debugger import DebuggerStateful -from .. import core - -from ..debugger.debugger import ( - Thread, - StackFrame, - DebugAdapterClient, -) +from .. import core, dap class ThreadStateful: def __init__(self, debugger: 'DebuggerStateful', id: int, name: Optional[str], stopped: bool): @@ -56,7 +50,7 @@ def stopped(self): return self._stopped @property - def frames(self)->List[StackFrame]: + def frames(self)->List[dap.StackFrame]: if self.expanded: return self._frames return [] @@ -66,7 +60,7 @@ def fetch_if_needed(self): return self.fetched = True - def response(frames: List[StackFrame]): + def response(frames: List[dap.StackFrame]): self._frames = frames self.debugger.update_selection_if_needed() self.dirty() diff --git a/modules/debugger/view_hover.py b/modules/debugger/view_hover.py new file mode 100644 index 00000000..3fd691a7 --- /dev/null +++ b/modules/debugger/view_hover.py @@ -0,0 +1,66 @@ + +import sublime + +from ..import core, ui, dap + +from .debugger import DebuggerStateful +from .debugger_project import DebuggerProject + +from ..components.variable_component import VariableStateful, VariableStatefulComponent + + +class ViewHoverProvider(core.Disposables): + def __init__(self, project: DebuggerProject, debugger: DebuggerStateful) -> None: + super().__init__() + self.debugger = debugger + self.project = project + self += ui.view_text_hovered.add(self.on_view_text_hovered) + + def on_view_text_hovered(self, event) -> None: + if not self.project.is_source_file(event.view): + return + + if self.debugger.state == DebuggerStateful.stopped: + return + + core.run(self.on_hover(event)) + + @core.async + def on_hover(self, event): + hover_word_seperators = self.debugger.adapter_configuration.hover_word_seperators + hover_word_regex_match = self.debugger.adapter_configuration.hover_word_regex_match + + if hover_word_seperators: + word = event.view.expand_by_class(event.point, sublime.CLASS_WORD_START | sublime.CLASS_WORD_END, separators=hover_word_seperators) + else: + word = event.view.word(event.point) + + word_string = event.view.substr(word) + if not word_string: + return + + if hover_word_regex_match: + x = re.search(hover_word_regex_match, word_string) + if not x: + core.log_info("hover match discarded because it failed matching the hover pattern, ", word_string) + return + word_string = x.group() + + try: + response = yield from self.debugger.adapter.Evaluate(word_string, self.debugger.selected_frame, 'hover') + yield from core.asyncio.sleep(0.25) + variable = dap.Variable(self.debugger.adapter, "", response.result, response.variablesReference) + event.view.add_regions('selected_hover', [word], scope="comment", flags=sublime.DRAW_NO_OUTLINE) + + def on_close() -> None: + event.view.erase_regions('selected_hover') + + variableState = VariableStateful(variable, None) + component = VariableStatefulComponent(variableState) + variableState.on_dirty = component.dirty + variableState.expand() + ui.Popup(component, event.view, word.a, on_close=on_close) + + # errors trying to evaluate a hover expression should be ignored + except dap.Error as e: + core.log_error("adapter failed hover evaluation", e) diff --git a/modules/debugger/view_selected_source.py b/modules/debugger/view_selected_source.py new file mode 100644 index 00000000..70d2c8cf --- /dev/null +++ b/modules/debugger/view_selected_source.py @@ -0,0 +1,99 @@ +import sublime + +from .. import core, ui, dap +from .. components.selected_line import SelectedLine +from .debugger import DebuggerStateful +from .debugger_project import DebuggerProject + +class ViewSelectedSourceProvider: + def __init__(self, project: DebuggerProject, debugger: DebuggerStateful): + self.debugger = debugger + self.project = project + self.updating = None + self.generated_view = None + self.selected_frame_line = None + + def select(self, source: dap.Source, line: int, stopped_reason: str): + if self.updating: + self.updating.cancel() + def on_error(error): + if error is not core.CancelledError: + core.log_error(error) + + @core.async + def select_async(source: dap.Source, line: int, stopped_reason: str): + view = yield from self.navigate_to_source(source, line) + self.selected_frame_line = SelectedLine(view, line, stopped_reason) + + self.updating = core.run(select_async(source, line, stopped_reason), on_error=on_error) + + def navigate(self, source: dap.Source, line: int): + if self.updating: + self.updating.cancel() + def on_error(error): + if error is not core.CancelledError: + core.log_error(error) + + @core.async + def navigate_async(source: dap.Source, line: int, stopped_reason: str): + self.clear_generated_view() + self.navigate_to_source(source, line, move_cursor=True) + + self.updating = core.run(navigate_async(source, line, stopped_reason), on_error=on_error) + + + def clear(self): + if self.updating: + self.updating.cancel() + + self.clear_selected() + self.clear_generated_view() + + def clear_selected(self): + if self.selected_frame_line: + self.selected_frame_line.dispose() + self.selected_frame_line = None + + def clear_generated_view(self): + if self.generated_view: + self.generated_view.close() + self.generated_view = None + + def dispose(self): + self.clear() + + @core.async + def navigate_to_source(self, source: dap.Source, line: int, move_cursor: bool = False) -> core.awaitable[sublime.View]: + self.clear_selected() + + # if we aren't going to reuse the previous generated view + # or the generated view was closed (no buffer) throw it away + if not source.sourceReference or self.generated_view and not self.generated_view.buffer_id(): + self.clear_generated_view() + + if source.sourceReference: + if not self.debugger.adapter: + raise core.Error('Debugger not running') + + content = yield from self.debugger.adapter.GetSource(source) + + view = self.generated_view or self.project.window.new_file() + self.generated_view = view + view.set_name(source.name) + view.set_read_only(False) + view.run_command('debugger_replace_contents', { + 'characters': content + }) + view.set_read_only(True) + view.set_scratch(True) + elif source.path: + view = yield from core.sublime_open_file_async(self.project.window, source.path) + else: + raise core.Error('source has no reference or path') + + view.run_command("debugger_show_line", { + 'line' : line - 1, + 'move_cursor' : move_cursor + }) + + return view \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index a3543edd..b18bb167 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,9 +3,9 @@ python_version = 3.7 check_untyped_defs = True strict_optional = True -mypy_path = libs/stubs, .. +mypy_path = modules/libs/stubs, .. -[mypy-sublime_debugger.libs.*] +[mypy-modules.libs.*] check_untyped_defs = False ignore_errors = True diff --git a/start.py b/start.py index d6c8c26f..c6c4a2da 100644 --- a/start.py +++ b/start.py @@ -11,6 +11,7 @@ # import all the commands so that sublime sees them from .modules.commands import * +from .modules.debugger.commands import DebuggerCommand from .modules.debugger.output_panel import * from .modules.debugger.debugger_interface import * from .modules.debugger.build.build import DebuggerBuildExecCommand