diff --git a/README.md b/README.md index c201f59..33d4eaf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # vim-jdb +This is a fork of Dica-Developer/vim-jdb so checkout the main branch as well. + Its a JAVA debugger frontend plugin for VIM. It allows to debug a JAVA program via the JDB debugger. It allows remote debugging via attach parameter. It marks by vim-jdb setted breakpoints and shows the current file and line the debugger stays in. @@ -18,6 +20,10 @@ It requires VIM >= 8.0 and VIM compiled with `channel`, `signs` and `job` suppor 8. use the command `:JDBStepOver` to execute to the next line 9. with `:JDBCommand` you can send any JDB command to the JDB JAVA process, e.g. you want to see all locals do `:JDBCommands locals` 10. with `:JDBContinue` you can resume the execution until the next breakpoint is hits +11. with `:JDBDebugProcess` will (currently) pick the function name you are currently in if it is annontated with "@Test". It will then construct the the path to the class from the class name and package name in the file. + finally it will start jdb like this e.g.: jdb -Dtest.single=functionmane org.junit.runner.JUnitCore yourpackage.classname + for this to work properly you need to set the env var CLASSPATH with your projects dependencies. For me I use gradle for dev so have a task named "classpath" and can do this from with in gradle before starting. + :let $CLASSPATH=system("gradle -q classpath") . ":build/classes/main:build/classes/test" ## Commands |Command|Description| @@ -32,8 +38,11 @@ It requires VIM >= 8.0 and VIM compiled with `channel`, `signs` and `job` suppor |JDBStepUp|steps a level up in the stack| |JDBStepI|steps to the next instruction| |JDBCommand|send any JDB command to the application under debug| +|JDBDebugProcess|While in a JUnit test this will start a debug process using junit -Dtest.single| ## Global variables To specify the JDB command to use you can overwrite the following variable `g:vimjdb_jdb_command`. The default is `jdb`. +For starting debug process for Unit tests you can override the following variables. +g:vimjdb_unit_test_class by default it is set tp 'org.junit.runnint.JUnitCore' diff --git a/plugin/vim-jdb.vim b/plugin/vim-jdb.vim index a890291..66eb83f 100644 --- a/plugin/vim-jdb.vim +++ b/plugin/vim-jdb.vim @@ -26,10 +26,12 @@ if !has('signs') finish endif +command! -nargs=? JDBDebugProcess call s:createDebugProcess() command! -nargs=? JDBAttach call s:attach() +command! JDBDebugUnit call s:startUnitTest() command! JDBDetach call s:detach() -command! JDBBreakpointOnLine call s:breakpointOnLine(expand('%:~:.'), line('.')) -command! JDBClearBreakpointOnLine call s:clearBreakpointOnLine(expand('%:~:.'), line('.')) +command! JDBBreakpointOnLine call s:setBreakpoint() +command! JDBClearBreakpointOnLine call s:removeBreakpoint() command! JDBContinue call s:continue() command! JDBStepOver call s:stepOver() command! JDBStepIn call s:stepIn() @@ -49,6 +51,9 @@ sign define currentline text=-> texthl=Search let s:job = '' let s:channel = '' +let s:running = 0 +let s:org_win_id = 0 +let s:currentfile = '' function! s:hash(name, linenumber) let l:result = 1 @@ -56,7 +61,7 @@ function! s:hash(name, linenumber) let l:result = (l:result * 2) + char2nr(c) endfor let l:result = (l:result * 2) + a:linenumber - return l:result + return strpart(l:result, 4) endfunction function! s:getClassNameFromFile(filename) @@ -72,36 +77,54 @@ endfunction function! JdbOutHandler(channel, msg) "TODO make debug logging out of it - "echom a:msg + echom a:msg let l:breakpoint = '' - if -1 < stridx(a:msg, 'Breakpoint hit:') + if -1 < stridx(a:msg, 'Breakpoint hit: "thread') echom "breakpoint hit" let l:breakpoint = split(a:msg, ',') let l:filename = l:breakpoint[1] - let l:filename = substitute(l:filename, '\.\a*()$', '', '') + let l:filename = substitute(l:filename, '\.\w*()$', '', '') let l:filename = substitute(l:filename, ' ', '', 'g') - let l:filename = join(split(l:filename, '\.'), '/') + let l:filenamefrag= split(l:filename, '\.') + let l:filename = findfile(filenamefrag[-1]) + echom 'found file ' . l:filename let l:linenumber = substitute(l:breakpoint[2], ',\|\.\| \|bci=\d*\|line=', '', 'g') - " TODO only open when current buffer is not the file to open - exe 'e +%foldopen! **/'. l:filename .'.java' - exe l:linenumber - exe 'sign unplace 2' - exe 'sign place 2 line='. l:linenumber .' name=currentline file='. expand("%:p") + " only open when current buffer is not the file to open + if l:filename != s:currentfile + if filereadable(l:filename) + exe 'e +%foldopen! **/'. l:filename + let s:currentfile = l:filename + exe l:linenumber + exe 'sign unplace 2' + exe 'sign place 2 line='. l:linenumber .' name=currentline file='. expand("%:p") + else + echom 'tried to open file ' . l:filename + endif + endif endif - if -1 < stridx(a:msg, 'Step completed:') + if -1 < stridx(a:msg, 'Step completed: "thread') " TODO handle ClassName$3.get() echom "Step completed" let l:breakpoint = split(a:msg, ',') let l:filename = l:breakpoint[1] - let l:filename = substitute(l:filename, '\.\a*()$', '', '') + let l:filename = substitute(l:filename, '\.\w*()$', '', '') let l:filename = substitute(l:filename, ' ', '', 'g') - let l:filename = join(split(l:filename, '\.'), '/') + let l:filenamefrag= split(l:filename, '\.') + let l:filename = findfile(filenamefrag[-1]) + echom 'found file ' . l:filename let l:linenumber = substitute(l:breakpoint[2], ',\|\.\| \|bci=\d*\|line=', '', 'g') - " TODO only open when current buffer is not the file to open - exe 'e +%foldopen! **/'. l:filename .'.java' - exe l:linenumber - exe 'sign unplace 2' - exe 'sign place 2 line='. l:linenumber .' name=currentline file='. expand("%:p") + " only open when current buffer is not the file to open + " if l:filename != l:currentfile + if filereadable(l:filename) + exe 'e +%foldopen! **/'. l:filename + let s:currentfile = l:filename + exe l:linenumber + exe 'sign unplace 2' + exe 'sign place 2 line='. l:linenumber .' name=currentline file='. expand("%:p") + else + echom 'tried to open file ' . l:filename + endif + " endif endif if -1 < stridx(a:msg, 'Set breakpoint ') || -1 < stridx(a:msg, 'Set deferred breakpoint ') echom "Set breakpoint" @@ -121,12 +144,64 @@ function! JdbOutHandler(channel, msg) echom "Continue" exe 'sign unplace 2' endif + if -1 < stridx(a:msg, 'The application exited') + exe 'sign unplace 2' + let s:channel = '' + let s:job = '' + let s:running = 0 + let win = bufwinnr('_JDB_SHELL_') + if win != -1 + exe win . 'wincmd w' + exe 'close' + endif + endif endfunction function! JdbErrHandler(channel, msg) + let s:running = 0 echoe 'Error on JDB communication: '. a:msg endfunction +let s:signCounter = 10 +let s:signtype = 'breakpoint' + +function! s:applyBreakPoints(channel, name) + let l:points = split(execute('sign place'), '\n') + let l:fileName = '' + for line in l:points + " Start of new file, listing all signs for this + if -1 < stridx(line, 'Signs for') + let l:lineparts = split(line, ' ') + let l:fileName = l:lineparts[-1] + let l:fileName = substitute(l:fileName ,':$', '', 'g') + endif + " if sign found with given name + if -1 < stridx(line, 'name='. a:name) + let l:lineparts = split(line, ' ') + let l:lineNumber = substitute(l:lineparts[0], 'line=','','g') + let l:className = s:getClassNameFromFile(l:fileName) + echom 'breakpoint line: ' . l:fileName .':'. l:lineNumber + call ch_sendraw(a:channel, "stop at " . l:className . ":" . l:lineNumber . "\n") + endif + endfor +endfunction + +function! s:setBreakpoint() + let l:fileName = expand('%:p') + let l:lineNumber = line('.') + let l:currentBuffer = bufnr('%') + exe 'sign place ' . s:signCounter . ' line=' . l:lineNumber . ' name=' . s:signtype . ' buffer=' . l:currentBuffer + let s:signCounter = s:signCounter + 1 + if s:running == 1 + call s:breakpointOnLine(l:fileName, l:lineNumber) + endif +endfunction + +function! s:removeBreakpoint() + sign unplace + call s:clearBreakpointOnLine(expand('%:p'), line('.')) +endfunction + function! s:openWindow(name, mode, size) let win = bufwinnr(a:name) if win == -1 @@ -160,13 +235,27 @@ endfunction function! s:attach(...) if s:job == '' || job_status(s:job) != 'run' - let l:hostAndPort = get(a:, 1, 'localhost:5005') + " save current window state + let s:org_win_id = win_getid() + let l:winview = winsaveview() + + let l:arg1 = get(a:, 1, 'localhost:5005') let l:jdbCommand = get(g:, 'vimjdb_jdb_command', 'jdb') call s:openWindow('_JDB_SHELL_', '', 15) - let s:job = job_start(l:jdbCommand .' -attach '. l:hostAndPort, {"out_modifiable": 0, "out_io": "buffer", "out_name": "_JDB_SHELL_", "out_cb": "JdbOutHandler", "err_modifiable": 0, "err_io": "buffer", "err_name": "_JDB_SHELL_", "err_cb": "JdbErrHandler"}) + echom "Attaching to java process" + let s:job = job_start(l:jdbCommand .' -attach '. l:arg1, {"out_modifiable": 0, "out_io": "buffer", "out_name": "_JDB_SHELL_", "out_cb": "JdbOutHandler", "err_modifiable": 0, "err_io": "buffer", "err_name": "_JDB_SHELL_", "err_cb": "JdbErrHandler"}) let s:channel = job_getchannel(s:job) + call ch_sendraw(s:channel, "run\n") call ch_sendraw(s:channel, "monitor where\n") + + " apply breakpoint into jdb process from defined signs + call s:applyBreakPoints(s:channel, 'breakpoint') + let s:running = 1 + + " Set cursor in where you started the process, in the correct buffer + call win_gotoid(s:org_win_id) + call winrestview(l:winview) else echom 'There is already a JDB session running. Detach first before you start a new one.' endif @@ -177,6 +266,7 @@ function! s:detach() call ch_sendraw(s:channel, "exit\n") let s:channel = '' let s:job = '' + let s:running = 0 let win = bufwinnr('_JDB_SHELL_') if win != -1 exe win . 'wincmd w' @@ -186,36 +276,57 @@ function! s:detach() endfunction function! s:breakpointOnLine(fileName, lineNumber) - "TODO check if we are on a java file and fail if not - let fileName = s:getClassNameFromFile(a:fileName) - "TODO store command temporary if not already connected - call ch_sendraw(s:channel, "stop at " . fileName . ":" . a:lineNumber . "\n") + if s:channel != '' + let l:fileName = s:getClassNameFromFile(a:fileName) + call ch_sendraw(s:channel, "stop at " . l:fileName . ":" . a:lineNumber . "\n") + endif endfunction function! s:clearBreakpointOnLine(fileName, lineNumber) - "TODO check if we are on a java file and fail if not - let fileName = s:getClassNameFromFile(a:fileName) - "TODO store command temporary if not already connected - call ch_sendraw(s:channel, "clear " . fileName . ":" . a:lineNumber . "\n") + if s:channel != '' + let l:fileName = s:getClassNameFromFile(a:fileName) + call ch_sendraw(s:channel, "clear " . l:fileName . ":" . a:lineNumber . "\n") + endif endfunction +" for all driver options check if running=0 if so please start the process by sending run first function! s:continue() + if s:running == 0 + call ch_sendraw(s:channel, "run\n") + let s:running = 1 + endif call ch_sendraw(s:channel, "resume\n") endfunction function! s:stepOver() + if s:running == 0 + call ch_sendraw(s:channel, "run\n") + let s:running = 1 + endif call ch_sendraw(s:channel, "next\n") endfunction function! s:stepUp() + if s:running == 0 + call ch_sendraw(s:channel, "run\n") + let s:running = 1 + endif call ch_sendraw(s:channel, "step up\n") endfunction function! s:stepIn() + if s:running == 0 + call ch_sendraw(s:channel, "run\n") + let s:running = 1 + endif call ch_sendraw(s:channel, "step in\n") endfunction function! s:stepI() + if s:running == 0 + call ch_sendraw(s:channel, "run\n") + let s:running = 1 + endif call ch_sendraw(s:channel, "stepi\n") endfunction @@ -233,3 +344,53 @@ function! s:toggleWatchWindow() endif endfunction +function! s:createDebugProcess() + let l:running = 0 + + " save current window state + let s:org_win_id = win_getid() + let l:winview = winsaveview() + " TODO Figure out a way to properly detect if the current function is a test + " or not, if so start the debugging wih this otherwise fall back to setting + " main + " get current function name + echom 'Finding java process and starting it' + " Backwards search for @Test annotation and grab the function it defines + call search('@Test','bce') + execute "normal! wwwyt(" + let l:fnname = @" + echom 'Function name: '. l:fnname + + " get package name + execute "normal! ggwyt;" + let l:pcname = @" + + " get class name + call search("public class", 'e') + execute "normal! wyiW" + let l:clname = @" + + " Build unit test process string + let l:appArgs = '' + if l:fnname != '' + let l:appArgs .= ' -Dtest.single=' . l:fnname + endif + let l:appArgs .= ' ' . get(g:, 'vimjdb_unit_test_class', 'org.junit.runner.JUnitCore') + let l:appArgs .= ' ' . l:pcname . '.' . l:clname + + " start jdb process in a new buffer + let l:jdbCommand = get(g:, 'vimjdb_jdb_command', 'jdb') + let l:process = l:jdbCommand .' '. l:appArgs + echom 'starting job: ' . l:process + call s:openWindow('_JDB_SHELL_', '', 15) + let s:job = job_start(l:process , {"out_modifiable": 0, "out_io": "buffer", "out_name": "_JDB_SHELL_", "out_cb": "JdbOutHandler", "err_modifiable": 0, "err_io": "buffer", "err_name": "_JDB_SHELL_","err_cb": "JdbErrHandler"}) + let s:channel = job_getchannel(s:job) + + " Apply breakpoints stored in sign's into the debug process + call s:applyBreakPoints(s:channel, 'breakpoint') + + " Set cursor in where you started the process, in the correct buffer + call win_gotoid(s:org_win_id) + call winrestview(l:winview) + +endfunction