From b6f7f8c97607d6c781b540e3556445dd4c318772 Mon Sep 17 00:00:00 2001 From: Yegappan Lakshmanan Date: Fri, 4 Feb 2022 18:32:16 -0800 Subject: [PATCH] Add suppport for expanding and shrinking the visual selection --- README.md | 8 ++- autoload/codeaction.vim | 2 + autoload/diag.vim | 2 + autoload/handlers.vim | 19 +++---- autoload/lsp.vim | 18 +++++-- autoload/lspserver.vim | 45 ++++++++++++++-- autoload/selection.vim | 116 ++++++++++++++++++++++++++++++++++++++++ doc/lsp.txt | 26 +++++++-- plugin/lsp.vim | 18 ++++--- test/unit_tests.vim | 69 ++++++++++++++++++++---- 10 files changed, 281 insertions(+), 42 deletions(-) create mode 100644 autoload/selection.vim diff --git a/README.md b/README.md index 08631d5..0f1614a 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,10 @@ Command|Description :LspGotoDeclaration|Go to the declaration of the keyword under cursor :LspGotoTypeDef|Go to the type definition of the keyword under cursor :LspGotoImpl|Go to the implementation of the keyword under cursor +:LspPeekDefinition|Open the definition of the symbol under cursor in the preview window. +:LspPeekDeclaration|Open the declaration of the symbol under cursor in the preview window. +:LspPeekTypeDef|Open the type definition of the symbol under cursor in the preview window. +:LspPeekImpl|Open the implementation of the symbol under cursor in the preview window. :LspShowSignature|Display the signature of the keyword under cursor :LspDiagShow|Display the diagnostics messages from the LSP server for the current buffer in a new location list. :LspDiagFirst|Display the first diagnostic message for the current buffer @@ -94,6 +98,7 @@ Command|Description :LspDiagPrev|Display the previous diagnostic message before the current line :LspDiagCurrent|Display the diagnostic message for the current line :LspShowReferences|Display the list of references to the keyword under cursor in a new location list. +:LspPeekReferences|Display the list of references to the keyword under cursor in a location list associated with the preview window. :LspHighlight|Highlight all the matches for the keyword under cursor :LspHighlightClear|Clear all the matches highlighted by :LspHighlight :LspOutline|Show the list of symbols defined in the current file in a separate window. @@ -104,7 +109,8 @@ Command|Description :LspRename|Rename the current symbol :LspCodeAction|Apply the code action supplied by the LSP server to the diagnostic in the current line. :LspSymbolSearch|Perform a workspace wide search for a symbol -:LspSelectionRange|Visually select the current symbol range +:LspSelectionExpand|Expand the current symbol range visual selection +:LspSelectionShrink|Shrink the current symbol range visual selection :LspFold|Fold the current file :LspWorkspaceAddFolder `{folder}`| Add a folder to the workspace :LspWorkspaceRemoveFolder `{folder}`|Remove a folder from the workspace diff --git a/autoload/codeaction.vim b/autoload/codeaction.vim index 51b2355..e7e838f 100644 --- a/autoload/codeaction.vim +++ b/autoload/codeaction.vim @@ -1,5 +1,7 @@ vim9script +# Functions related to handling LSP code actions to fix diagnostics. + var util = {} var textedit = {} diff --git a/autoload/diag.vim b/autoload/diag.vim index 9c984e0..e5a5b00 100644 --- a/autoload/diag.vim +++ b/autoload/diag.vim @@ -1,5 +1,7 @@ vim9script +# Functions related to handling LSP diagnostics. + var opt = {} var util = {} diff --git a/autoload/handlers.vim b/autoload/handlers.vim index 85a067b..cbb4512 100644 --- a/autoload/handlers.vim +++ b/autoload/handlers.vim @@ -12,6 +12,7 @@ var textedit = {} var symbol = {} var codeaction = {} var callhier = {} +var selection = {} if has('patch-8.2.4019') import './lspoptions.vim' as opt_import @@ -22,6 +23,7 @@ if has('patch-8.2.4019') import './symbol.vim' as symbol_import import './codeaction.vim' as codeaction_import import './callhierarchy.vim' as callhierarchy_import + import './selection.vim' as selection_import opt.lspOptions = opt_import.lspOptions util.WarnMsg = util_import.WarnMsg @@ -38,6 +40,7 @@ if has('patch-8.2.4019') codeaction.ApplyCodeAction = codeaction_import.ApplyCodeAction callhier.IncomingCalls = callhierarchy_import.IncomingCalls callhier.OutgoingCalls = callhierarchy_import.OutgoingCalls + selection.SelectionStart = selection_import.SelectionStart else import lspOptions from './lspoptions.vim' import {WarnMsg, @@ -51,6 +54,7 @@ else import {ShowReferences, GotoSymbol} from './symbol.vim' import ApplyCodeAction from './codeaction.vim' import {IncomingCalls, OutgoingCalls} from './callhierarchy.vim' + import {SelectionStart} from './selection.vim' opt.lspOptions = lspOptions util.WarnMsg = WarnMsg @@ -67,6 +71,7 @@ else codeaction.ApplyCodeAction = ApplyCodeAction callhier.IncomingCalls = IncomingCalls callhier.OutgoingCalls = OutgoingCalls + selection.SelectionStart = SelectionStart endif # process the 'initialize' method reply from the LSP server @@ -567,19 +572,7 @@ enddef # Reply: 'textDocument/selectionRange' # Result: SelectionRange[] | null def s:processSelectionRangeReply(lspserver: dict, req: dict, reply: dict) - if reply.result->empty() - return - endif - - var r: dict> = reply.result[0].range - var bnr: number = bufnr() - var start_col: number = util.GetLineByteFromPos(bnr, r.start) + 1 - var end_col: number = util.GetLineByteFromPos(bnr, r.end) - - :normal! v"_y - setcharpos("'<", [0, r.start.line + 1, start_col, 0]) - setcharpos("'>", [0, r.end.line + 1, end_col, 0]) - :normal! gv + selection.SelectionStart(lspserver, reply.result) enddef # Reply: 'textDocument/foldingRange' diff --git a/autoload/lsp.vim b/autoload/lsp.vim index 822c465..e7411ac 100644 --- a/autoload/lsp.vim +++ b/autoload/lsp.vim @@ -879,16 +879,24 @@ export def RemoveWorkspaceFolder(dirArg: string) lspserver.removeWorkspaceFolder(dirName) enddef -# visually select a range of positions around the current cursor. -export def SelectionRange() +# expand the previous selection or start a new selection +export def SelectionExpand() var lspserver: dict = s:curbufGetServerChecked() if lspserver->empty() return endif - var fname: string = @% - # TODO: Also support passing a range - lspserver.selectionRange(fname) + lspserver.selectionExpand() +enddef + +# shrink the previous selection or start a new selection +export def SelectionShrink() + var lspserver: dict = s:curbufGetServerChecked() + if lspserver->empty() + return + endif + + lspserver.selectionShrink() enddef # fold the entire document diff --git a/autoload/lspserver.vim b/autoload/lspserver.vim index c0ee844..e59b40a 100644 --- a/autoload/lspserver.vim +++ b/autoload/lspserver.vim @@ -7,11 +7,14 @@ vim9script var handlers = {} var diag = {} var util = {} +var selection = {} if has('patch-8.2.4019') import './handlers.vim' as handlers_import import './util.vim' as util_import import './diag.vim' as diag_import + import './selection.vim' as selection_import + handlers.ProcessReply = handlers_import.ProcessReply handlers.ProcessNotif = handlers_import.ProcessNotif handlers.ProcessRequest = handlers_import.ProcessRequest @@ -23,6 +26,7 @@ if has('patch-8.2.4019') util.LspFileToUri = util_import.LspFileToUri util.PushCursorToTagStack = util_import.PushCursorToTagStack diag.GetDiagByLine = diag_import.GetDiagByLine + selection.SelectionModify = selection_import.SelectionModify else import {ProcessReply, ProcessNotif, @@ -35,6 +39,8 @@ else LspBufnrToUri, LspFileToUri, PushCursorToTagStack} from './util.vim' + import {SelectionModify} from './selection.vim' + handlers.ProcessReply = ProcessReply handlers.ProcessNotif = ProcessNotif handlers.ProcessRequest = ProcessRequest @@ -46,6 +52,7 @@ else util.LspFileToUri = LspFileToUri util.PushCursorToTagStack = PushCursorToTagStack diag.GetDiagByLine = GetDiagByLine + selection.SelectionModify = SelectionModify endif # LSP server standard output handler @@ -63,7 +70,7 @@ enddef # LSP server exit callback def s:exit_cb(lspserver: dict, job: job, status: number): void util.WarnMsg("LSP server exited with status " .. status) - lspserver.job = v:none + lspserver.job = v:null lspserver.running = false lspserver.ready = false lspserver.requests = {} @@ -201,7 +208,7 @@ def s:stopServer(lspserver: dict): number lspserver.exitServer() lspserver.job->job_stop() - lspserver.job = v:none + lspserver.job = v:null lspserver.running = false lspserver.ready = false lspserver.requests = {} @@ -862,11 +869,40 @@ def s:selectionRange(lspserver: dict, fname: string) return endif + # clear the previous selection reply + lspserver.selection = {} + var req = lspserver.createRequest('textDocument/selectionRange') # interface SelectionRangeParams # interface TextDocumentIdentifier req.params->extend({textDocument: {uri: util.LspFileToUri(fname)}, positions: [s:getLspPosition()]}) lspserver.sendMessage(req) + + s:waitForReponse(lspserver, req) +enddef + +# Expand the previous selection or start a new one +def s:selectionExpand(lspserver: dict) + # Check whether LSP server supports selection ranges + if !lspserver.caps->has_key('selectionRangeProvider') + || !lspserver.caps.selectionRangeProvider + util.ErrMsg("Error: LSP server does not support selection ranges") + return + endif + + selection.SelectionModify(lspserver, true) +enddef + +# Shrink the previous selection or start a new one +def s:selectionShrink(lspserver: dict) + # Check whether LSP server supports selection ranges + if !lspserver.caps->has_key('selectionRangeProvider') + || !lspserver.caps.selectionRangeProvider + util.ErrMsg("Error: LSP server does not support selection ranges") + return + endif + + selection.SelectionModify(lspserver, false) enddef # fold the entire document @@ -922,7 +958,8 @@ export def NewLspServer(path: string, args: list): dict workspaceSymbolPopup: 0, workspaceSymbolQuery: '', peekSymbol: false, - callHierarchyType: '' + callHierarchyType: '', + selection: {} } # Add the LSP server functions lspserver->extend({ @@ -968,6 +1005,8 @@ export def NewLspServer(path: string, args: list): dict addWorkspaceFolder: function('s:addWorkspaceFolder', [lspserver]), removeWorkspaceFolder: function('s:removeWorkspaceFolder', [lspserver]), selectionRange: function('s:selectionRange', [lspserver]), + selectionExpand: function('s:selectionExpand', [lspserver]), + selectionShrink: function('s:selectionShrink', [lspserver]), foldRange: function('s:foldRange', [lspserver]), executeCommand: function('s:executeCommand', [lspserver]), showCapabilities: function('s:showCapabilities', [lspserver]) diff --git a/autoload/selection.vim b/autoload/selection.vim new file mode 100644 index 0000000..1d4f090 --- /dev/null +++ b/autoload/selection.vim @@ -0,0 +1,116 @@ +vim9script + +# Functions related to handling LSP range selection. + +var util = {} +if has('patch-8.2.4019') + import './util.vim' as util_import + + util.GetLineByteFromPos = util_import.GetLineByteFromPos +else + import {GetLineByteFromPos} from './util.vim' + + util.GetLineByteFromPos = GetLineByteFromPos +endif + +# Visually (character-wise) select the text in a range +def s:selectText(bnr: number, range: dict>) + var start_col: number = util.GetLineByteFromPos(bnr, range.start) + 1 + var end_col: number = util.GetLineByteFromPos(bnr, range.end) + + :normal! v"_y + setcharpos("'<", [0, range.start.line + 1, start_col, 0]) + setcharpos("'>", [0, range.end.line + 1, end_col, 0]) + :normal! gv +enddef + +# Process the range selection reply from LSP server and start a new selection +export def SelectionStart(lspserver: dict, sel: list>) + if sel->empty() + return + endif + + var bnr: number = bufnr() + + # save the reply for expanding or shrinking the selected text. + lspserver.selection = {bnr: bnr, selRange: sel[0], index: 0} + + s:selectText(bnr, sel[0].range) +enddef + +# Locate the range in the LSP reply at a specified level +def s:getSelRangeAtLevel(selRange: dict, level: number): dict + var r: dict = selRange + var idx: number = 0 + + while idx != level + if !r->has_key('parent') + break + endif + r = r.parent + idx += 1 + endwhile + + return r +enddef + +# Returns true if the current visual selection matches a range in the +# selection reply from LSP. +def s:selectionFromLSP(range: dict, startpos: list, endpos: list): bool + return startpos[1] == range.start.line + 1 + && endpos[1] == range.end.line + 1 + && startpos[2] == range.start.character + 1 + && endpos[2] == range.end.character +enddef + +g:Logs = [] + +# Expand or Shrink the current selection or start a new one. +export def SelectionModify(lspserver: dict, expand: bool) + var fname: string = @% + var bnr: number = bufnr() + + add(g:Logs, 'SelectionModify: expand = ' .. expand->string()) + + if mode() == 'v' && !lspserver.selection->empty() + && lspserver.selection.bnr == bnr + && !lspserver.selection->empty() + # Already in characterwise visual mode and the previous LSP selection + # reply for this buffer is available. Modify the current selection. + + var selRange: dict = lspserver.selection.selRange + var startpos: list = getcharpos("v") + var endpos: list = getcharpos(".") + var idx: number = lspserver.selection.index + + # Locate the range in the LSP reply for the current selection + selRange = s:getSelRangeAtLevel(selRange, lspserver.selection.index) + + # If the current selection is present in the LSP reply, then modify the + # selection + if s:selectionFromLSP(selRange.range, startpos, endpos) + if expand + # expand the selection + if selRange->has_key('parent') + selRange = selRange.parent + lspserver.selection.index = idx + 1 + endif + else + # shrink the selection + if idx > 0 + idx -= 1 + selRange = s:getSelRangeAtLevel(lspserver.selection.selRange, idx) + lspserver.selection.index = idx + endif + endif + + s:selectText(bnr, selRange.range) + return + endif + endif + + # Start a new selection + lspserver.selectionRange(fname) +enddef + +# vim: shiftwidth=2 softtabstop=2 diff --git a/doc/lsp.txt b/doc/lsp.txt index 63e6e99..411ee85 100644 --- a/doc/lsp.txt +++ b/doc/lsp.txt @@ -2,7 +2,7 @@ Author: Yegappan Lakshmanan (yegappan AT yahoo DOT com) For Vim version 8.2.2342 and above -Last change: Feb 2, 2022 +Last change: Feb 4, 2022 ============================================================================== *lsp-license* @@ -115,7 +115,8 @@ The following commands are provided: :LspCodeAction Apply the code action supplied by the LSP server to the diagnostic in the current line. :LspSymbolSearch Perform a workspace wide search for a symbol -:LspSelectionRange Visually select the current symbol range +:LspSelectionExpand Expand the current symbol range visual selection +:LspSelectionShrink Shrink the current symbol range visual selection :LspFold Fold the current file :LspWorkspaceAddFolder {folder} Add a folder to the workspace @@ -453,8 +454,25 @@ diagnostic messages, you can add the following line to your .vimrc file: < Default is false - *:LspSelectionRange* -:LspSelectionRange Visually select the current symbol range. + *:LspSelectionExpand* +:LspSelectionExpand Expand the current symbol range visual selection. It + is useful to create a visual map to use this command. + Example: > + + xnoremap le LspSelectionExpand +< + With the above map, you can press "le" in visual mode + successively to expand the current visual region. + + *:LspSelectionShrink* +:LspSelectionShrink Shrink the current symbol range visual selection. It + is useful to create a visual map to use this command. + Example: > + + xnoremap ls LspSelectionShrink +< + With the above map, you can press "ls" in visual mode + successively to shrink the current visual region. *:LspFold* :LspFold Fold the current file. diff --git a/plugin/lsp.vim b/plugin/lsp.vim index c9ef725..4da8915 100644 --- a/plugin/lsp.vim +++ b/plugin/lsp.vim @@ -48,7 +48,8 @@ if has('patch-8.2.4257') lspf.codeAction = lsp.CodeAction lspf.symbolSearch = lsp.SymbolSearch lspf.hover = lsp.Hover - lspf.selectionRange = lsp.SelectionRange + lspf.selectionExpand = lsp.SelectionExpand + lspf.selectionShrink = lsp.SelectionShrink lspf.foldDocument = lsp.FoldDocument lspf.listWorkspaceFolders = lsp.ListWorkspaceFolders lspf.addWorkspaceFolder = lsp.AddWorkspaceFolder @@ -93,7 +94,8 @@ elseif has('patch-8.2.4019') lspf.codeAction = lsp_import.CodeAction lspf.symbolSearch = lsp_import.SymbolSearch lspf.hover = lsp_import.Hover - lspf.selectionRange = lsp_import.SelectionRange + lspf.selectionExpand = lsp_import.SelectionExpand + lspf.selectionShrink = lsp_import.SelectionShrink lspf.foldDocument = lsp_import.FoldDocument lspf.listWorkspaceFolders = lsp_import.ListWorkspaceFolders lspf.addWorkspaceFolder = lsp_import.AddWorkspaceFolder @@ -127,7 +129,8 @@ else CodeAction, SymbolSearch, Hover, - SelectionRange, + SelectionExpand, + SelectionShrink, FoldDocument, ListWorkspaceFolders, AddWorkspaceFolder, @@ -169,7 +172,8 @@ else lspf.codeAction = CodeAction lspf.symbolSearch = SymbolSearch lspf.hover = Hover - lspf.selectionRange = SelectionRange + lspf.selectionExpand = SelectionExpand + lspf.selectionShrink = SelectionShrink lspf.foldDocument = FoldDocument lspf.listWorkspaceFolders = ListWorkspaceFolders lspf.addWorkspaceFolder = AddWorkspaceFolder @@ -216,7 +220,8 @@ var Trename = s:lspf.rename var TcodeAction = s:lspf.codeAction var TsymbolSearch = s:lspf.symbolSearch var Thover = s:lspf.hover -var TselectionRange = s:lspf.selectionRange +var TselectionExpand = s:lspf.selectionExpand +var TselectionShrink = s:lspf.selectionShrink var TfoldDocument = s:lspf.foldDocument var TlistWorkspaceFolders = s:lspf.listWorkspaceFolders var TaddWorkspaceFolder = s:lspf.addWorkspaceFolder @@ -269,7 +274,8 @@ command! -nargs=0 -bar LspRename call Trename() command! -nargs=0 -bar LspCodeAction call TcodeAction() command! -nargs=? -bar LspSymbolSearch call TsymbolSearch() command! -nargs=0 -bar LspHover call Thover() -command! -nargs=0 -bar LspSelectionRange call TselectionRange() +command! -nargs=0 -bar LspSelectionExpand call TselectionExpand() +command! -nargs=0 -bar LspSelectionShrink call TselectionShrink() command! -nargs=0 -bar LspFold call TfoldDocument() command! -nargs=0 -bar LspWorkspaceListFolders call TlistWorkspaceFolders() command! -nargs=1 -bar -complete=dir LspWorkspaceAddFolder call TaddWorkspaceFolder() diff --git a/test/unit_tests.vim b/test/unit_tests.vim index dc9270c..5d4dc9f 100644 --- a/test/unit_tests.vim +++ b/test/unit_tests.vim @@ -397,8 +397,8 @@ def Test_LspRename() :%bw! enddef -# Test for :LspSelectionRange -def Test_LspSelectionRange() +# Test for :LspSelectionExpand and :LspSelectionShrink +def Test_LspSelection() silent! edit Xtest.c sleep 500m var lines: list =<< trim END @@ -413,37 +413,86 @@ def Test_LspSelectionRange() END setline(1, lines) sleep 1 - # start a block-wise visual mode, LspSelectionRange should change this to + # start a block-wise visual mode, LspSelectionExpand should change this to # a characterwise visual mode. exe "normal! 1G\G\"_y" cursor(2, 1) redraw! - :LspSelectionRange - sleep 1 + :LspSelectionExpand redraw! normal! y assert_equal('v', visualmode()) assert_equal([2, 8], [line("'<"), line("'>")]) - # start a linewise visual mode, LspSelectionRange should change this to + # start a linewise visual mode, LspSelectionExpand should change this to # a characterwise visual mode. exe "normal! 3GViB\"_y" cursor(4, 29) redraw! - :LspSelectionRange - sleep 1 + :LspSelectionExpand redraw! normal! y assert_equal('v', visualmode()) assert_equal([4, 5, 6, 5], [line("'<"), col("'<"), line("'>"), col("'>")]) + + # Expand the visual selection + xnoremap le LspSelectionExpand + xnoremap ls LspSelectionShrink + cursor(5, 8) + normal vley + assert_equal([5, 8, 5, 12], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vleley + assert_equal([5, 8, 5, 14], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vleleley + assert_equal([4, 30, 6, 5], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vleleleley + assert_equal([4, 5, 6, 5], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vleleleleley + assert_equal([2, 1, 8, 1], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vleleleleleley + assert_equal([1, 1, 8, 1], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vleleleleleleley + assert_equal([1, 1, 8, 1], [line("'<"), col("'<"), line("'>"), col("'>")]) + + # Shrink the visual selection + cursor(5, 8) + normal vlsy + assert_equal([5, 8, 5, 12], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vlelsy + assert_equal([5, 8, 5, 12], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vlelelsy + assert_equal([5, 8, 5, 12], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vlelelelsy + assert_equal([5, 8, 5, 14], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vlelelelelsy + assert_equal([4, 30, 6, 5], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vlelelelelelsy + assert_equal([4, 5, 6, 5], [line("'<"), col("'<"), line("'>"), col("'>")]) + cursor(5, 8) + normal vlelelelelelelsy + assert_equal([2, 1, 8, 1], [line("'<"), col("'<"), line("'>"), col("'>")]) + + xunmap le + xunmap ls bw! # empty file - assert_equal('', execute('LspSelectionRange')) + assert_equal('', execute('LspSelectionExpand')) # file without an LSP server edit a.b assert_equal(['Error: LSP server for "a.b" is not found'], - execute('LspSelectionRange')->split("\n")) + execute('LspSelectionExpand')->split("\n")) :%bw! enddef -- 2.48.1