diff --git a/README.md b/README.md index e7bf65f0..53ed166d 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,9 @@ The actual installation is no different than for any other Vim plugin, you can * install manually: Clone or download a tarball of the plugin and move its content in your `~/.vim/` directory. You should call `:helptags ~/.vim/doc` to generate the necessary help tags afterwards. -* use Pathogen: Clone this repository to your `~/.vim/bundle` directory and - `:call pathogen#helptags()` afterwards. +* use Pathogen: Clone this repository to your `~/.vim/bundle` directory, run + `:execute pathogen#infect()` and `:call pathogen#helptags()` afterwards. + * use your favorite plugin manager: Put the respective instruction in your init file and update your plugins afterwards. For Vundle this would be `Plugin 'vim-vdebug/vdebug'` and `:PluginInstall`. diff --git a/doc/Vdebug.txt b/doc/Vdebug.txt index 54f7a1be..495d8830 100644 --- a/doc/Vdebug.txt +++ b/doc/Vdebug.txt @@ -194,7 +194,7 @@ able to apply it to your circumstance. The most popular DBGP debugger for PHP is Xdebug. I find that the most effective way to install Xdebug is to use PECL, instead -of a OS-level package manager such as aptitude or yum. You can also compile it +of an OS-level package manager such as aptitude or yum. You can also compile it from source if you're feeling hardy. To install it via PECL, run this from a command line (requires root/admin privileges): > @@ -206,7 +206,7 @@ it as a zend extension. This can be done in the PHP INI file, but in Ubuntu I can add a new file to /etc/php5/conf.d/ that contains all the configuration options and gets loaded automatically by PHP. -Add these options to the INI file: > +For Xdebug v2, add these options to the INI file: > zend_extension=/path/to/xdebug.so xdebug.remote_enable=on @@ -214,6 +214,16 @@ Add these options to the INI file: > xdebug.remote_host=localhost xdebug.remote_port=9000 < + +For Xdebug v3, add these options to the INI file: > + + zend_extension=/path/to/xdebug.so + xdebug.mode=debug + xdebug.client_host=localhost + xdebug.client_port=9000 +< +Please refer to https://xdebug.org/docs/upgrade_guide for instructions on how to upgrade from v2 to v3. + If using Apache, restart it to enable the new library. The command line interface should be ready to go - type "php -v" and you should see the line > with Xdebug v2.2.0, Copyright (c) 2002-2012, by Derick Rethans @@ -232,13 +242,13 @@ then use this instead of php when debugging. For instance, instead of "php myscript.php", run "php-xdebug myscript.php" to start the debugger session. You are now officially ready to start debugging with PHP. However, if you've -become unstuck at any point then there are plenty of Google tutorials on -setting up Xdebug, or you can email me and I'll do my best to help. +become stuck at any point then there are plenty of Google tutorials on setting +up Xdebug, or you can email me and I'll do my best to help. ------------------------------------------------------------------------------ 3.2 Python set up *VdebugSetUpPython* -Python has an standalone debugging tool that you can use from the command line, +Python has a standalone debugging tool that you can use from the command line, but to use Vdebug in conjunction with your Python scripts you will have to grab the "pydbgp" tool, created by ActiveState (who make the Komodo Edit/IDE software). @@ -283,7 +293,7 @@ Try installing and using it like this: > ------------------------------------------------------------------------------ 3.3 Ruby set up *VdebugSetUpRuby* -Like Python, Ruby has an standalone debugging tool that you can use from the +Like Python, Ruby has a standalone debugging tool that you can use from the command line, but to use Vdebug in conjunction with your Ruby scripts you will have to get the "rdbgp.rb" script that comes bundled with Komodo Edit/IDE by Activestate. @@ -325,7 +335,7 @@ If you're still having trouble, drop me an email. Perl is one of the trickier languages to set up, unless you have very specific instructions on what to do. Fortunately, that's what I'm going to give you! -Like Python and Ruby, Activestate provide a standalone module that you can use +Like Python and Ruby, Activestate provides a standalone module that you can use to remotely debug Perl applications. What makes this more difficult than Python and Ruby is that the debugging script changes and, as far as I can see, breaks after a particular version. That means you have to get the right version from @@ -383,7 +393,7 @@ is the port. ------------------------------------------------------------------------------ 3.6 TCL/Wish set up *VdebugSetUpTcl* -Like Python and Ruby, Tcl and Wish have an standalone debugging tool that you +Like Python and Ruby, Tcl and Wish have a standalone debugging tool that you can use from the command line, which has again been made available by ActiveState. @@ -538,6 +548,15 @@ To open the window use the command :BreakpointWindow. It's in a table format, so hopefully it won't need any more explanation. To close the window again, just re-type :BreakpointWindow or :q. +In this window while on the line of a breakpoint you can: + - Jump to a breakpoint by pressing + - Toggle a breakpoint by pressing (default) + - Delete a breakpoint by pressing
+ +In this window while visually selecting multiple breakpoints you can: + - Delete multiple breakpoints by pressing + - Toggle multiple breakpoints by pressing (default) + To see more about breakpoints see |VdebugBreakpoints|. ------------------------------------------------------------------------------ @@ -739,6 +758,10 @@ To remove all breakpoints in one go use the command: > :BreakpointRemove * < +In the breakpoint window a breakpoint can be removed by pressing
while on a +line with a breakpoint or by creating a visual selection of the breakpoints and +deleting them at once with . + ------------------------------------------------------------------------------ 4.5 Evaluating code *VdebugEval* @@ -1148,6 +1171,21 @@ direction. It is possible to have multiple file path mappings by adding more items in the "path_maps" dictionary. +Be careful when you use symlinked paths. Resolving the local path may be +needed before adding to the "path_maps" dictionary (see |resolve()| for example). +The remote log of the debugger (or the local, see |VdebugOptions-debug_window_level| +or |VdebugOptions-debug_file_level|) may be helpful to inspect the paths that are present +in requests and responses. + +If you happen to be using the same layout for all your remote servers in +different projects, you may map the same remote directory to the root of your +project. For instance, if the root of your project is always /app, and you +always run vim from the root of your projects, you may want to use the +following configuration: > + + let g:vdebug_options.path_maps = {"/app": getcwd() } +< + ------------------------------------------------------------------------------ 9.2 Connecting the two machines *VdebugRemoteConnection* @@ -1201,7 +1239,7 @@ the question is good I might even add it to this list. the language you're using and that you're doing what's necessary to activate the engine when running the script (e.g. setting environment variables/URL variables). If the problem persists, check that the - debugger engine is connecting the same port and address that Vdebug is + debugger engine is connecting to the same port and address that Vdebug is binding to (the 'port' and 'server' options). The server option is blank by default, which means that it will connect to all available interfaces. Unless you've changed this option, it's unlikely to be the issue. Also @@ -1261,7 +1299,7 @@ the question is good I might even add it to this list. repository and try to implement it yourself! I do accept merges. Q. Why doesn't Ross, the largest friend, simply eat the other ones? - A. Think of the indigestion that would result. + A. Think of the indigestion that would result in. Q. My command line php script seems to stop at the first line, but the source code is not loaded in the debugger window, how can that be? diff --git a/plugin/vdebug.vim b/plugin/vdebug.vim index 8d7962bc..216fe681 100644 --- a/plugin/vdebug.vim +++ b/plugin/vdebug.vim @@ -8,7 +8,7 @@ " Description: Multi-language debugger client for Vim (PHP, Ruby, Python, " Perl, NodeJS) " Maintainer: Jon Cairns -" Version: 1.4.1 +" Version: 2.0.0 " Inspired by the Xdebug plugin, which was originally written by " Seung Woo Shin sayclub.com> and extended by many " others. @@ -34,14 +34,16 @@ if !has('python3') finish endif -" Nice characters get screwed up on windows -if has('win32') || has('win64') - let g:vdebug_force_ascii = 1 -elseif has('multi_byte') == 0 - let g:vdebug_force_ascii = 1 -else - let g:vdebug_force_ascii = 0 -end +if !exists('g:vdebug_force_ascii') + " Nice characters get screwed up on windows + if has('win32') || has('win64') + let g:vdebug_force_ascii = 1 + elseif has('multi_byte') == 0 + let g:vdebug_force_ascii = 1 + else + let g:vdebug_force_ascii = 0 + end +endif if !exists('g:vdebug_options') let g:vdebug_options = {} @@ -77,6 +79,8 @@ let g:vdebug_options_defaults = { \ 'port' : 9000, \ 'timeout' : 20, \ 'server' : '', +\ "proxy_host" : '', +\ "proxy_port" : 9001, \ 'on_close' : 'stop', \ 'break_on_open' : 1, \ 'ide_key' : '', @@ -90,6 +94,7 @@ let g:vdebug_options_defaults = { \ 'marker_open_tree' : '▾', \ 'sign_breakpoint' : '▷', \ 'sign_current' : '▶', +\ 'sign_current_stack_position' : '▶', \ 'sign_disabled': '▌▌', \ 'continuous_mode' : 1, \ 'background_listener' : 1, @@ -105,6 +110,8 @@ if g:vdebug_force_ascii == 1 let g:vdebug_options_defaults['marker_open_tree'] = '-' let g:vdebug_options_defaults['sign_breakpoint'] = 'B>' let g:vdebug_options_defaults['sign_current'] = '->' + let g:vdebug_options_defaults['sign_current_stack_position'] = '->' + let g:vdebug_options_defaults['sign_disabled'] = 'B|' endif " Create the top dog @@ -117,6 +124,7 @@ command! -nargs=? -complete=customlist,s:BreakpointTypes Breakpoint python3 debu command! -nargs=? -complete=customlist,s:BreakpointTypes SetBreakpoint python3 debugger.set_breakpoint() command! VdebugStart python3 debugger.run() command! -nargs=? BreakpointRemove python3 debugger.remove_breakpoint() +command! -nargs=? BreakpointToggle python3 debugger.toggle_breakpoint() command! BreakpointWindow python3 debugger.toggle_breakpoint_window() command! -nargs=? -bang VdebugEval python3 debugger.handle_eval('', ) command! -nargs=+ -complete=customlist,s:OptionNames VdebugOpt :call Vdebug_set_option() @@ -137,12 +145,25 @@ end if hlexists('DbgBreakptSign') == 0 hi default DbgBreakptSign term=reverse ctermfg=White ctermbg=Green guifg=#ffffff guibg=#00ff00 end +if hlexists('DbgDisabledLine') == 0 + highlight DbgDisabledLine term=reverse ctermbg=White ctermfg=Cyan guibg=#b4ee9a guifg=#888888 +endif +if hlexists('DbgDisabledSign') == 0 + highlight DbgDisabledSign term=reverse ctermbg=White ctermfg=Cyan guibg=#b4ee9a guifg=#888888 +endif +if hlexists('DbgCurrentStackPositionSign') == 0 + highlight DbgCurrentStackPositionSign term=reverse ctermbg=White ctermfg=Cyan guibg=#e17e67 guifg=#888888 +endif +if hlexists('DbgCurrentStackPositionLine') == 0 + highlight DbgCurrentStackPositionLine term=reverse ctermbg=White ctermfg=Cyan guibg=#e17e67 guifg=#888888 +endif " Signs and highlighted lines for breakpoints, etc. function! s:DefineSigns() exe 'sign define breakpt text=' . g:vdebug_options['sign_breakpoint'] . ' texthl=DbgBreakptSign linehl=DbgBreakptLine' exe 'sign define current text=' . g:vdebug_options['sign_current'] . ' texthl=DbgCurrentSign linehl=DbgCurrentLine' exe 'sign define breakpt_dis text=' . g:vdebug_options['sign_disabled'] . ' texthl=DbgDisabledSign linehl=DbgDisabledLine' + exe 'sign define current_stack_position text=' . g:vdebug_options['sign_current_stack_position'] . ' texthl=DbgCurrentStackPositionSign linehl=DbgCurrentStackPositionLine' endfunction function! s:BreakpointTypes(A,L,P) @@ -314,7 +335,9 @@ function! Vdebug_statusline() endfunction augroup Vdebug -autocmd VimLeavePre * python3 debugger.close() +augroup END +augroup VdebugOut +autocmd VimLeavePre * python3 debugger.quit() augroup END call Vdebug_load_options(g:vdebug_options) diff --git a/python3/vdebug/connection.py b/python3/vdebug/connection.py index a73aa111..f7494e23 100644 --- a/python3/vdebug/connection.py +++ b/python3/vdebug/connection.py @@ -4,6 +4,8 @@ import sys import threading import time +import asyncio +import xml.etree.ElementTree as ET from . import log @@ -80,7 +82,7 @@ def send_msg(self, cmd): cmd -- command to send """ - #self.sock.send(cmd + '\0') + # self.sock.send(cmd + '\0') MSGLEN = len(cmd) totalsent = 0 while totalsent < MSGLEN: @@ -102,13 +104,17 @@ def __init__(self, input_stream=None): """ self.__sock = None self.input_stream = input_stream + self.proxy_success = False - def start(self, host='', port=9000, timeout=30): + def start(self, host='', proxy_host = '', proxy_port = 9001, idekey = None, port=9000, timeout=30): """Listen for a connection from the debugger. Listening for the actual connection is handled by self.listen() host -- host name where debugger is running (default '') port -- port number which debugger is listening on (default 9000) + proxy_host -- If using a DBGp Proxy, host name where the proxy is running (default None to disable) + proxy_port -- If using a DBGp Proxy, port where the proxy is listening for debugger connections (default 9001) + idekey -- The idekey that our Api() wrapper is expecting. Only required if using a proxy timeout -- time in seconds to wait for a debugger connection before giving up (default 30) """ print('Waiting for a connection (Ctrl-C to cancel, this message will ' @@ -119,13 +125,17 @@ def start(self, host='', port=9000, timeout=30): serv.setblocking(1) serv.bind((host, port)) serv.listen(5) - self.__sock = self.listen(serv, timeout) + if proxy_host and proxy_port: + # Register ourselves with the proxy server + self.proxyinit(proxy_host, proxy_port, port, idekey) + self.__sock = self.accept(serv, timeout) except socket.timeout: raise TimeoutError("Timeout waiting for connection") finally: + self.proxystop(proxy_host, proxy_port, idekey) serv.close() - def listen(self, serv, timeout): + def accept(self, serv, timeout): """Non-blocking listener. Provides support for keyboard interrupts from the user. Although it's non-blocking, the user interface will still block until the timeout is reached. @@ -154,14 +164,52 @@ def socket(self): def has_socket(self): return self.__sock is not None + def proxyinit(self, proxy_host, proxy_port, port, idekey): + """Register ourselves with the proxy.""" + if not proxy_host or not proxy_port: + return + + self.log("Connecting to DBGp proxy [%s:%d]" % (proxy_host, proxy_port)) + proxy_conn = socket.create_connection((proxy_host, proxy_port), 30) + + self.log("Sending proxyinit command") + msg = 'proxyinit -p %d -k %s -m 0' % (port, idekey) + proxy_conn.send(msg.encode()) + proxy_conn.shutdown(socket.SHUT_WR) + + # Parse proxy response + response = proxy_conn.recv(8192) + proxy_conn.close() + response = ET.fromstring(response) + self.proxy_success = bool(response.get("success")) + + def proxystop(self, proxy_host, proxy_port, idekey): + """De-register ourselves from the proxy.""" + if not self.proxy_success: + return + + proxy_conn = socket.create_connection((proxy_host, proxy_port), 30) + + self.log("Sending proxystop command") + msg = 'proxystop -k %s' % str(idekey) + proxy_conn.send(msg.encode()) + proxy_conn.close() + self.proxy_success = False + + class BackgroundSocketCreator(threading.Thread): - def __init__(self, host, port, message_q, output_q): - self.__message_q = message_q + def __init__(self, host, port, proxy_host, proxy_port, idekey, output_q): self.__output_q = output_q self.__host = host self.__port = port + self.__proxy_host = proxy_host + self.__proxy_port = proxy_port + self.__idekey = idekey + self.proxy_success = False + self.__socket_task = None + self.__loop = None threading.Thread.__init__(self) @staticmethod @@ -169,23 +217,36 @@ def log(message): log.Log(message, log.Logger.DEBUG) def run(self): + # needed for python 3.5 + self.__loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.__loop) + self.__loop.run_until_complete(self.run_async()) + + async def run_async(self): self.log("Started") self.log("Listening on port %s" % self.__port) try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.setblocking(1) + s.setblocking(False) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((self.__host, self.__port)) - s.settimeout(5) # timeout after 5 seconds so we can check messages s.listen(5) while 1: try: - self.__peek_for_exit() - client, address = s.accept() + # using ensure_future here since before 3.7, this is not a coroutine, but returns a future + self.__socket_task = asyncio.ensure_future(self.__loop.sock_accept(s)) + if self.__proxy_host and self.__proxy_port: + # Register ourselves with the proxy server + await self.proxyinit() + client, address = await self.__socket_task + # set resulting socket to blocking + client.setblocking(True) + self.log("Found client, %s" % str(address)) self.__output_q.put((client, address)) break except socket.error: + await self.proxystop() # No connection pass except socket.error as socket_error: @@ -195,41 +256,75 @@ def run(self): if socket_error.errno == errno.EADDRINUSE: self.log("Address already in use") print("Socket is already in use") - except Exception: + except asyncio.CancelledError as e: + self.log("Stopping server") + self.__socket_task = None + except Exception as e: print("Exception caught") self.log("Error: %s" % str(sys.exc_info())) self.log("Stopping server") finally: + await self.proxystop() self.log("Finishing socket server") s.close() - def __peek_for_exit(self): - try: - # self.log("Checking for exit") - self.__check_exit(self.__message_q.get_nowait()) - except queue.Empty: - pass + async def proxyinit(self): + """Register ourselves with the proxy.""" + if not self.__proxy_host or not self.__proxy_port: + return - @staticmethod - def __check_exit(message): - if message == "exit": - raise Exception("Exiting") + self.log("Connecting to DBGp proxy [%s:%d]" % (self.__proxy_host, self.__proxy_port)) + proxy_conn = socket.create_connection((self.__proxy_host, self.__proxy_port), 30) + + self.log("Sending proxyinit command") + msg = 'proxyinit -p %d -k %s -m 0' % (self.__port, self.__idekey) + proxy_conn.send(msg.encode()) + proxy_conn.shutdown(socket.SHUT_WR) + + # Parse proxy response + response = proxy_conn.recv(8192) + proxy_conn.close() + response = ET.fromstring(response) + self.proxy_success = bool(response.get("success")) + + async def proxystop(self): + """De-register ourselves from the proxy.""" + if not self.proxy_success: + return + + proxy_conn = socket.create_connection((self.__proxy_host, self.__proxy_port), 30) + + self.log("Sending proxystop command") + msg = 'proxystop -k %s' % str(self.__idekey) + proxy_conn.send(msg.encode()) + proxy_conn.close() + self.proxy_success = False + + + + def _exit(self): + if self.__socket_task: + # this will raise asyncio.CancelledError + self.__socket_task.cancel() + + # called from outside of the thread + def exit(self): + self.__loop.call_soon_threadsafe(self._exit) class SocketServer: def __init__(self): - self.__message_q = queue.Queue(0) self.__socket_q = queue.Queue(1) self.__thread = None def __del__(self): self.stop() - def start(self, host, port): + def start(self, host, port, proxy_host, proxy_port, ide_key): if not self.is_alive(): self.__thread = BackgroundSocketCreator( - host, port, self.__message_q, self.__socket_q) + host, port, proxy_host, proxy_port, ide_key, self.__socket_q) self.__thread.start() def is_alive(self): @@ -243,7 +338,7 @@ def socket(self): def stop(self): if self.is_alive(): - self.__message_q.put_nowait("exit") + self.__thread.exit() self.__thread.join(3000) if self.has_socket(): self.socket()[0].close() diff --git a/python3/vdebug/dbgp.py b/python3/vdebug/dbgp.py index dcf3ca5d..053f0186 100644 --- a/python3/vdebug/dbgp.py +++ b/python3/vdebug/dbgp.py @@ -1,7 +1,4 @@ import base64 -import socket -import sys -import time import xml.etree.ElementTree as ET from . import log @@ -159,7 +156,7 @@ def get_code(self): parts = cmd.split('-- ') missing_padding = len(parts[1]) % 4 if missing_padding != 0: - parts[1] += '='* (4 - missing_padding) + parts[1] += '=' * (4 - missing_padding) return base64.b64decode(parts[1].encode('utf-8')).decode('utf-8') @@ -184,7 +181,7 @@ def is_supported(self): def __str__(self): if self.is_supported(): xml = self.as_xml() - return xml.text + return xml.text if xml.text else "" return "* Feature not supported *" @@ -251,10 +248,10 @@ def send_cmd(self, cmd, args='', res_cls=Response): send += ' -i ' + str(self.transID) if args: send += ' ' + args - log.Log("Command: "+send, log.Logger.DEBUG) + log.Log("Command: " + send, log.Logger.DEBUG) self.conn.send_msg(send) msg = self.conn.recv_msg() - log.Log("Response: "+msg, log.Logger.DEBUG) + log.Log("Response: " + msg, log.Logger.DEBUG) return res_cls(msg, cmd, args, self) def status(self): @@ -273,7 +270,7 @@ def feature_get(self, name): name -- name of the feature, e.g. encoding """ - return self.send_cmd('feature_get', '-n '+str(name), + return self.send_cmd('feature_get', '-n ' + str(name), FeatureGetResponse) def feature_set(self, name, value): @@ -492,7 +489,7 @@ def __init_children(self, node): for c in children: if c.tag == tagname: idx += 1 - p = self._create_child(c, self, self.depth+1) + p = self._create_child(c, self, self.depth + 1) self.children.append(p) if idx == self.num_declared_children: p.mark_as_last_child() @@ -545,7 +542,7 @@ def _determine_displayname(self, node): "['%s']" % node.get('name') else: self.display_name = self.parent.display_name + \ - "->"+node.get('name') + "->" + node.get('name') elif self.language == 'perl': self.display_name = node.get('fullname') else: diff --git a/python3/vdebug/debugger_interface.py b/python3/vdebug/debugger_interface.py index 0a8530d6..9aa3f8ac 100644 --- a/python3/vdebug/debugger_interface.py +++ b/python3/vdebug/debugger_interface.py @@ -74,6 +74,19 @@ def handle_return_keypress(self): """ return self.event_dispatcher.by_position(self.session_handler) + def handle_delete_line_keypress(self): + """React to a
keypress event. + """ + return self.event_dispatcher.delete_line(self.session_handler) + + def handle_delete_visual_keypress(self): + """React to a vunmap keypress event. + """ + return self.event_dispatcher.event_visual( + self.session_handler, + 'delete' + ) + def handle_double_click(self): """React to a mouse double click event. """ @@ -147,6 +160,11 @@ def remove_breakpoint(self, args=None): """ self.session_handler.dispatch_event("remove_breakpoint", args) + def jump_breakpoint(self, args=None): + """Jump to a breakpoint in the source window from the breakpoint window + """ + self.session_handler.dispatch_event("breakpoint_jump", args) + def get_context(self): """Get all the variables in the default context """ @@ -161,3 +179,8 @@ def close(self): """Close the connection, or the UI if already closed. """ self.session_handler.stop() + + def quit(self): + """Close the connection, or the UI if already closed. On Exit + """ + self.session_handler.stop(quiet=True) diff --git a/python3/vdebug/event.py b/python3/vdebug/event.py index 303e1dc9..87842e4c 100644 --- a/python3/vdebug/event.py +++ b/python3/vdebug/event.py @@ -44,16 +44,16 @@ class CursorEvalEvent(Event): """Evaluate the variable currently under the cursor. """ char_regex = { - "default": "a-zA-Z0-9_.\[\]'\"", - "ruby": "$@a-zA-Z0-9_.\[\]'\"", + "default": "a-zA-Z0-9_.\\[\\]'\"", + "ruby": "$@a-zA-Z0-9_.\\[\\]'\"", "perl": "$a-zA-Z0-9_{}'\"", - "php": "$@%a-zA-Z0-9_\[\]'\"\->" + "php": "$@%a-zA-Z0-9_\\[\\]'\">-" } var_regex = { "default": "^[a-zA-Z_]", "ruby": "^[$@a-zA-Z_]", - "php": "^[\$A-Z]", + "php": r"^[\$A-Z]", "perl": "^[$@%]" } @@ -116,6 +116,7 @@ def run(self): line = self.ui.windows.stack().line_at(lineno - 1) if line.find(" @ ") == -1: return False + self.ui.windows.stack().place_pointer(lineno) stack_number_startpos = line.find("[") + 1 stack_number_endpos = line[stack_number_startpos:].rfind("]") + 1 @@ -128,6 +129,7 @@ def run(self): lineno = file_and_line[line_pos+1:] self.ui.sourcewin.set_file(file) self.ui.sourcewin.set_line(lineno) + self.ui.sourcewin.place_pointer(lineno) self.dispatch("change_stack", stack_number) @@ -272,7 +274,10 @@ class RefreshEvent(Event): def run(self, status): - status_str = str(status) + try: + status_str = str(status) + except TypeError: + return if not status_str: return @@ -443,6 +448,32 @@ def run(self, args): self.session_handler.breakpoints().remove_breakpoint_by_id(id) return + # If in the breakpoint window, toggle breakpoint instead of adding/deleting one + window_name = Dispatcher._get_window_name() + if window_name == self.session_handler.ui().windows.breakpoints().name: + buf = vim.current.buffer + try: + (lnum1, col1) = buf.mark('<') + (lnum2, col2) = buf.mark('>') + lines = vim.eval('getline({}, {})'.format(lnum1, lnum2)) + + # The visual marks do not unset automatically + vim.command(':delmarks <>') + except TypeError: + lineno = vim.current.window.cursor[0] + lines = [self.session_handler.ui().windows.breakpoints().line_at(lineno - 1)] + + for line in lines: + id = Dispatcher._get_breakpoint_id_breakpoint_window(line) + if not id: + log.Log("No breakpoint founr at current cursor position", + log.Logger.DEBUG) + continue + + self.session_handler.dispatch_event("toggle_breakpoint", "{} {}".format(id, "toggle")) + + return + bp = breakpoint.Breakpoint.parse(self.ui, args) if bp.type == "line": id = self.session_handler.breakpoints().find_breakpoint( @@ -512,6 +543,34 @@ def run(self, args): if action == "toggle": return self.dispatch("toggle_breakpoint", str(bp.id)) +class BreakpointJumpEvent(BreakpointStatusEvent): + + """Event used to trigger the source window change to the breakpoint + + The id of the breakpoint under the VIM cursor is retrieved, and the source file + and line number are set with the breakpoint data. + """ + + def run(self): + lineno = vim.current.window.cursor[0] + + log.Log("User action in breakpoint window, line %s" % lineno, + log.Logger.DEBUG) + line = self.ui.windows.breakpoints().line_at(lineno - 1) + + # Match on ID + id = re.findall('^[\\s][0-9]*[\\s]', line) + if not id: + return False + + bp = self.get_breakpoint(id[0].strip()) + + if bp is not None and (bp.type == "line" or bp.type == "conditional"): + file = bp.get_file() + lineno = bp.get_line() + self.ui.sourcewin.set_file(file) + self.ui.sourcewin.set_line(lineno) + class CycleBreakpointStatusEvent(BreakpointStatusEvent): @@ -704,6 +763,7 @@ class Dispatcher: "enable_breakpoint": EnableBreakpointEvent, "disable_breakpoint": DisableBreakpointEvent, "breakpoint_status": BreakpointStatusEvent, + "breakpoint_jump": BreakpointJumpEvent, "get_context": GetContextEvent, "reload_keymappings": ReloadKeymappingsEvent, "remove_breakpoint": RemoveBreakpointEvent, @@ -719,6 +779,8 @@ def __init__(self, session_handler): def dispatch_event(self, name, *args): try: + log.Log("Dispatching {} event".format(name), + log.Logger.INFO) Dispatcher.events[name](self.__session_handler).run(*args) except Exception as e: self.__ex_handler.handle(e) @@ -743,15 +805,46 @@ def by_position(self, session): log.Logger.DEBUG) return False + def delete_line(self, session): + window_name = self._get_window_name() + + if window_name == session.ui().windows.breakpoints().name: + lineno = vim.current.window.cursor[0] + if lineno > 3: + line = session.ui().windows.breakpoints().line_at(lineno - 1) + + id = self._get_breakpoint_id_breakpoint_window(line) + if not id: + log.Log("No breakpoint founr at current cursor position", + log.Logger.DEBUG) + return False + + RemoveBreakpointEvent(session).run(id) + + def event_visual(self, session, event): + window_name = self._get_window_name() + + if window_name == session.ui().windows.breakpoints().name: + buf = vim.current.buffer + (lnum1, col1) = buf.mark('<') + (lnum2, col2) = buf.mark('>') + + lines = vim.eval('getline({}, {})'.format(lnum1, lnum2)) + + for line in lines: + id = self._get_breakpoint_id_breakpoint_window(line) + if not id: + log.Log("No breakpoint found at current cursor position", + log.Logger.DEBUG) + return False + + if event == 'delete': + RemoveBreakpointEvent(session).run(id) + @staticmethod def _get_event_by_position(session): - buf_name = vim.current.buffer.name - p = re.compile(r'.*[\\/]([^\\/]+)') - m = p.match(buf_name) - if m is None: - return None + window_name = Dispatcher._get_window_name() - window_name = m.group(1) if window_name == session.ui().windows.watch().name: lineno = vim.current.window.cursor[0] log.Log("User action in watch window, line %s" % lineno, @@ -765,3 +858,29 @@ def _get_event_by_position(session): return WatchWindowHideEvent(session) elif window_name == session.ui().windows.stack().name: return StackWindowLineSelectEvent(session) + elif window_name == session.ui().windows.breakpoints().name: + lineno = vim.current.window.cursor[0] + if lineno > 3: + return BreakpointJumpEvent(session) + + @staticmethod + def _get_window_name(): + buf_name = vim.current.buffer.name + p = re.compile(r'.*[\\/]([^\\/]+)') + m = p.match(buf_name) + if m is None: + return False + + window_name = m.group(1) + return window_name + + @staticmethod + def _get_breakpoint_id_breakpoint_window(line): + # Match on ID + id = re.findall(r'^[\s][0-9]*[\s]', line) + if not id: + log.Log("No breakpoint found at current cursor position", + log.Logger.DEBUG) + return False + + return id[0] diff --git a/python3/vdebug/listener.py b/python3/vdebug/listener.py index 15775904..0c39ad2d 100644 --- a/python3/vdebug/listener.py +++ b/python3/vdebug/listener.py @@ -22,6 +22,9 @@ def __init__(self): def start(self): self.__server.start(opts.Options.get('server'), opts.Options.get('port', int), + opts.Options.get('proxy_host'), + opts.Options.get('proxy_port', int), + opts.Options.get('ide_key'), opts.Options.get('timeout', int)) def stop(self): @@ -51,7 +54,10 @@ def start(self): if opts.Options.get("auto_start", int): vim.command('autocmd Vdebug CursorHold,CursorHoldI,CursorMoved,CursorMovedI,FocusGained,FocusLost * python3 debugger.start_if_ready()') self.__server.start(opts.Options.get('server'), - opts.Options.get('port', int)) + opts.Options.get('port', int), + opts.Options.get('proxy_host'), + opts.Options.get('proxy_port', int), + opts.Options.get('ide_key')) def stop(self): if opts.Options.get("auto_start", bool): diff --git a/python3/vdebug/log.py b/python3/vdebug/log.py index 5dadea35..7db9a5ff 100644 --- a/python3/vdebug/log.py +++ b/python3/vdebug/log.py @@ -81,7 +81,7 @@ def __init__(self, debug_level, filename): def __open(self): try: - self.f = open(self.filename, 'w') + self.f = open(self.filename, 'w', encoding='utf-8') except IOError as e: raise error.LogError("Invalid file name '%s' for log file: %s" % (self.filename, e)) @@ -104,6 +104,8 @@ class Log: loggers = {} + + def __init__(self, string, level=Logger.INFO): Log.log(string, level) diff --git a/python3/vdebug/session.py b/python3/vdebug/session.py index 893b8d69..a6792bbc 100644 --- a/python3/vdebug/session.py +++ b/python3/vdebug/session.py @@ -37,13 +37,14 @@ def listen(self): print("Waiting for a connection: none found so far") elif self.listener and self.listener.is_ready(): print("Found connection, starting debugger") + log.Log("Got connection, starting", log.Logger.DEBUG) self.__new_session() else: self.start_listener() def start_listener(self): self.listener = listener.Listener.create() - print("Vdebug will wait for a connection in the background") + print("Vdebug will wait for a connection") util.Environment.reload() if self.is_open(): self.ui().set_status("listening") @@ -64,7 +65,7 @@ def run(self): else: self.listen() - def stop(self): + def stop(self, quiet=False): if self.is_connected(): self.__session.close_connection() elif self.is_listening(): @@ -72,7 +73,8 @@ def stop(self): elif self.is_open(): self.__ui.close() else: - self.__ui.say("Vdebug is not running") + if False is quiet: + self.__ui.say("Vdebug is not running") def close(self): self.stop_listening() @@ -102,6 +104,7 @@ def start_if_ready(self): try: if self.listener.is_ready(): print("Found connection, starting debugger") + log.Log("Got connection, starting", log.Logger.DEBUG) self.__new_session() return True return False @@ -205,7 +208,9 @@ def start(self, connection): self.__ui.set_conn_details(addr[0], addr[1]) self.__collect_context_names() - self.__set_features() + self.__check_features() # only for debugging at the moment + self.__set_default_features() # features we try by default + self.__set_features() # user defined features self.__initialize_breakpoints() if opts.Options.get('break_on_open', int) == 1: @@ -228,6 +233,64 @@ def detach(self): self.close_connection(False) + def __check_features(self): + must_features = [ + 'language_supports_threads', + 'language_name', + 'language_version', + 'encoding', # has set + 'protocol_version', + 'supports_async', + 'data_encoding', + 'breakpoint_languages', + 'breakpoint_types', + 'resolved_breakpoints', + 'multiple_sessions', # has set + 'max_children', # has set + 'max_data', # has set + 'max_depth', # has set + 'extended_properties', # has set + ] + maybe_features = [ + 'supported_encodings', + 'supports_postmortem', + 'show_hidden', # has set + 'notify_ok', # has set + ] + for feature in must_features: + try: + feature_value = self.__api.feature_get(feature) + log.Log( + "Must Feature: %s = %s" % (feature, str(feature_value)), + log.Logger.DEBUG + ) + except dbgp.DBGPError: + error_str = "Failed to get feature %s" % feature + log.Log(error_str, log.Logger.DEBUG) + + for feature in maybe_features: + try: + feature_value = self.__api.feature_get(feature) + log.Log( + "Maybe Feature: %s = %s" % (feature, str(feature_value)), + log.Logger.DEBUG + ) + except dbgp.DBGPError: + error_str = "Failed to get feature %s" % feature + log.Log(error_str, log.Logger.DEBUG) + + def __set_default_features(self): + features = { + 'multiple_sessions': 0, # explicitly disable multiple sessions atm + 'extended_properties': 1, + } + for name, value in features.items(): + try: + self.__api.feature_set(name, value) + except dbgp.DBGPError as e: + error_str = "Failed to set feature %s: %s" % (name, e.args[0]) + log.Log(error_str, log.Logger.DEBUG) + def __set_features(self): """Evaluate vim dictionary of features and pass to debugger. diff --git a/python3/vdebug/ui/vimui.py b/python3/vdebug/ui/vimui.py index 415e4c94..5f46ba66 100644 --- a/python3/vdebug/ui/vimui.py +++ b/python3/vdebug/ui/vimui.py @@ -158,8 +158,8 @@ def __init__(self): 'DebuggerStatus': 'vertical leftabove new' }, 'window_size': { - 'DebuggerWatch': { 'height' : 15 }, - 'DebuggerStatus': { 'height' : 1 } + 'DebuggerWatch': {'height': 15}, + 'DebuggerStatus': {'height': 1} }, 'window_arrangement': [ 'DebuggerWatch', @@ -278,9 +278,11 @@ def place_breakpoint(sign_id, file, line): def enable_breakpoint(self, breakpoint): self.place_breakpoint(breakpoint.id, breakpoint.file, breakpoint.line) + self.windows.breakpoints().update_breakpoint(breakpoint) def disable_breakpoint(self, breakpoint): self.place_disabled_breakpoint(breakpoint.id, breakpoint.file, breakpoint.line) + self.windows.breakpoints().update_breakpoint(breakpoint) @staticmethod def place_disabled_breakpoint(sign_id, file, line): @@ -349,6 +351,8 @@ class SourceWindow(interface.Window): file = None pointer_sign_id = '6145' breakpoint_sign_id = '6146' + has_sign_priority = vim.vvars['version'] > 801 \ + or (hasattr(vim, 'funcs') and vim.funcs.has('nvim-0-4-0')) def focus(self): vim.command("1wincmd w") @@ -364,11 +368,13 @@ def command(self, cmd, silent=True): vim.command(command_str) def set_file(self, file): - if file == self.file: + self.focus() + + if file == self.file and self.file == vim.current.buffer.name: return + self.file = file log.Log("Setting source file: %s" % file, log.Logger.INFO) - self.focus() vim.command('call Vdebug_edit("%s")' % str(file).replace("\\", "\\\\")) def set_line(self, lineno): @@ -384,8 +390,10 @@ def get_file(self): def place_pointer(self, line): log.Log("Placing pointer sign on line "+str(line), log.Logger.INFO) self.remove_pointer() - vim.command('sign place %s name=current line=%s file=%s' - % (self.pointer_sign_id, line, self.file)) + signstr = 'sign place %s name=current line=%s file=%s' + if self.has_sign_priority: + signstr = 'sign place %s name=current priority=99 line=%s file=%s' + vim.command(signstr % (self.pointer_sign_id, line, self.file)) def remove_pointer(self): vim.command('sign unplace %s' % self.pointer_sign_id) @@ -578,7 +586,8 @@ def create(self, open_cmd): self.creation_count += 1 if self.creation_count == 1: - cmd = 'autocmd Vdebug BufWinLeave %s' % self.name + cmd = 'autocmd Vdebug BufWinLeave %s silent! bdelete %s' \ + % (self.name, self.name) cmd += ' python3 debugger.mark_window_as_closed("%s")' % self.name vim.command(cmd) @@ -614,26 +623,28 @@ class BreakpointWindow(Window): name = "DebuggerBreakpoints" header = """=========================================================== - ID | TYPE | DATA + ID | ACTIVE | TYPE | DATA ===========================================================""" def on_create(self): if self.creation_count == 1: self.insert(self.header, 0) + self.command('inoremap dd ' + ':python3 debugger.handle_delete_line_keypress()') + self.command('nnoremap dd ' + ':python3 debugger.handle_delete_line_keypress()') + self.command('vnoremap d ' + ':python3 debugger.handle_delete_visual_keypress()') + self.command('xnoremap d ' + ':python3 debugger.handle_delete_visual_keypress()') + self.command('inoremap ' + ':python3 debugger.handle_return_keypress()') + self.command('nnoremap ' + ':python3 debugger.handle_return_keypress()') self.command('setlocal syntax=debugger_breakpoint') def add_breakpoint(self, breakpoint): - bp_str = " %-7i | %-11s | " % (breakpoint.id, breakpoint.type) - if breakpoint.type == 'line': - bp_str += "%s:%s" % (breakpoint.file, str(breakpoint.line)) - elif breakpoint.type == 'conditional': - bp_str += "%s:%s when (%s)" % ( - breakpoint.file, str(breakpoint.line), breakpoint.condition) - elif breakpoint.type == 'exception': - bp_str += "Exception: %s" % breakpoint.exception - elif breakpoint.type == 'call' or breakpoint.type == 'return': - bp_str += "Function: %s" % breakpoint.function - + bp_str = self.breakpoint_str(breakpoint) self.write(bp_str) def remove_breakpoint(self, breakpoint_id): @@ -645,6 +656,31 @@ def remove_breakpoint(self, breakpoint_id): self._buffer.delete(i) i += 1 + def update_breakpoint(self, breakpoint): + i = 0 + for l in self._buffer.contents(): + bp_str = " %i " % breakpoint.id + bp_id_len = len(bp_str) + if l[:bp_id_len] == bp_str: + + bp_str = self.breakpoint_str(breakpoint) + self.insert(bp_str, i, True, True) + i += 1 + + def breakpoint_str(self, breakpoint): + bp_str = " %-7i | %-6s | %-11s | " % (breakpoint.id, breakpoint.enabled, breakpoint.type) + if breakpoint.type == 'line': + bp_str += "%s:%s" % (breakpoint.file, str(breakpoint.line)) + elif breakpoint.type == 'conditional': + bp_str += "%s:%s when (%s)" % ( + breakpoint.file, str(breakpoint.line), breakpoint.condition) + elif breakpoint.type == 'exception': + bp_str += "Exception: %s" % breakpoint.exception + elif breakpoint.type == 'call' or breakpoint.type == 'return': + bp_str += "Function: %s" % breakpoint.function + + return bp_str + class LogWindow(Window): @@ -667,6 +703,10 @@ class StackWindow(Window): name = "DebuggerStack" + pointer_sign_id = '6147' + has_sign_priority = vim.vvars['version'] > 801 \ + or (hasattr(vim, 'funcs') and vim.funcs.has('nvim-0-4-0')) + def on_create(self): self.command('inoremap ' ':python3 debugger.handle_return_keypress()') @@ -679,6 +719,18 @@ def on_create(self): def write(self, msg, return_focus=True): Window.write(self, msg, after="normal gg") + def place_pointer(self, line): + log.Log("Stack window: placing pointer sign on line "+str(line), log.Logger.INFO) + self.remove_pointer() + signstr = 'sign place %s name=current_stack_position line=%s' + if self.has_sign_priority: + signstr = \ + 'sign place %s name=current_stack_position priority=99 line=%s' + vim.command(signstr % (self.pointer_sign_id, line)) + + def remove_pointer(self): + vim.command('sign unplace %s' % self.pointer_sign_id) + class WatchWindow(Window): diff --git a/python3/vdebug/util.py b/python3/vdebug/util.py index 653705e6..09bf48fb 100644 --- a/python3/vdebug/util.py +++ b/python3/vdebug/util.py @@ -105,7 +105,7 @@ class Keymapper: """ exclude = ["run", "close", "set_breakpoint", "enable_breakpoint", "disable_breakpoint", - "toggle_breakpoint" "eval_visual"] + "toggle_breakpoint", "eval_visual"] def __init__(self): self.is_mapped = False @@ -278,7 +278,8 @@ def as_local(self, quote=False): return self.local def as_remote(self): - return self.remote + # on win os backslash will cost breakpoint miss hit + return self.remote.replace('\\', '/') if self.is_win else self.remote @staticmethod def _findSeparator(path): diff --git a/tests/test_dbgp_api.py b/tests/test_dbgp_api.py index 26382d57..4e998059 100644 --- a/tests/test_dbgp_api.py +++ b/tests/test_dbgp_api.py @@ -127,10 +127,10 @@ def test_stop_retval(self): assert str(status_res) == "stopping" def test_detatch_retval(self): - """Test that the detatch command receives a message from the api.""" + """Test that the detach command receives a message from the api.""" self.p.conn.recv_msg.return_value = """\n - = 3 and sys.version_info[1] >= 6: handle.write.assert_called_once() @@ -115,6 +115,6 @@ def test_shutdown_with_file(self): with mock.patch(self.open_name, mock.mock_open()) as mocked_open: self.logger.log('text', self.level) self.logger.shutdown() - mocked_open.assert_called_once_with(self.filename, 'w') + mocked_open.assert_called_once_with(self.filename, 'w', encoding='utf-8') handle = mocked_open() handle.close.assert_called_once_with()