1980 lines
55 KiB
VimL
1980 lines
55 KiB
VimL
" Init section - set default values, highlight colors
|
|
|
|
let s:rdebug_port = 39767
|
|
let s:debugger_port = 39768
|
|
" hostname() returns something strange in Windows (E98BD9A419BB41D), so set hostname explicitly
|
|
let s:hostname = 'localhost' "hostname()
|
|
" ~/.vim for Linux, vimfiles for Windows
|
|
let s:runtime_dir = expand('<sfile>:h:h')
|
|
" File for communicating between intermediate Ruby script ruby_debugger.rb and
|
|
" this plugin
|
|
let s:tmp_file = s:runtime_dir . '/tmp/ruby_debugger'
|
|
let s:server_output_file = s:runtime_dir . '/tmp/ruby_debugger_output'
|
|
" Default id for sign of current line
|
|
let s:current_line_sign_id = 120
|
|
let s:separator = "++vim-ruby-debugger separator++"
|
|
let s:sign_id = 0
|
|
|
|
" Create tmp directory if it doesn't exist
|
|
if !isdirectory(s:runtime_dir . '/tmp')
|
|
call mkdir(s:runtime_dir . '/tmp')
|
|
endif
|
|
|
|
" Init breakpoint signs
|
|
hi def link Breakpoint Error
|
|
sign define breakpoint linehl=Breakpoint text=xx
|
|
|
|
" Init current line signs
|
|
hi def link CurrentLine DiffAdd
|
|
sign define current_line linehl=CurrentLine text=>>
|
|
|
|
" Loads this file. Required for autoloading the code for this plugin
|
|
fun! ruby_debugger#load_debugger()
|
|
if !s:check_prerequisites()
|
|
finish
|
|
endif
|
|
endf
|
|
|
|
|
|
" Check all requirements for the current plugin
|
|
fun! s:check_prerequisites()
|
|
let problems = []
|
|
if v:version < 700
|
|
call add(problems, "RubyDebugger: This plugin requires Vim >= 7.")
|
|
endif
|
|
if !has("clientserver")
|
|
call add(problems, "RubyDebugger: This plugin requires +clientserver option")
|
|
endif
|
|
if !executable("rdebug-ide")
|
|
call add(problems, "RubyDebugger: You don't have installed 'ruby-debug-ide' gem or executable 'rdebug-ide' can't be found in your PATH")
|
|
endif
|
|
if !(has("win32") || has("win64")) && !executable("lsof")
|
|
call add(problems, "RubyDebugger: You don't have 'lsof' installed or executable 'lsof' can't be found in your PATH")
|
|
endif
|
|
if g:ruby_debugger_builtin_sender && !has("ruby")
|
|
call add(problems, "RubyDebugger: You are trying to use built-in Ruby in Vim, but your Vim doesn't compiled with +ruby. Set g:ruby_debugger_builtin_sender = 0 in your .vimrc to resolve that issue.")
|
|
end
|
|
if empty(problems)
|
|
return 1
|
|
else
|
|
for p in problems
|
|
echoerr p
|
|
endfor
|
|
return 0
|
|
endif
|
|
endf
|
|
|
|
|
|
" End of init section
|
|
|
|
|
|
" *** Common (global) functions
|
|
|
|
" Split string of tags to List. E.g.,
|
|
" <variables><variable name="a" value="b" /><variable name="c" value="d" /></variables>
|
|
" will be splitted to
|
|
" [ '<variable name="a" value="b" />', '<variable name="c" value="d" />' ]
|
|
function! s:get_tags(cmd)
|
|
let tags = []
|
|
let cmd = a:cmd
|
|
" Remove wrap tags
|
|
let inner_tags_match = s:get_inner_tags(cmd)
|
|
if !empty(inner_tags_match)
|
|
" Then find every tag and remove it from source string
|
|
let pattern = '<.\{-}\/>'
|
|
let inner_tags = inner_tags_match[1]
|
|
let tagmatch = matchlist(inner_tags, pattern)
|
|
while empty(tagmatch) == 0
|
|
call add(tags, tagmatch[0])
|
|
" These symbols are interpretated as special, we need to escape them
|
|
let tagmatch[0] = escape(tagmatch[0], '[]~*\')
|
|
" Remove it from source string
|
|
let inner_tags = substitute(inner_tags, tagmatch[0], '', '')
|
|
" Find next tag
|
|
let tagmatch = matchlist(inner_tags, pattern)
|
|
endwhile
|
|
endif
|
|
return tags
|
|
endfunction
|
|
|
|
|
|
" Return match of inner tags without wrap tags. E.g.:
|
|
" <variables><variable name="a" value="b" /></variables> mathes only <variable />
|
|
function! s:get_inner_tags(cmd)
|
|
return matchlist(a:cmd, '^<.\{-}>\(.\{-}\)<\/.\{-}>$')
|
|
endfunction
|
|
|
|
|
|
" Return Dict of attributes.
|
|
" E.g., from <variable name="a" value="b" /> it returns
|
|
" {'name' : 'a', 'value' : 'b'}
|
|
function! s:get_tag_attributes(cmd)
|
|
let attributes = {}
|
|
let cmd = a:cmd
|
|
" Find type of used quotes (" or ')
|
|
let quote_match = matchlist(cmd, "\\w\\+=\\(.\\)")
|
|
let quote = empty(quote_match) ? "\"" : escape(quote_match[1], "'\"")
|
|
let pattern = "\\(\\w\\+\\)=" . quote . "\\(.\\{-}\\)" . quote
|
|
" Find every attribute and remove it from source string
|
|
let attrmatch = matchlist(cmd, pattern)
|
|
while !empty(attrmatch)
|
|
" Values of attributes can be escaped by HTML entities, unescape them
|
|
let attributes[attrmatch[1]] = s:unescape_html(attrmatch[2])
|
|
" These symbols are interpretated as special, we need to escape them
|
|
let attrmatch[0] = escape(attrmatch[0], '[]~*\')
|
|
" Remove it from source string
|
|
let cmd = substitute(cmd, attrmatch[0], '', '')
|
|
" Find next attribute
|
|
let attrmatch = matchlist(cmd, pattern)
|
|
endwhile
|
|
return attributes
|
|
endfunction
|
|
|
|
|
|
" Unescape HTML entities
|
|
function! s:unescape_html(html)
|
|
let result = substitute(a:html, "&", "\\&", "g")
|
|
let result = substitute(result, """, "\"", "g")
|
|
let result = substitute(result, "<", "<", "g")
|
|
let result = substitute(result, ">", ">", "g")
|
|
return result
|
|
endfunction
|
|
|
|
|
|
function! s:quotify(exp)
|
|
let quoted = a:exp
|
|
let quoted = substitute(quoted, "\"", "\\\\\"", 'g')
|
|
return quoted
|
|
endfunction
|
|
|
|
|
|
" Get filename of current buffer
|
|
function! s:get_filename()
|
|
return expand("%:p")
|
|
endfunction
|
|
|
|
|
|
" Send message to debugger. This function should never be used explicitly,
|
|
" only through g:RubyDebugger.send_command function
|
|
function! s:send_message_to_debugger(message)
|
|
if g:ruby_debugger_fast_sender
|
|
call system(s:runtime_dir . "/bin/socket " . s:hostname . " " . s:debugger_port . " \"" . a:message . "\"")
|
|
else
|
|
if g:ruby_debugger_builtin_sender
|
|
ruby << RUBY
|
|
require 'socket'
|
|
attempts = 0
|
|
a = nil
|
|
host = VIM::evaluate("s:hostname")
|
|
port = VIM::evaluate("s:debugger_port")
|
|
message = VIM::evaluate("a:message").gsub("\\\"", '"')
|
|
begin
|
|
a = TCPSocket.open(host, port)
|
|
a.puts(message)
|
|
a.close
|
|
rescue Errno::ECONNREFUSED
|
|
attempts += 1
|
|
if attempts < 400
|
|
sleep 0.05
|
|
retry
|
|
else
|
|
puts("#{host}:#{port} can not be opened")
|
|
exit
|
|
end
|
|
ensure
|
|
a.close if a && !a.closed?
|
|
end
|
|
RUBY
|
|
else
|
|
let script = "ruby -e \"require 'socket'; "
|
|
let script .= "attempts = 0; "
|
|
let script .= "a = nil; "
|
|
let script .= "begin; "
|
|
let script .= "a = TCPSocket.open('" . s:hostname . "', " . s:debugger_port . "); "
|
|
let script .= "a.puts(%q[" . substitute(substitute(a:message, '[', '\[', 'g'), ']', '\]', 'g') . "]);"
|
|
let script .= "a.close; "
|
|
let script .= "rescue Errno::ECONNREFUSED; "
|
|
let script .= "attempts += 1; "
|
|
let script .= "if attempts < 400; "
|
|
let script .= "sleep 0.05; "
|
|
let script .= "retry; "
|
|
let script .= "else; "
|
|
let script .= "puts('" . s:hostname . ":" . s:debugger_port . " can not be opened'); "
|
|
let script .= "exit; "
|
|
let script .= "end; "
|
|
let script .= "ensure; "
|
|
let script .= "a.close if a && !a.closed?; "
|
|
let script .= "end; \""
|
|
let output = system(script)
|
|
if output =~ 'can not be opened'
|
|
call g:RubyDebugger.logger.put("Can't send a message to rdebug - port is not opened")
|
|
endif
|
|
endif
|
|
endif
|
|
endfunction
|
|
|
|
|
|
function! s:unplace_sign_of_current_line()
|
|
if has("signs")
|
|
exe ":sign unplace " . s:current_line_sign_id
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Remove all variables of current line, remove current line sign. Usually it
|
|
" is needed before next/step/cont commands
|
|
function! s:clear_current_state()
|
|
call s:unplace_sign_of_current_line()
|
|
let g:RubyDebugger.variables = {}
|
|
let g:RubyDebugger.frames = []
|
|
" Clear variables and frames window (just show our empty variables Dict)
|
|
if s:variables_window.is_open()
|
|
call s:variables_window.open()
|
|
endif
|
|
if s:frames_window.is_open()
|
|
call s:frames_window.open()
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Open given file and jump to given line
|
|
" (stolen from NERDTree)
|
|
function! s:jump_to_file(file, line)
|
|
"if the file is already open in this tab then just stick the cursor in it
|
|
let window_number = bufwinnr('^' . a:file . '$')
|
|
if window_number != -1
|
|
exe window_number . "wincmd w"
|
|
else
|
|
" Check if last accessed window is usable to use it
|
|
" Usable window - not quickfix, explorer, modified, etc
|
|
if !s:is_window_usable(winnr("#"))
|
|
exe s:first_normal_window() . "wincmd w"
|
|
else
|
|
" If it is usable, jump to it
|
|
exe 'wincmd p'
|
|
endif
|
|
exe "edit " . a:file
|
|
endif
|
|
exe "normal " . a:line . "G"
|
|
endfunction
|
|
|
|
|
|
" Return 1 if window is usable (not quickfix, explorer, modified, only one
|
|
" window, ...)
|
|
function! s:is_window_usable(winnumber)
|
|
"If there is only one window (winnr("$") - windows count)
|
|
if winnr("$") ==# 1
|
|
return 0
|
|
endif
|
|
|
|
" Current window number
|
|
let oldwinnr = winnr()
|
|
|
|
" Switch to given window and check it
|
|
exe a:winnumber . "wincmd p"
|
|
let specialWindow = getbufvar("%", '&buftype') != '' || getwinvar('%', '&previewwindow')
|
|
let modified = &modified
|
|
|
|
exe oldwinnr . "wincmd p"
|
|
|
|
"if it is a special window, e.g. quickfix or another explorer plugin
|
|
if specialWindow
|
|
return 0
|
|
endif
|
|
|
|
if &hidden
|
|
return 1
|
|
endif
|
|
|
|
" If this window is modified, but there is another opened window with
|
|
" current file, return 1. Otherwise - 0
|
|
return !modified || s:buf_in_windows(winbufnr(a:winnumber)) >= 2
|
|
endfunction
|
|
|
|
|
|
" Determine the number of windows open to this buffer number.
|
|
function! s:buf_in_windows(buffer_number)
|
|
let count = 0
|
|
let window_number = 1
|
|
while 1
|
|
let buffer_number = winbufnr(window_number)
|
|
if buffer_number < 0
|
|
break
|
|
endif
|
|
if buffer_number ==# a:buffer_number
|
|
let count = count + 1
|
|
endif
|
|
let window_number = window_number + 1
|
|
endwhile
|
|
|
|
return count
|
|
endfunction
|
|
|
|
|
|
" Find first 'normal' window (not quickfix, explorer, etc)
|
|
function! s:first_normal_window()
|
|
let i = 1
|
|
while i <= winnr("$")
|
|
let bnum = winbufnr(i)
|
|
if bnum != -1 && getbufvar(bnum, '&buftype') ==# '' && !getwinvar(i, '&previewwindow')
|
|
return i
|
|
endif
|
|
let i += 1
|
|
endwhile
|
|
return -1
|
|
endfunction
|
|
|
|
" *** Queue class (start)
|
|
|
|
let s:Queue = {}
|
|
|
|
" ** Public methods
|
|
|
|
" Constructor of new queue.
|
|
function! s:Queue.new() dict
|
|
let var = copy(self)
|
|
let var.queue = []
|
|
let var.after = ""
|
|
return var
|
|
endfunction
|
|
|
|
|
|
" Execute next command in the queue and remove it from queue
|
|
function! s:Queue.execute() dict
|
|
if !empty(self.queue)
|
|
let message = join(self.queue, s:separator)
|
|
call self.empty()
|
|
call g:RubyDebugger.send_command(message)
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Execute 'after' hook only if queue is empty
|
|
function! s:Queue.after_hook() dict
|
|
if self.after != "" && empty(self.queue)
|
|
call self.after()
|
|
endif
|
|
endfunction
|
|
|
|
|
|
function! s:Queue.add(element) dict
|
|
call add(self.queue, a:element)
|
|
endfunction
|
|
|
|
|
|
function! s:Queue.empty() dict
|
|
let self.queue = []
|
|
endfunction
|
|
|
|
|
|
" *** Queue class (end)
|
|
|
|
|
|
|
|
|
|
" *** Public interface (start)
|
|
|
|
let RubyDebugger = { 'commands': {}, 'variables': {}, 'settings': {}, 'breakpoints': [], 'frames': [], 'exceptions': [] }
|
|
let g:RubyDebugger.queue = s:Queue.new()
|
|
|
|
|
|
" Run debugger server. It takes one optional argument with path to debugged
|
|
" ruby script ('script/server webrick' by default)
|
|
function! RubyDebugger.start(...) dict
|
|
let g:RubyDebugger.server = s:Server.new(s:hostname, s:rdebug_port, s:debugger_port, s:runtime_dir, s:tmp_file, s:server_output_file)
|
|
let script_string = a:0 && !empty(a:1) ? a:1 : 'script/server webrick'
|
|
if script_string[0] != '/'
|
|
let script_string = "'" . getcwd() . '/' . substitute(script_string, "'", "", "g") . "'"
|
|
endif
|
|
|
|
echo "Loading debugger..."
|
|
call g:RubyDebugger.server.start(script_string)
|
|
|
|
let g:RubyDebugger.exceptions = []
|
|
for breakpoint in g:RubyDebugger.breakpoints
|
|
call g:RubyDebugger.queue.add(breakpoint.command())
|
|
endfor
|
|
call g:RubyDebugger.queue.add('start')
|
|
echo "Debugger started"
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Stop running server.
|
|
function! RubyDebugger.stop() dict
|
|
if has_key(g:RubyDebugger, 'server')
|
|
call g:RubyDebugger.server.stop()
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" This function receives commands from the debugger. When ruby_debugger.rb
|
|
" gets output from rdebug-ide, it writes it to the special file and 'kick'
|
|
" the plugin by remotely calling RubyDebugger.receive_command(), e.g.:
|
|
" vim --servername VIM --remote-send 'call RubyDebugger.receive_command()'
|
|
" That's why +clientserver is required
|
|
" This function analyzes the special file and gives handling to right command
|
|
function! RubyDebugger.receive_command() dict
|
|
let file_contents = join(readfile(s:tmp_file), "")
|
|
call g:RubyDebugger.logger.put("Received command: " . file_contents)
|
|
let commands = split(file_contents, s:separator)
|
|
for cmd in commands
|
|
if !empty(cmd)
|
|
if match(cmd, '<breakpoint ') != -1
|
|
call g:RubyDebugger.commands.jump_to_breakpoint(cmd)
|
|
elseif match(cmd, '<suspended ') != -1
|
|
call g:RubyDebugger.commands.jump_to_breakpoint(cmd)
|
|
elseif match(cmd, '<exception ') != -1
|
|
call g:RubyDebugger.commands.handle_exception(cmd)
|
|
elseif match(cmd, '<breakpointAdded ') != -1
|
|
call g:RubyDebugger.commands.set_breakpoint(cmd)
|
|
elseif match(cmd, '<catchpointSet ') != -1
|
|
call g:RubyDebugger.commands.set_exception(cmd)
|
|
elseif match(cmd, '<variables>') != -1
|
|
call g:RubyDebugger.commands.set_variables(cmd)
|
|
elseif match(cmd, '<error>') != -1
|
|
call g:RubyDebugger.commands.error(cmd)
|
|
elseif match(cmd, '<message>') != -1
|
|
call g:RubyDebugger.commands.message(cmd)
|
|
elseif match(cmd, '<eval ') != -1
|
|
call g:RubyDebugger.commands.eval(cmd)
|
|
elseif match(cmd, '<processingException ') != -1
|
|
call g:RubyDebugger.commands.processing_exception(cmd)
|
|
elseif match(cmd, '<frames>') != -1
|
|
call g:RubyDebugger.commands.trace(cmd)
|
|
endif
|
|
endif
|
|
endfor
|
|
call g:RubyDebugger.queue.after_hook()
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
function! RubyDebugger.send_command_wrapper(command)
|
|
call g:RubyDebugger.send_command(a:command)
|
|
endfunction
|
|
|
|
" We set function this way, because we want have possibility to mock it by
|
|
" other function in tests
|
|
let RubyDebugger.send_command = function("<SID>send_message_to_debugger")
|
|
|
|
|
|
" Open variables window
|
|
function! RubyDebugger.open_variables() dict
|
|
call s:variables_window.toggle()
|
|
call g:RubyDebugger.logger.put("Opened variables window")
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Open breakpoints window
|
|
function! RubyDebugger.open_breakpoints() dict
|
|
call s:breakpoints_window.toggle()
|
|
call g:RubyDebugger.logger.put("Opened breakpoints window")
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Open frames window
|
|
function! RubyDebugger.open_frames() dict
|
|
call s:frames_window.toggle()
|
|
call g:RubyDebugger.logger.put("Opened frames window")
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Set/remove breakpoint at current position. If argument
|
|
" is given, it will set conditional breakpoint (argument is condition)
|
|
function! RubyDebugger.toggle_breakpoint(...) dict
|
|
let line = line(".")
|
|
let file = s:get_filename()
|
|
let existed_breakpoints = filter(copy(g:RubyDebugger.breakpoints), 'v:val.line == ' . line . ' && v:val.file == "' . escape(file, '\') . '"')
|
|
" If breakpoint with current file/line doesn't exist, create it. Otherwise -
|
|
" remove it
|
|
if empty(existed_breakpoints)
|
|
let breakpoint = s:Breakpoint.new(file, line)
|
|
call add(g:RubyDebugger.breakpoints, breakpoint)
|
|
call breakpoint.send_to_debugger()
|
|
else
|
|
let breakpoint = existed_breakpoints[0]
|
|
call filter(g:RubyDebugger.breakpoints, 'v:val.id != ' . breakpoint.id)
|
|
call breakpoint.delete()
|
|
endif
|
|
" Update info in Breakpoints window
|
|
if s:breakpoints_window.is_open()
|
|
call s:breakpoints_window.open()
|
|
exe "wincmd p"
|
|
endif
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Remove all breakpoints
|
|
function! RubyDebugger.remove_breakpoints() dict
|
|
for breakpoint in g:RubyDebugger.breakpoints
|
|
call breakpoint.delete()
|
|
endfor
|
|
let g:RubyDebugger.breakpoints = []
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Eval the passed in expression
|
|
function! RubyDebugger.eval(exp) dict
|
|
let quoted = s:quotify(a:exp)
|
|
call g:RubyDebugger.queue.add("eval " . quoted)
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Sets conditional breakpoint where cursor is placed
|
|
function! RubyDebugger.conditional_breakpoint(exp) dict
|
|
let line = line(".")
|
|
let file = s:get_filename()
|
|
let existed_breakpoints = filter(copy(g:RubyDebugger.breakpoints), 'v:val.line == ' . line . ' && v:val.file == "' . escape(file, '\') . '"')
|
|
" If breakpoint with current file/line doesn't exist, create it. Otherwise -
|
|
" remove it
|
|
if empty(existed_breakpoints)
|
|
echo "You can set condition only to already set breakpoints. Move cursor to set breakpoint and add condition"
|
|
else
|
|
let breakpoint = existed_breakpoints[0]
|
|
let quoted = s:quotify(a:exp)
|
|
call breakpoint.add_condition(quoted)
|
|
" Update info in Breakpoints window
|
|
if s:breakpoints_window.is_open()
|
|
call s:breakpoints_window.open()
|
|
exe "wincmd p"
|
|
endif
|
|
call g:RubyDebugger.queue.execute()
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Catch all exceptions with given name
|
|
function! RubyDebugger.catch_exception(exp) dict
|
|
if has_key(g:RubyDebugger, 'server') && g:RubyDebugger.server.is_running()
|
|
let quoted = s:quotify(a:exp)
|
|
let exception = s:Exception.new(quoted)
|
|
call add(g:RubyDebugger.exceptions, exception)
|
|
if s:breakpoints_window.is_open()
|
|
call s:breakpoints_window.open()
|
|
exe "wincmd p"
|
|
endif
|
|
call g:RubyDebugger.queue.execute()
|
|
else
|
|
echo "Sorry, but you can set Exceptional Breakpoints only with running debugger"
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Next
|
|
function! RubyDebugger.next() dict
|
|
call g:RubyDebugger.queue.add("next")
|
|
call s:clear_current_state()
|
|
call g:RubyDebugger.logger.put("Step over")
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Step
|
|
function! RubyDebugger.step() dict
|
|
call g:RubyDebugger.queue.add("step")
|
|
call s:clear_current_state()
|
|
call g:RubyDebugger.logger.put("Step into")
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Finish
|
|
function! RubyDebugger.finish() dict
|
|
call g:RubyDebugger.queue.add("finish")
|
|
call s:clear_current_state()
|
|
call g:RubyDebugger.logger.put("Step out")
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Continue
|
|
function! RubyDebugger.continue() dict
|
|
call g:RubyDebugger.queue.add("cont")
|
|
call s:clear_current_state()
|
|
call g:RubyDebugger.logger.put("Continue")
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Exit
|
|
function! RubyDebugger.exit() dict
|
|
call g:RubyDebugger.queue.add("exit")
|
|
call s:clear_current_state()
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Show output log of Ruby script
|
|
function! RubyDebugger.show_log() dict
|
|
exe "view " . s:server_output_file
|
|
setlocal autoread
|
|
" Per gorkunov's request
|
|
setlocal wrap
|
|
setlocal nonumber
|
|
if exists(":AnsiEsc")
|
|
exec ":AnsiEsc"
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Debug current opened test
|
|
function! RubyDebugger.run_test() dict
|
|
let file = s:get_filename()
|
|
if file =~ '_spec\.rb$'
|
|
call g:RubyDebugger.start(g:ruby_debugger_spec_path . ' ' . file)
|
|
elseif file =~ '\.feature$'
|
|
call g:RubyDebugger.start(g:ruby_debugger_cucumber_path . ' ' . file)
|
|
elseif file =~ '_test\.rb$'
|
|
call g:RubyDebugger.start(file)
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" *** Public interface (end)
|
|
|
|
|
|
|
|
|
|
" *** RubyDebugger Commands (what debugger returns)
|
|
|
|
|
|
" <breakpoint file="test.rb" line="1" threadId="1" />
|
|
" <suspended file='test.rb' line='1' threadId='1' />
|
|
" Jump to file/line where execution was suspended, set current line sign and get local variables
|
|
function! RubyDebugger.commands.jump_to_breakpoint(cmd) dict
|
|
let attrs = s:get_tag_attributes(a:cmd)
|
|
call s:jump_to_file(attrs.file, attrs.line)
|
|
call g:RubyDebugger.logger.put("Jumped to breakpoint " . attrs.file . ":" . attrs.line)
|
|
|
|
if has("signs")
|
|
exe ":sign place " . s:current_line_sign_id . " line=" . attrs.line . " name=current_line file=" . attrs.file
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" <exception file="test.rb" line="1" type="NameError" message="some exception message" threadId="4" />
|
|
" Show message error and jump to given file/line
|
|
function! RubyDebugger.commands.handle_exception(cmd) dict
|
|
let message_match = matchlist(a:cmd, 'message="\(.\{-}\)"')
|
|
call g:RubyDebugger.commands.jump_to_breakpoint(a:cmd)
|
|
echo "Exception message: " . s:unescape_html(message_match[1])
|
|
endfunction
|
|
|
|
|
|
" <catchpointSet exception="NoMethodError"/>
|
|
" Confirm setting of exception catcher
|
|
function! RubyDebugger.commands.set_exception(cmd) dict
|
|
let attrs = s:get_tag_attributes(a:cmd)
|
|
call g:RubyDebugger.logger.put("Exception successfully set: " . attrs.exception)
|
|
endfunction
|
|
|
|
|
|
" <breakpointAdded no="1" location="test.rb:2" />
|
|
" Add debugger info to breakpoints (pid of debugger, debugger breakpoint's id)
|
|
" Assign rest breakpoints to debugger recursively, if there are breakpoints
|
|
" from old server runnings or not assigned breakpoints (e.g., if you at first
|
|
" set some breakpoints, and then run the debugger by :Rdebugger)
|
|
function! RubyDebugger.commands.set_breakpoint(cmd)
|
|
let attrs = s:get_tag_attributes(a:cmd)
|
|
let file_match = matchlist(attrs.location, '\(.*\):\(.*\)')
|
|
let pid = g:RubyDebugger.server.rdebug_pid
|
|
|
|
" Find added breakpoint in array and assign debugger's info to it
|
|
for breakpoint in g:RubyDebugger.breakpoints
|
|
if expand(breakpoint.file) == expand(file_match[1]) && expand(breakpoint.line) == expand(file_match[2])
|
|
let breakpoint.debugger_id = attrs.no
|
|
let breakpoint.rdebug_pid = pid
|
|
if has_key(breakpoint, 'condition')
|
|
call breakpoint.add_condition(breakpoint.condition)
|
|
endif
|
|
endif
|
|
endfor
|
|
|
|
call g:RubyDebugger.logger.put("Breakpoint is set: " . file_match[1] . ":" . file_match[2])
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" <variables>
|
|
" <variable name="array" kind="local" value="Array (2 element(s))" type="Array" hasChildren="true" objectId="-0x2418a904"/>
|
|
" </variables>
|
|
" Assign list of got variables to parent variable and (optionally) show them
|
|
function! RubyDebugger.commands.set_variables(cmd)
|
|
let tags = s:get_tags(a:cmd)
|
|
let list_of_variables = []
|
|
|
|
" Create hash from list of tags
|
|
for tag in tags
|
|
let attrs = s:get_tag_attributes(tag)
|
|
let variable = s:Var.new(attrs)
|
|
call add(list_of_variables, variable)
|
|
endfor
|
|
|
|
" If there is no variables, create unnamed root variable. Local variables
|
|
" will be chilren of this variable
|
|
if g:RubyDebugger.variables == {}
|
|
let g:RubyDebugger.variables = s:VarParent.new({'hasChildren': 'true'})
|
|
let g:RubyDebugger.variables.is_open = 1
|
|
let g:RubyDebugger.variables.children = []
|
|
endif
|
|
|
|
" If g:RubyDebugger.current_variable exists, then it contains parent
|
|
" variable of got subvariables. Assign them to it.
|
|
if has_key(g:RubyDebugger, 'current_variable')
|
|
let variable = g:RubyDebugger.current_variable
|
|
if variable != {}
|
|
call variable.add_childs(list_of_variables)
|
|
call g:RubyDebugger.logger.put("Opening child variable: " . variable.attributes.objectId)
|
|
" Variables Window is always open if we got subvariables
|
|
call s:variables_window.open()
|
|
else
|
|
call g:RubyDebugger.logger.put("Can't found variable")
|
|
endif
|
|
unlet g:RubyDebugger.current_variable
|
|
else
|
|
" Otherwise, assign them to unnamed root variable
|
|
if g:RubyDebugger.variables.children == []
|
|
call g:RubyDebugger.variables.add_childs(list_of_variables)
|
|
call g:RubyDebugger.logger.put("Initializing local variables")
|
|
if s:variables_window.is_open()
|
|
" show variables only if Variables Window is open
|
|
call s:variables_window.open()
|
|
endif
|
|
endif
|
|
endif
|
|
|
|
endfunction
|
|
|
|
|
|
" <eval expression="User.all" value="[#User ... ]" />
|
|
" Just show result of evaluation
|
|
function! RubyDebugger.commands.eval(cmd)
|
|
" rdebug-ide-gem doesn't escape attributes of tag properly, so we should not
|
|
" use usual attribute extractor here...
|
|
let match = matchlist(a:cmd, "<eval expression=\"\\(.\\{-}\\)\" value=\"\\(.*\\)\" \\/>")
|
|
echo "Evaluated expression:\n" . s:unescape_html(match[1]) ."\nResulted value is:\n" . match[2] . "\n"
|
|
endfunction
|
|
|
|
|
|
" <processingException type="SyntaxError" message="some message" />
|
|
" Just show exception message
|
|
function! RubyDebugger.commands.processing_exception(cmd)
|
|
let attrs = s:get_tag_attributes(a:cmd)
|
|
let message = "RubyDebugger Exception, type: " . attrs.type . ", message: " . attrs.message
|
|
echo message
|
|
call g:RubyDebugger.logger.put(message)
|
|
endfunction
|
|
|
|
|
|
" <frames>
|
|
" <frame no='1' file='/path/to/file.rb' line='21' current='true' />
|
|
" <frame no='2' file='/path/to/file.rb' line='11' />
|
|
" </frames>
|
|
" Assign all frames, fill Frames window by them
|
|
function! RubyDebugger.commands.trace(cmd)
|
|
let tags = s:get_tags(a:cmd)
|
|
let list_of_frames = []
|
|
|
|
" Create hash from list of tags
|
|
for tag in tags
|
|
let attrs = s:get_tag_attributes(tag)
|
|
let frame = s:Frame.new(attrs)
|
|
call add(list_of_frames, frame)
|
|
endfor
|
|
|
|
let g:RubyDebugger.frames = list_of_frames
|
|
|
|
if s:frames_window.is_open()
|
|
" show backtrace only if Backtrace Window is open
|
|
call s:frames_window.open()
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" <error>Error</error>
|
|
" Just show error
|
|
function! RubyDebugger.commands.error(cmd)
|
|
let error_match = s:get_inner_tags(a:cmd)
|
|
if !empty(error_match)
|
|
let error = error_match[1]
|
|
echo "RubyDebugger Error: " . error
|
|
call g:RubyDebugger.logger.put("Got error: " . error)
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" <message>Message</message>
|
|
" Just show message
|
|
function! RubyDebugger.commands.message(cmd)
|
|
let message_match = s:get_inner_tags(a:cmd)
|
|
if !empty(message_match)
|
|
let message = message_match[1]
|
|
echo "RubyDebugger Message: " . message
|
|
call g:RubyDebugger.logger.put("Got message: " . message)
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" *** End of debugger Commands
|
|
|
|
|
|
|
|
" *** Window class (start). Abstract Class for creating window.
|
|
" Must be inherited. Mostly, stolen from the NERDTree.
|
|
|
|
let s:Window = {}
|
|
let s:Window['next_buffer_number'] = 1
|
|
let s:Window['position'] = 'botright'
|
|
let s:Window['size'] = 10
|
|
|
|
" ** Public methods
|
|
|
|
" Constructs new window
|
|
function! s:Window.new(name, title) dict
|
|
let new_variable = copy(self)
|
|
let new_variable.name = a:name
|
|
let new_variable.title = a:title
|
|
return new_variable
|
|
endfunction
|
|
|
|
|
|
" Clear all data from window
|
|
function! s:Window.clear() dict
|
|
silent 1,$delete _
|
|
endfunction
|
|
|
|
|
|
" Close window
|
|
function! s:Window.close() dict
|
|
if !self.is_open()
|
|
throw "RubyDebug: Window " . self.name . " is not open"
|
|
endif
|
|
|
|
if winnr("$") != 1
|
|
call self.focus()
|
|
close
|
|
exe "wincmd p"
|
|
else
|
|
" If this is only one window, just quit
|
|
:q
|
|
endif
|
|
call self._log("Closed window with name: " . self.name)
|
|
endfunction
|
|
|
|
|
|
" Get window number
|
|
function! s:Window.get_number() dict
|
|
if self._exist_for_tab()
|
|
return bufwinnr(self._buf_name())
|
|
else
|
|
return -1
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Display data to the window
|
|
function! s:Window.display()
|
|
call self._log("Start displaying data in window with name: " . self.name)
|
|
call self.focus()
|
|
setlocal modifiable
|
|
|
|
let current_line = line(".")
|
|
let current_column = col(".")
|
|
let top_line = line("w0")
|
|
|
|
call self.clear()
|
|
|
|
call self._insert_data()
|
|
call self._restore_view(top_line, current_line, current_column)
|
|
|
|
setlocal nomodifiable
|
|
call self._log("Complete displaying data in window with name: " . self.name)
|
|
endfunction
|
|
|
|
|
|
" Put cursor to the window
|
|
function! s:Window.focus() dict
|
|
exe self.get_number() . " wincmd w"
|
|
call self._log("Set focus to window with name: " . self.name)
|
|
endfunction
|
|
|
|
|
|
" Return 1 if window is opened
|
|
function! s:Window.is_open() dict
|
|
return self.get_number() != -1
|
|
endfunction
|
|
|
|
|
|
" Open window and display data (stolen from NERDTree)
|
|
function! s:Window.open() dict
|
|
if !self.is_open()
|
|
" create the window
|
|
silent exec self.position . ' ' . self.size . ' new'
|
|
|
|
if !self._exist_for_tab()
|
|
" If the window is not opened/exists, create new
|
|
call self._set_buf_name(self._next_buffer_name())
|
|
silent! exec "edit " . self._buf_name()
|
|
" This function does not exist in Window class and should be declared in
|
|
" descendants
|
|
call self.bind_mappings()
|
|
else
|
|
" Or just jump to opened buffer
|
|
silent! exec "buffer " . self._buf_name()
|
|
endif
|
|
|
|
" set buffer options
|
|
setlocal winfixheight
|
|
setlocal noswapfile
|
|
setlocal buftype=nofile
|
|
setlocal nowrap
|
|
setlocal foldcolumn=0
|
|
setlocal nobuflisted
|
|
setlocal nospell
|
|
setlocal nolist
|
|
iabc <buffer>
|
|
setlocal cursorline
|
|
setfiletype ruby_debugger_window
|
|
call self._log("Opened window with name: " . self.name)
|
|
endif
|
|
|
|
if has("syntax") && exists("g:syntax_on") && !has("syntax_items")
|
|
call self.setup_syntax_highlighting()
|
|
endif
|
|
|
|
call self.display()
|
|
endfunction
|
|
|
|
|
|
" Open/close window
|
|
function! s:Window.toggle() dict
|
|
call self._log("Toggling window with name: " . self.name)
|
|
if self._exist_for_tab() && self.is_open()
|
|
call self.close()
|
|
else
|
|
call self.open()
|
|
end
|
|
endfunction
|
|
|
|
|
|
" ** Private methods
|
|
|
|
|
|
" Return buffer name, that is stored in tab variable
|
|
function! s:Window._buf_name() dict
|
|
return t:window_{self.name}_buf_name
|
|
endfunction
|
|
|
|
|
|
" Return 1 if the window exists in current tab
|
|
function! s:Window._exist_for_tab() dict
|
|
return exists("t:window_" . self.name . "_buf_name")
|
|
endfunction
|
|
|
|
|
|
" Insert data to the window
|
|
function! s:Window._insert_data() dict
|
|
let old_p = @p
|
|
" Put data to the register and then show it by 'put' command
|
|
let @p = self.render()
|
|
silent exe "normal \"pP"
|
|
let @p = old_p
|
|
call self._log("Inserted data to window with name: " . self.name)
|
|
endfunction
|
|
|
|
|
|
function! s:Window._log(string) dict
|
|
if has_key(self, 'logger')
|
|
call self.logger.put(a:string)
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Calculate correct name for the window
|
|
function! s:Window._next_buffer_name() dict
|
|
let name = self.name . s:Window.next_buffer_number
|
|
let s:Window.next_buffer_number += 1
|
|
return name
|
|
endfunction
|
|
|
|
|
|
" Restore the view
|
|
function! s:Window._restore_view(top_line, current_line, current_column) dict
|
|
let old_scrolloff=&scrolloff
|
|
let &scrolloff=0
|
|
call cursor(a:top_line, 1)
|
|
normal! zt
|
|
call cursor(a:current_line, a:current_column)
|
|
let &scrolloff = old_scrolloff
|
|
call self._log("Restored view of window with name: " . self.name)
|
|
endfunction
|
|
|
|
|
|
function! s:Window._set_buf_name(name) dict
|
|
let t:window_{self.name}_buf_name = a:name
|
|
endfunction
|
|
|
|
|
|
" *** Window class (end)
|
|
|
|
|
|
|
|
" *** WindowVariables class (start)
|
|
|
|
" Inherits variables window from abstract window class
|
|
let s:WindowVariables = copy(s:Window)
|
|
|
|
" ** Public methods
|
|
|
|
function! s:WindowVariables.bind_mappings()
|
|
nnoremap <buffer> <2-leftmouse> :call <SID>window_variables_activate_node()<cr>
|
|
nnoremap <buffer> o :call <SID>window_variables_activate_node()<cr>"
|
|
endfunction
|
|
|
|
|
|
" Returns string that contains all variables (for Window.display())
|
|
function! s:WindowVariables.render() dict
|
|
let variables = self.title . "\n"
|
|
let variables .= (g:RubyDebugger.variables == {} ? '' : g:RubyDebugger.variables.render())
|
|
return variables
|
|
endfunction
|
|
|
|
|
|
" TODO: Is there some way to call s:WindowVariables.activate_node from mapping
|
|
" command?
|
|
" Expand/collapse variable under cursor
|
|
function! s:window_variables_activate_node()
|
|
let variable = s:Var.get_selected()
|
|
if variable != {} && variable.type == "VarParent"
|
|
if variable.is_open
|
|
call variable.close()
|
|
else
|
|
call variable.open()
|
|
endif
|
|
endif
|
|
call g:RubyDebugger.queue.execute()
|
|
endfunction
|
|
|
|
|
|
" Add syntax highlighting
|
|
function! s:WindowVariables.setup_syntax_highlighting()
|
|
execute "syn match rdebugTitle #" . self.title . "#"
|
|
|
|
syn match rdebugPart #[| `]\+#
|
|
syn match rdebugPartFile #[| `]\+-# contains=rdebugPart nextgroup=rdebugChild contained
|
|
syn match rdebugChild #.\{-}\t# nextgroup=rdebugType contained
|
|
|
|
syn match rdebugClosable #[| `]\+\~# contains=rdebugPart nextgroup=rdebugParent contained
|
|
syn match rdebugOpenable #[| `]\++# contains=rdebugPart nextgroup=rdebugParent contained
|
|
syn match rdebugParent #.\{-}\t# nextgroup=rdebugType contained
|
|
|
|
syn match rdebugType #.\{-}\t# nextgroup=rdebugValue contained
|
|
syn match rdebugValue #.*\t#he=e-1 nextgroup=rdebugId contained
|
|
syn match rdebugId #.*# contained
|
|
|
|
syn match rdebugParentLine '[| `]\+[+\~].*' contains=rdebugClosable,rdebugOpenable transparent
|
|
syn match rdebugChildLine '[| `]\+-.*' contains=rdebugPartFile transparent
|
|
|
|
hi def link rdebugTitle Identifier
|
|
hi def link rdebugClosable Type
|
|
hi def link rdebugOpenable Title
|
|
hi def link rdebugPart Special
|
|
hi def link rdebugPartFile Type
|
|
hi def link rdebugChild Normal
|
|
hi def link rdebugParent Directory
|
|
hi def link rdebugType Type
|
|
hi def link rdebugValue Special
|
|
hi def link rdebugId Ignore
|
|
endfunction
|
|
|
|
|
|
" *** WindowVariables class (end)
|
|
|
|
|
|
|
|
" *** WindowBreakpoints class (start)
|
|
|
|
" Inherits WindowBreakpoints from Window
|
|
let s:WindowBreakpoints = copy(s:Window)
|
|
|
|
" ** Public methods
|
|
|
|
function! s:WindowBreakpoints.bind_mappings()
|
|
nnoremap <buffer> <2-leftmouse> :call <SID>window_breakpoints_activate_node()<cr>
|
|
nnoremap <buffer> o :call <SID>window_breakpoints_activate_node()<cr>
|
|
nnoremap <buffer> d :call <SID>window_breakpoints_delete_node()<cr>
|
|
endfunction
|
|
|
|
|
|
" Returns string that contains all breakpoints (for Window.display())
|
|
function! s:WindowBreakpoints.render() dict
|
|
let breakpoints = ""
|
|
let breakpoints .= self.title . "\n"
|
|
for breakpoint in g:RubyDebugger.breakpoints
|
|
let breakpoints .= breakpoint.render()
|
|
endfor
|
|
let exceptions = map(copy(g:RubyDebugger.exceptions), 'v:val.render()')
|
|
let breakpoints .= "\nException breakpoints: " . join(exceptions, ", ")
|
|
return breakpoints
|
|
endfunction
|
|
|
|
|
|
" TODO: Is there some way to call s:WindowBreakpoints.activate_node from mapping
|
|
" command?
|
|
" Open breakpoint under cursor
|
|
function! s:window_breakpoints_activate_node()
|
|
let breakpoint = s:Breakpoint.get_selected()
|
|
if breakpoint != {}
|
|
call breakpoint.open()
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Delete breakpoint under cursor
|
|
function! s:window_breakpoints_delete_node()
|
|
let breakpoint = s:Breakpoint.get_selected()
|
|
if breakpoint != {}
|
|
call breakpoint.delete()
|
|
call filter(g:RubyDebugger.breakpoints, "v:val.id != " . breakpoint.id)
|
|
call s:breakpoints_window.open()
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Add syntax highlighting
|
|
function! s:WindowBreakpoints.setup_syntax_highlighting() dict
|
|
execute "syn match rdebugTitle #" . self.title . "#"
|
|
|
|
syn match rdebugId "^\d\+\s" contained nextgroup=rdebugDebuggerId
|
|
syn match rdebugDebuggerId "\d*\s" contained nextgroup=rdebugFile
|
|
syn match rdebugFile ".*:" contained nextgroup=rdebugLine
|
|
syn match rdebugLine "\d\+" contained
|
|
|
|
syn match rdebugWrapper "^\d\+.*" contains=rdebugId transparent
|
|
|
|
hi def link rdebugId Directory
|
|
hi def link rdebugDebuggerId Type
|
|
hi def link rdebugFile Normal
|
|
hi def link rdebugLine Special
|
|
endfunction
|
|
|
|
|
|
" *** WindowBreakpoints class (end)
|
|
|
|
|
|
|
|
" *** WindowFrames class (start)
|
|
|
|
" Inherits WindowFrames from Window
|
|
let s:WindowFrames = copy(s:Window)
|
|
|
|
" ** Public methods
|
|
|
|
function! s:WindowFrames.bind_mappings()
|
|
nnoremap <buffer> <2-leftmouse> :call <SID>window_frames_activate_node()<cr>
|
|
nnoremap <buffer> o :call <SID>window_frames_activate_node()<cr>
|
|
endfunction
|
|
|
|
|
|
" Returns string that contains all frames (for Window.display())
|
|
function! s:WindowFrames.render() dict
|
|
let frames = ""
|
|
let frames .= self.title . "\n"
|
|
for frame in g:RubyDebugger.frames
|
|
let frames .= frame.render()
|
|
endfor
|
|
return frames
|
|
endfunction
|
|
|
|
|
|
" Open frame under cursor
|
|
function! s:window_frames_activate_node()
|
|
let frame = s:Frame.get_selected()
|
|
if frame != {}
|
|
call frame.open()
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Add syntax highlighting
|
|
function! s:WindowFrames.setup_syntax_highlighting() dict
|
|
execute "syn match rdebugTitle #" . self.title . "#"
|
|
|
|
syn match rdebugId "^\d\+\s" contained nextgroup=rdebugFile
|
|
syn match rdebugFile ".*:" contained nextgroup=rdebugLine
|
|
syn match rdebugLine "\d\+" contained
|
|
|
|
syn match rdebugWrapper "^\d\+.*" contains=rdebugId transparent
|
|
|
|
hi def link rdebugId Directory
|
|
hi def link rdebugFile Normal
|
|
hi def link rdebugLine Special
|
|
endfunction
|
|
|
|
|
|
" *** WindowFrames class (end)
|
|
|
|
|
|
|
|
|
|
" *** Var proxy class (start)
|
|
|
|
let s:Var = { 'id' : 0 }
|
|
|
|
" ** Public methods
|
|
|
|
" This is a proxy method for creating new variable
|
|
function! s:Var.new(attrs)
|
|
if has_key(a:attrs, 'hasChildren') && a:attrs['hasChildren'] == 'true'
|
|
return s:VarParent.new(a:attrs)
|
|
else
|
|
return s:VarChild.new(a:attrs)
|
|
end
|
|
endfunction
|
|
|
|
|
|
" Get variable under cursor
|
|
function! s:Var.get_selected()
|
|
let line = getline(".")
|
|
" Get its id - it is last in the string
|
|
let match = matchlist(line, '.*\t\(\d\+\)$')
|
|
let id = get(match, 1)
|
|
if id
|
|
let variable = g:RubyDebugger.variables.find_variable({'id' : id})
|
|
return variable
|
|
else
|
|
return {}
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" *** Var proxy class (end)
|
|
|
|
|
|
|
|
" *** VarChild class (start)
|
|
|
|
let s:VarChild = {}
|
|
|
|
" ** Public methods
|
|
|
|
" Constructs new variable without childs
|
|
function! s:VarChild.new(attrs)
|
|
let new_variable = copy(self)
|
|
let new_variable.attributes = a:attrs
|
|
let new_variable.parent = {}
|
|
let new_variable.level = 0
|
|
let new_variable.type = "VarChild"
|
|
let s:Var.id += 1
|
|
let new_variable.id = s:Var.id
|
|
return new_variable
|
|
endfunction
|
|
|
|
|
|
" Renders data of the variable
|
|
function! s:VarChild.render()
|
|
return self._render(0, 0, [], len(self.parent.children) ==# 1)
|
|
endfunction
|
|
|
|
|
|
" VarChild can't be opened because it can't have children. But VarParent can
|
|
function! s:VarChild.open()
|
|
return 0
|
|
endfunction
|
|
|
|
|
|
" VarChild can't be closed because it can't have children. But VarParent can
|
|
function! s:VarChild.close()
|
|
return 0
|
|
endfunction
|
|
|
|
|
|
" VarChild can't be parent. But VarParent can. If Var have hasChildren ==
|
|
" true, then it is parent
|
|
function! s:VarChild.is_parent()
|
|
return has_key(self.attributes, 'hasChildren') && get(self.attributes, 'hasChildren') ==# 'true'
|
|
endfunction
|
|
|
|
|
|
" Output format for Variables Window
|
|
function! s:VarChild.to_s()
|
|
return get(self.attributes, "name", "undefined") . "\t" . get(self.attributes, "type", "undefined") . "\t" . get(self.attributes, "value", "undefined") . "\t" . get(self, "id", "0")
|
|
endfunction
|
|
|
|
|
|
" Find and return variable by given Dict of attrs, e.g.: {'name' : 'var1'}
|
|
function! s:VarChild.find_variable(attrs)
|
|
if self._match_attributes(a:attrs)
|
|
return self
|
|
else
|
|
return {}
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Find and return array of variables that match given Dict of attrs
|
|
function! s:VarChild.find_variables(attrs)
|
|
let variables = []
|
|
if self._match_attributes(a:attrs)
|
|
call add(variables, self)
|
|
endif
|
|
return variables
|
|
endfunction
|
|
|
|
|
|
" ** Private methods
|
|
|
|
|
|
" Recursive function, that renders Variable and all its childs (if they are
|
|
" presented). Stolen from NERDTree
|
|
function! s:VarChild._render(depth, draw_text, vertical_map, is_last_child)
|
|
let output = ""
|
|
if a:draw_text ==# 1
|
|
let tree_parts = ''
|
|
|
|
" get all the leading spaces and vertical tree parts for this line
|
|
if a:depth > 1
|
|
for j in a:vertical_map[0:-2]
|
|
if j ==# 1
|
|
let tree_parts = tree_parts . '| '
|
|
else
|
|
let tree_parts = tree_parts . ' '
|
|
endif
|
|
endfor
|
|
endif
|
|
|
|
" get the last vertical tree part for this line which will be different
|
|
" if this node is the last child of its parent
|
|
if a:is_last_child
|
|
let tree_parts = tree_parts . '`'
|
|
else
|
|
let tree_parts = tree_parts . '|'
|
|
endif
|
|
|
|
" smack the appropriate dir/file symbol on the line before the file/dir
|
|
" name itself
|
|
if self.is_parent()
|
|
if self.is_open
|
|
let tree_parts = tree_parts . '~'
|
|
else
|
|
let tree_parts = tree_parts . '+'
|
|
endif
|
|
else
|
|
let tree_parts = tree_parts . '-'
|
|
endif
|
|
let line = tree_parts . self.to_s()
|
|
let output = output . line . "\n"
|
|
|
|
endif
|
|
|
|
if self.is_parent() && self.is_open
|
|
if len(self.children) > 0
|
|
|
|
" draw all the nodes children except the last
|
|
let last_index = len(self.children) - 1
|
|
if last_index > 0
|
|
for i in self.children[0:last_index - 1]
|
|
let output = output . i._render(a:depth + 1, 1, add(copy(a:vertical_map), 1), 0)
|
|
endfor
|
|
endif
|
|
|
|
" draw the last child, indicating that it IS the last
|
|
let output = output . self.children[last_index]._render(a:depth + 1, 1, add(copy(a:vertical_map), 0), 1)
|
|
|
|
endif
|
|
endif
|
|
|
|
return output
|
|
|
|
endfunction
|
|
|
|
|
|
" Return 1 if *all* given attributes (pairs key/value) match to current
|
|
" variable
|
|
function! s:VarChild._match_attributes(attrs)
|
|
let conditions = 1
|
|
for attr in keys(a:attrs)
|
|
if has_key(self.attributes, attr)
|
|
" If current key is contained in attributes of variable (they were
|
|
" attributes in <variable /> tag, then trying to match there.
|
|
let conditions = conditions && self.attributes[attr] == a:attrs[attr]
|
|
elseif has_key(self, attr)
|
|
" Otherwise, if current key is contained in auxiliary attributes of the
|
|
" variable, trying to match there
|
|
let conditions = conditions && self[attr] == a:attrs[attr]
|
|
else
|
|
" Otherwise, this variable is not match
|
|
let conditions = 0
|
|
break
|
|
endif
|
|
endfor
|
|
return conditions
|
|
endfunction
|
|
|
|
|
|
" *** VarChild class (end)
|
|
|
|
|
|
|
|
|
|
" *** VarParent class (start)
|
|
|
|
" Inherits VarParent from VarChild
|
|
let s:VarParent = copy(s:VarChild)
|
|
|
|
" ** Public methods
|
|
|
|
|
|
" Initializes new variable with childs
|
|
function! s:VarParent.new(attrs)
|
|
if !has_key(a:attrs, 'hasChildren') || a:attrs['hasChildren'] != 'true'
|
|
throw "RubyDebug: VarParent must be initialized with hasChildren = true"
|
|
endif
|
|
let new_variable = copy(self)
|
|
let new_variable.attributes = a:attrs
|
|
let new_variable.parent = {}
|
|
let new_variable.is_open = 0
|
|
let new_variable.level = 0
|
|
let new_variable.children = []
|
|
let new_variable.type = "VarParent"
|
|
let s:Var.id += 1
|
|
let new_variable.id = s:Var.id
|
|
return new_variable
|
|
endfunction
|
|
|
|
|
|
" Open variable, init its children and display them
|
|
function! s:VarParent.open()
|
|
let self.is_open = 1
|
|
call self._init_children()
|
|
return 0
|
|
endfunction
|
|
|
|
|
|
" Close variable and display it
|
|
function! s:VarParent.close()
|
|
let self.is_open = 0
|
|
call s:variables_window.display()
|
|
if has_key(g:RubyDebugger, "current_variable")
|
|
unlet g:RubyDebugger.current_variable
|
|
endif
|
|
return 0
|
|
endfunction
|
|
|
|
|
|
" Renders data of the variable
|
|
function! s:VarParent.render()
|
|
return self._render(0, 0, [], len(self.children) ==# 1)
|
|
endfunction
|
|
|
|
|
|
|
|
" Add childs to the variable. You always should use this method instead of
|
|
" explicit assigning to children property (like 'add(self.children, variables)')
|
|
function! s:VarParent.add_childs(childs)
|
|
" If children are given by array, extend self.children by this array
|
|
if type(a:childs) == type([])
|
|
for child in a:childs
|
|
let child.parent = self
|
|
let child.level = self.level + 1
|
|
endfor
|
|
call extend(self.children, a:childs)
|
|
else
|
|
" Otherwise, add child to self.children
|
|
let a:childs.parent = self
|
|
let child.level = self.level + 1
|
|
call add(self.children, a:childs)
|
|
end
|
|
endfunction
|
|
|
|
|
|
" Find and return variable by given Dict of attrs, e.g.: {'name' : 'var1'}
|
|
" If current variable doesn't match these attributes, try to find in children
|
|
function! s:VarParent.find_variable(attrs)
|
|
if self._match_attributes(a:attrs)
|
|
return self
|
|
else
|
|
for child in self.children
|
|
let result = child.find_variable(a:attrs)
|
|
if result != {}
|
|
return result
|
|
endif
|
|
endfor
|
|
endif
|
|
return {}
|
|
endfunction
|
|
|
|
|
|
" Find and return array of variables that match given Dict of attrs.
|
|
" Try to match current variable and its children
|
|
function! s:VarParent.find_variables(attrs)
|
|
let variables = []
|
|
if self._match_attributes(a:attrs)
|
|
call add(variables, self)
|
|
endif
|
|
for child in self.children
|
|
call extend(variables, child.find_variables(a:attrs))
|
|
endfor
|
|
return variables
|
|
endfunction
|
|
|
|
|
|
" ** Private methods
|
|
|
|
|
|
" Update children of the variable
|
|
function! s:VarParent._init_children()
|
|
" Remove all the current child nodes
|
|
let self.children = []
|
|
|
|
" Get children
|
|
if has_key(self.attributes, 'objectId')
|
|
let g:RubyDebugger.current_variable = self
|
|
call g:RubyDebugger.queue.add('var instance ' . self.attributes.objectId)
|
|
endif
|
|
|
|
endfunction
|
|
|
|
|
|
" *** VarParent class (end)
|
|
|
|
|
|
|
|
" *** Logger class (start)
|
|
|
|
|
|
let s:Logger = {}
|
|
|
|
function! s:Logger.new(file)
|
|
let new_variable = copy(self)
|
|
let new_variable.file = a:file
|
|
call writefile([], new_variable.file)
|
|
return new_variable
|
|
endfunction
|
|
|
|
|
|
" Log datetime and then message
|
|
function! s:Logger.put(string)
|
|
let file = readfile(self.file)
|
|
let string = strftime("%Y/%m/%d %H:%M:%S") . ' ' . a:string
|
|
call add(file, string)
|
|
call writefile(file, self.file)
|
|
endfunction
|
|
|
|
|
|
" *** Logger class (end)
|
|
|
|
|
|
" *** Breakpoint class (start)
|
|
|
|
let s:Breakpoint = { 'id': 0 }
|
|
|
|
" ** Public methods
|
|
|
|
" Constructor of new brekpoint. Create new breakpoint and set sign.
|
|
function! s:Breakpoint.new(file, line)
|
|
let var = copy(self)
|
|
let var.file = a:file
|
|
let var.line = a:line
|
|
let s:Breakpoint.id += 1
|
|
let var.id = s:Breakpoint.id
|
|
|
|
call var._set_sign()
|
|
call var._log("Set breakpoint to: " . var.file . ":" . var.line)
|
|
return var
|
|
endfunction
|
|
|
|
|
|
" Destroyer of the breakpoint. It just sends commands to debugger and destroys
|
|
" sign, but you should manually remove it from breakpoints array
|
|
function! s:Breakpoint.delete() dict
|
|
call self._unset_sign()
|
|
call self._send_delete_to_debugger()
|
|
endfunction
|
|
|
|
|
|
" Add condition to breakpoint. If server is not running, just store it, it
|
|
" will be evaluated after starting the server
|
|
function! s:Breakpoint.add_condition(condition) dict
|
|
let self.condition = a:condition
|
|
if has_key(g:RubyDebugger, 'server') && g:RubyDebugger.server.is_running() && has_key(self, 'debugger_id')
|
|
call g:RubyDebugger.queue.add(self.condition_command())
|
|
endif
|
|
endfunction
|
|
|
|
|
|
|
|
" Send adding breakpoint message to debugger, if it is run
|
|
function! s:Breakpoint.send_to_debugger() dict
|
|
if has_key(g:RubyDebugger, 'server') && g:RubyDebugger.server.is_running()
|
|
call g:RubyDebugger.queue.add(self.command())
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Command for setting breakpoint (e.g.: 'break /path/to/file:23')
|
|
function! s:Breakpoint.command() dict
|
|
return 'break ' . self.file . ':' . self.line
|
|
endfunction
|
|
|
|
|
|
" Command for adding condition to breakpoin (e.g.: 'condition 1 x>5')
|
|
function! s:Breakpoint.condition_command() dict
|
|
return 'condition ' . self.debugger_id . ' ' . self.condition
|
|
endfunction
|
|
|
|
|
|
" Find and return breakpoint under cursor
|
|
function! s:Breakpoint.get_selected() dict
|
|
let line = getline(".")
|
|
let match = matchlist(line, '^\(\d\+\)')
|
|
let id = get(match, 1)
|
|
let breakpoints = filter(copy(g:RubyDebugger.breakpoints), "v:val.id == " . id)
|
|
if !empty(breakpoints)
|
|
return breakpoints[0]
|
|
else
|
|
return {}
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Output format for Breakpoints Window
|
|
function! s:Breakpoint.render() dict
|
|
let output = self.id . " " . (exists("self.debugger_id") ? self.debugger_id : '') . " " . self.file . ":" . self.line
|
|
if exists("self.condition")
|
|
let output .= " " . self.condition
|
|
endif
|
|
return output . "\n"
|
|
endfunction
|
|
|
|
|
|
" Open breakpoint in existed/new window
|
|
function! s:Breakpoint.open() dict
|
|
call s:jump_to_file(self.file, self.line)
|
|
endfunction
|
|
|
|
|
|
" ** Private methods
|
|
|
|
|
|
function! s:Breakpoint._set_sign() dict
|
|
if has("signs")
|
|
exe ":sign place " . self.id . " line=" . self.line . " name=breakpoint file=" . self.file
|
|
endif
|
|
endfunction
|
|
|
|
|
|
function! s:Breakpoint._unset_sign() dict
|
|
if has("signs")
|
|
exe ":sign unplace " . self.id
|
|
endif
|
|
endfunction
|
|
|
|
|
|
function! s:Breakpoint._log(string) dict
|
|
call g:RubyDebugger.logger.put(a:string)
|
|
endfunction
|
|
|
|
|
|
" Send deleting breakpoint message to debugger, if it is run
|
|
" (e.g.: 'delete 5')
|
|
function! s:Breakpoint._send_delete_to_debugger() dict
|
|
if has_key(g:RubyDebugger, 'server') && g:RubyDebugger.server.is_running()
|
|
let message = 'delete ' . self.debugger_id
|
|
call g:RubyDebugger.queue.add(message)
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" *** Breakpoint class (end)
|
|
|
|
" *** Exception class (start)
|
|
" These are ruby exceptions we catch with 'catch Exception' command
|
|
" (:RdbCatch)
|
|
|
|
let s:Exception = { }
|
|
|
|
" ** Public methods
|
|
|
|
" Constructor of new exception.
|
|
function! s:Exception.new(name)
|
|
let var = copy(self)
|
|
let var.name = a:name
|
|
call var._log("Trying to set exception: " . var.name)
|
|
call g:RubyDebugger.queue.add(var.command())
|
|
return var
|
|
endfunction
|
|
|
|
|
|
" Command for setting exception (e.g.: 'catch NameError')
|
|
function! s:Exception.command() dict
|
|
return 'catch ' . self.name
|
|
endfunction
|
|
|
|
|
|
" Output format for Breakpoints Window
|
|
function! s:Exception.render() dict
|
|
return self.name
|
|
endfunction
|
|
|
|
|
|
" ** Private methods
|
|
|
|
|
|
function! s:Exception._log(string) dict
|
|
call g:RubyDebugger.logger.put(a:string)
|
|
endfunction
|
|
|
|
|
|
" *** Exception class (end)
|
|
|
|
|
|
|
|
|
|
" *** Frame class (start)
|
|
|
|
let s:Frame = { }
|
|
|
|
" ** Public methods
|
|
|
|
" Constructor of new frame.
|
|
" Create new frame and set sign to it.
|
|
function! s:Frame.new(attrs)
|
|
let var = copy(self)
|
|
let var.no = a:attrs.no
|
|
let var.file = a:attrs.file
|
|
let var.line = a:attrs.line
|
|
if has_key(a:attrs, 'current')
|
|
let var.current = (a:attrs.current == 'true')
|
|
else
|
|
let var.current = 0
|
|
endif
|
|
"let s:sign_id += 1
|
|
"let var.sign_id = s:sign_id
|
|
"call var._set_sign()
|
|
return var
|
|
endfunction
|
|
|
|
|
|
" Find and return frame under cursor
|
|
function! s:Frame.get_selected() dict
|
|
let line = getline(".")
|
|
let match = matchlist(line, '^\(\d\+\)')
|
|
let no = get(match, 1)
|
|
let frames = filter(copy(g:RubyDebugger.frames), "v:val.no == " . no)
|
|
if !empty(frames)
|
|
return frames[0]
|
|
else
|
|
return {}
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Output format for Frame Window
|
|
function! s:Frame.render() dict
|
|
return self.no . (self.current ? ' Current' : ''). " " . self.file . ":" . self.line . "\n"
|
|
endfunction
|
|
|
|
|
|
" Open frame in existed/new window
|
|
function! s:Frame.open() dict
|
|
call s:jump_to_file(self.file, self.line)
|
|
endfunction
|
|
|
|
|
|
" ** Private methods
|
|
|
|
function! s:Frame._log(string) dict
|
|
call g:RubyDebugger.logger.put(a:string)
|
|
endfunction
|
|
|
|
|
|
function! s:Frame._set_sign() dict
|
|
if has("signs")
|
|
exe ":sign place " . self.sign_id . " line=" . self.line . " name=frame file=" . self.file
|
|
endif
|
|
endfunction
|
|
|
|
|
|
function! s:Frame._unset_sign() dict
|
|
if has("signs")
|
|
exe ":sign unplace " . self.sign_id
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" *** Frame class (end)
|
|
|
|
|
|
|
|
" *** Server class (start)
|
|
|
|
let s:Server = {}
|
|
|
|
" ** Public methods
|
|
|
|
" Constructor of new server. Just inits it, not runs
|
|
function! s:Server.new(hostname, rdebug_port, debugger_port, runtime_dir, tmp_file, output_file) dict
|
|
let var = copy(self)
|
|
let var.hostname = a:hostname
|
|
let var.rdebug_port = a:rdebug_port
|
|
let var.debugger_port = a:debugger_port
|
|
let var.runtime_dir = a:runtime_dir
|
|
let var.tmp_file = a:tmp_file
|
|
let var.output_file = a:output_file
|
|
return var
|
|
endfunction
|
|
|
|
|
|
" Start the server. It will kill any listeners on given ports before.
|
|
function! s:Server.start(script) dict
|
|
call self._stop_server(self.rdebug_port)
|
|
call self._stop_server(self.debugger_port)
|
|
" Remove leading and trailing quotes
|
|
let script_name = substitute(a:script, "\\(^['\"]\\|['\"]$\\)", '', 'g')
|
|
let rdebug = 'rdebug-ide -p ' . self.rdebug_port . ' -- ' . script_name
|
|
let os = has("win32") || has("win64") ? 'win' : 'posix'
|
|
" Example - ruby ~/.vim/bin/ruby_debugger.rb 39767 39768 vim VIM /home/anton/.vim/tmp/ruby_debugger posix
|
|
let debugger_parameters = ' ' . self.hostname . ' ' . self.rdebug_port . ' ' . self.debugger_port . ' ' . g:ruby_debugger_progname . ' ' . v:servername . ' "' . self.tmp_file . '" ' . os
|
|
|
|
" Start in background
|
|
if has("win32") || has("win64")
|
|
silent exe '! start ' . rdebug
|
|
let debugger = 'ruby "' . expand(self.runtime_dir . "/bin/ruby_debugger.rb") . '"' . debugger_parameters
|
|
silent exe '! start ' . debugger
|
|
else
|
|
call system(rdebug . ' > ' . self.output_file . ' 2>&1 &')
|
|
let debugger = 'ruby ' . expand(self.runtime_dir . "/bin/ruby_debugger.rb") . debugger_parameters
|
|
call system(debugger. ' &')
|
|
endif
|
|
|
|
" Set PIDs of processes
|
|
let self.rdebug_pid = self._get_pid(self.rdebug_port, 1)
|
|
let self.debugger_pid = self._get_pid(self.debugger_port, 1)
|
|
|
|
call g:RubyDebugger.logger.put("Start debugger")
|
|
endfunction
|
|
|
|
|
|
" Kill servers and empty PIDs
|
|
function! s:Server.stop() dict
|
|
call self._kill_process(self.rdebug_pid)
|
|
call self._kill_process(self.debugger_pid)
|
|
let self.rdebug_pid = ""
|
|
let self.debugger_pid = ""
|
|
endfunction
|
|
|
|
|
|
" Return 1 if processes with set PID exist.
|
|
function! s:Server.is_running() dict
|
|
return (self._get_pid(self.rdebug_port, 0) =~ '^\d\+$') && (self._get_pid(self.debugger_port, 0) =~ '^\d\+$')
|
|
endfunction
|
|
|
|
|
|
" ** Private methods
|
|
|
|
|
|
" Get PID of process, that listens given port on given host. If must_get_pid
|
|
" parameter is true, it will try to get PID for 20 seconds.
|
|
function! s:Server._get_pid(port, must_get_pid)
|
|
let attempt = 0
|
|
let pid = self._get_pid_attempt(a:port)
|
|
while a:must_get_pid && pid == "" && attempt < 2000
|
|
sleep 10m
|
|
let attempt += 1
|
|
let pid = self._get_pid_attempt(a:port)
|
|
endwhile
|
|
return pid
|
|
endfunction
|
|
|
|
|
|
" Just try to get PID of process and return empty string if it was
|
|
" unsuccessful
|
|
function! s:Server._get_pid_attempt(port)
|
|
if has("win32") || has("win64")
|
|
let netstat = system("netstat -anop tcp")
|
|
let pid_match = matchlist(netstat, ':' . a:port . '\s.\{-}LISTENING\s\+\(\d\+\)')
|
|
let pid = len(pid_match) > 0 ? pid_match[1] : ""
|
|
elseif executable('lsof')
|
|
let pid = system("lsof -i tcp:" . a:port . " | grep LISTEN | awk '{print $2}'")
|
|
let pid = substitute(pid, '\n', '', '')
|
|
else
|
|
let pid = ""
|
|
endif
|
|
return pid
|
|
endfunction
|
|
|
|
|
|
" Kill listener of given host/port
|
|
function! s:Server._stop_server(port) dict
|
|
let pid = self._get_pid(a:port, 0)
|
|
if pid =~ '^\d\+$'
|
|
call self._kill_process(pid)
|
|
endif
|
|
endfunction
|
|
|
|
|
|
" Kill process with given PID
|
|
function! s:Server._kill_process(pid) dict
|
|
echo "Killing server with pid " . a:pid
|
|
call system("ruby -e 'Process.kill(9," . a:pid . ")'")
|
|
sleep 100m
|
|
call self._log("Killed server with pid: " . a:pid)
|
|
endfunction
|
|
|
|
|
|
function! s:Server._log(string) dict
|
|
call g:RubyDebugger.logger.put(a:string)
|
|
endfunction
|
|
|
|
|
|
" *** Server class (end)
|
|
|
|
|
|
|
|
" *** Creating instances (start)
|
|
|
|
if !exists("g:ruby_debugger_fast_sender")
|
|
let g:ruby_debugger_fast_sender = 0
|
|
endif
|
|
" This variable allows to use built-in Ruby (see ':help ruby' and s:send_message_to_debugger function)
|
|
if !exists("g:ruby_debugger_builtin_sender")
|
|
if has("ruby")
|
|
let g:ruby_debugger_builtin_sender = 1
|
|
else
|
|
let g:ruby_debugger_builtin_sender = 0
|
|
endif
|
|
endif
|
|
if !exists("g:ruby_debugger_spec_path")
|
|
let g:ruby_debugger_spec_path = '/usr/bin/spec'
|
|
endif
|
|
if !exists("g:ruby_debugger_cucumber_path")
|
|
let g:ruby_debugger_cucumber_path = '/usr/bin/cucumber'
|
|
endif
|
|
if !exists("g:ruby_debugger_progname")
|
|
let g:ruby_debugger_progname = v:progname
|
|
endif
|
|
|
|
" Creating windows
|
|
let s:variables_window = s:WindowVariables.new("variables", "Variables_Window")
|
|
let s:breakpoints_window = s:WindowBreakpoints.new("breakpoints", "Breakpoints_Window")
|
|
let s:frames_window = s:WindowFrames.new("frames", "Backtrace_Window")
|
|
|
|
" Init logger. The plugin logs all its actions. If you have some troubles,
|
|
" this file can help
|
|
let s:logger_file = s:runtime_dir . '/tmp/ruby_debugger_log'
|
|
let RubyDebugger.logger = s:Logger.new(s:logger_file)
|
|
let s:variables_window.logger = RubyDebugger.logger
|
|
let s:breakpoints_window.logger = RubyDebugger.logger
|
|
let s:frames_window.logger = RubyDebugger.logger
|
|
|
|
" *** Creating instances (end)
|
|
|