]> Sergey Matveev's repositories - vim-lsp.git/commitdiff
Move completed related functions to the completion.vim file
authorYegappan Lakshmanan <yegappan@yahoo.com>
Sun, 13 Nov 2022 02:30:35 +0000 (18:30 -0800)
committerYegappan Lakshmanan <yegappan@yahoo.com>
Sun, 13 Nov 2022 02:30:35 +0000 (18:30 -0800)
autoload/lsp/buffer.vim
autoload/lsp/completion.vim [new file with mode: 0644]
autoload/lsp/lsp.vim
autoload/lsp/lspserver.vim

index e0d18fa8a9674346682d6f0e81927d4664f262fd..207c5382856ff8b94dcb881dcbca3a6ae15f8e3c 100644 (file)
@@ -2,6 +2,8 @@ vim9script
 
 # Functions for managing the per-buffer LSP server information
 
+import './util.vim'
+
 # Buffer number to LSP server map
 var bufnrToServer: dict<dict<any>> = {}
 
@@ -29,4 +31,29 @@ export def BufHasLspServer(bnr: number): bool
   return bufnrToServer->has_key(bnr)
 enddef
 
+# Returns the LSP server for the current buffer if it is running and is ready.
+# Returns an empty dict if the server is not found or is not ready.
+export def CurbufGetServerChecked(): dict<any>
+  var fname: string = @%
+  if fname == ''
+    return {}
+  endif
+
+  var lspserver: dict<any> = CurbufGetServer()
+  if lspserver->empty()
+    util.ErrMsg($'Error: Language server for "{&filetype}" file type is not found')
+    return {}
+  endif
+  if !lspserver.running
+    util.ErrMsg($'Error: Language server for "{&filetype}" file type is not running')
+    return {}
+  endif
+  if !lspserver.ready
+    util.ErrMsg($'Error: Language server for "{&filetype}" file type is not ready')
+    return {}
+  endif
+
+  return lspserver
+enddef
+
 # vim: tabstop=8 shiftwidth=2 softtabstop=2
diff --git a/autoload/lsp/completion.vim b/autoload/lsp/completion.vim
new file mode 100644 (file)
index 0000000..1c5e702
--- /dev/null
@@ -0,0 +1,448 @@
+vim9script
+
+# LSP completion related functions
+
+import './util.vim'
+import './buffer.vim' as buf
+import './options.vim' as opt
+import './textedit.vim'
+
+# per-filetype omni-completion enabled/disabled table
+var ftypeOmniCtrlMap: dict<bool> = {}
+
+# Returns true if omni-completion is enabled for filetype 'ftype'.
+# Otherwise, returns false.
+def LspOmniComplEnabled(ftype: string): bool
+  return ftypeOmniCtrlMap->get(ftype, v:false)
+enddef
+
+# Enables or disables omni-completion for filetype 'fype'
+export def OmniComplSet(ftype: string, enabled: bool)
+  ftypeOmniCtrlMap->extend({[ftype]: enabled})
+enddef
+
+# Map LSP complete item kind to a character
+def LspCompleteItemKindChar(kind: number): string
+  var kindMap: list<string> = ['',
+               't', # Text
+               'm', # Method
+               'f', # Function
+               'C', # Constructor
+               'F', # Field
+               'v', # Variable
+               'c', # Class
+               'i', # Interface
+               'M', # Module
+               'p', # Property
+               'u', # Unit
+               'V', # Value
+               'e', # Enum
+               'k', # Keyword
+               'S', # Snippet
+               'C', # Color
+               'f', # File
+               'r', # Reference
+               'F', # Folder
+               'E', # EnumMember
+               'd', # Contant
+               's', # Struct
+               'E', # Event
+               'o', # Operator
+               'T'  # TypeParameter
+       ]
+  if kind > 25
+    return ''
+  endif
+  return kindMap[kind]
+enddef
+
+# Remove all the snippet placeholders from 'str' and return the value.
+# Based on a similar function in the vim-lsp plugin.
+def MakeValidWord(str_arg: string): string
+  var str = substitute(str_arg, '\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g')
+  str = substitute(str, '\\\(.\)', '\1', 'g')
+  var valid = matchstr(str, '^[^"'' (<{\[\t\r\n]\+')
+  if empty(valid)
+    return str
+  endif
+  if valid =~# ':$'
+    return valid[: -2]
+  endif
+  return valid
+enddef
+
+# process the 'textDocument/completion' reply from the LSP server
+# Result: CompletionItem[] | CompletionList | null
+export def CompletionReply(lspserver: dict<any>, cItems: any)
+  if cItems->empty()
+    return
+  endif
+
+  var items: list<dict<any>>
+  if cItems->type() == v:t_list
+    items = cItems
+  else
+    items = cItems.items
+  endif
+
+  var completeItems: list<dict<any>> = []
+  for item in items
+    var d: dict<any> = {}
+    if item->has_key('textEdit') && item.textEdit->has_key('newText')
+      d.word = item.textEdit.newText
+    elseif item->has_key('insertText')
+      d.word = item.insertText
+    else
+      d.word = item.label
+    endif
+    if item->get('insertTextFormat', 1) == 2
+      # snippet completion.  Needs a snippet plugin to expand the snippet.
+      # Remove all the snippet placeholders
+      d.word = MakeValidWord(d.word)
+    endif
+    d.abbr = item.label
+    d.dup = 1
+    if item->has_key('kind')
+      # namespace CompletionItemKind
+      # map LSP kind to complete-item-kind
+      d.kind = LspCompleteItemKindChar(item.kind)
+    endif
+    if lspserver.completionLazyDoc
+      d.info = 'Lazy doc'
+    else
+      if item->has_key('detail') && item.detail != ''
+        # Solve a issue where if a server send a detail field
+        # with a "\n", on the menu will be everything joined with
+        # a "^@" separating it. (example: clangd)
+        d.menu = item.detail->split("\n")[0]
+      endif
+      if item->has_key('documentation')
+        if item.documentation->type() == v:t_string && item.documentation != ''
+          d.info = item.documentation
+        elseif item.documentation->type() == v:t_dict
+            && item.documentation.value->type() == v:t_string
+          d.info = item.documentation.value
+        endif
+      endif
+    endif
+    d.user_data = item
+    completeItems->add(d)
+  endfor
+
+  if opt.lspOptions.autoComplete && !lspserver.omniCompletePending
+    if completeItems->empty()
+      # no matches
+      return
+    endif
+
+    var m = mode()
+    if m != 'i' && m != 'R' && m != 'Rv'
+      # If not in insert or replace mode, then don't start the completion
+      return
+    endif
+
+    if completeItems->len() == 1
+       && getline('.')->matchstr(completeItems[0].word .. '\>') != ''
+      # only one complete match. No need to show the completion popup
+      return
+    endif
+
+    var start_col: number = 0
+
+    # FIXME: The following doesn't work with typescript as one of the
+    # completion item has a start column that is before the special character.
+    # For example, when completing the methods for "str.", the dot is removed.
+    #
+    # # Find the start column for the completion.  If any of the entries
+    # # returned by the LSP server has a starting position, then use that.
+    # for item in items
+    #   if item->has_key('textEdit')
+    #     start_col = item.textEdit.range.start.character + 1
+    #     break
+    #   endif
+    # endfor
+
+    # LSP server didn't return a starting position for completion, search
+    # backwards from the current cursor position for a non-keyword character.
+    if start_col == 0
+      var line: string = getline('.')
+      var start = col('.') - 1
+      while start > 0 && line[start - 1] =~ '\k'
+       start -= 1
+      endwhile
+      start_col = start + 1
+    endif
+
+    complete(start_col, completeItems)
+  else
+    lspserver.completeItems = completeItems
+    lspserver.omniCompletePending = false
+  endif
+enddef
+
+# process the 'completionItem/resolve' reply from the LSP server
+# Result: CompletionItem
+export def CompletionResolveReply(lspserver: dict<any>, cItem: dict<any>)
+  if cItem->empty()
+    return
+  endif
+
+  # check if completion item is still selected
+  var cInfo = complete_info()
+  if cInfo->empty()
+      || !cInfo.pum_visible
+      || cInfo.selected == -1
+      || cInfo.items[cInfo.selected].user_data.label != cItem.label
+    return
+  endif
+
+  var infoText: list<string>
+  var infoKind: string
+
+  if cItem->has_key('detail')
+    # Solve a issue where if a server send the detail field with "\n",
+    # on the completion popup, everything will be joined with "^@"
+    # (example: typescript-language-server)
+    infoText->extend(cItem.detail->split("\n"))
+  endif
+
+  if cItem->has_key('documentation')
+    if !infoText->empty()
+      infoText->extend(['- - -'])
+    endif
+    if cItem.documentation->type() == v:t_dict
+      # MarkupContent
+      if cItem.documentation.kind == 'plaintext'
+        infoText->extend(cItem.documentation.value->split("\n"))
+        infoKind = 'text'
+      elseif cItem.documentation.kind == 'markdown'
+        infoText->extend(cItem.documentation.value->split("\n"))
+        infoKind = 'lspgfm'
+      else
+        util.ErrMsg($'Error: Unsupported documentation type ({cItem.documentation.kind})')
+        return
+      endif
+    elseif cItem.documentation->type() == v:t_string
+      infoText->extend(cItem.documentation->split("\n"))
+    else
+      util.ErrMsg($'Error: Unsupported documentation ({cItem.documentation->string()})')
+      return
+    endif
+  endif
+
+  if infoText->empty()
+    return
+  endif
+
+  # check if completion item is changed in meantime
+  cInfo = complete_info()
+  if cInfo->empty()
+      || !cInfo.pum_visible
+      || cInfo.selected == -1
+      || cInfo.items[cInfo.selected].user_data.label != cItem.label
+    return
+  endif
+
+  var id = popup_findinfo()
+  if id > 0
+    var bufnr = id->winbufnr()
+    id->popup_settext(infoText)
+    infoKind->setbufvar(bufnr, '&ft')
+    id->popup_show()
+  endif
+enddef
+
+# omni complete handler
+def g:LspOmniFunc(findstart: number, base: string): any
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
+  if lspserver->empty()
+    return -2
+  endif
+
+  if findstart
+    # first send all the changes in the current buffer to the LSP server
+    listener_flush()
+
+    lspserver.omniCompletePending = v:true
+    lspserver.completeItems = []
+    # initiate a request to LSP server to get list of completions
+    lspserver.getCompletion(1, '')
+
+    # locate the start of the word
+    var line = getline('.')
+    var start = charcol('.') - 1
+    while start > 0 && line[start - 1] =~ '\k'
+      start -= 1
+    endwhile
+    return start
+  else
+    # Wait for the list of matches from the LSP server
+    var count: number = 0
+    while lspserver.omniCompletePending && count < 1000
+      if complete_check()
+       return v:none
+      endif
+      sleep 2m
+      count += 1
+    endwhile
+
+    var res: list<dict<any>> = []
+    for item in lspserver.completeItems
+      res->add(item)
+    endfor
+    return res->empty() ? v:none : res
+  endif
+enddef
+
+# Insert mode completion handler. Used when 24x7 completion is enabled
+# (default).
+def LspComplete()
+  var lspserver: dict<any> = buf.CurbufGetServer()
+  if lspserver->empty() || !lspserver.running || !lspserver.ready
+    return
+  endif
+
+  var cur_col: number = col('.')
+  var line: string = getline('.')
+
+  if cur_col == 0 || line->empty()
+    return
+  endif
+
+  # Trigger kind is 1 for 24x7 code complete or manual invocation
+  var triggerKind: number = 1
+  var triggerChar: string = ''
+
+  # If the character before the cursor is not a keyword character or is not
+  # one of the LSP completion trigger characters, then do nothing.
+  if line[cur_col - 2] !~ '\k'
+    var trigidx = lspserver.completionTriggerChars->index(line[cur_col - 2])
+    if trigidx == -1
+      return
+    endif
+    # completion triggered by one of the trigger characters
+    triggerKind = 2
+    triggerChar = lspserver.completionTriggerChars[trigidx]
+  endif
+
+  # first send all the changes in the current buffer to the LSP server
+  listener_flush()
+
+  # initiate a request to LSP server to get list of completions
+  lspserver.getCompletion(triggerKind, triggerChar)
+enddef
+
+# Lazy complete documentation handler
+def LspResolve()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
+  if lspserver->empty()
+    return
+  endif
+
+  var item = v:event.completed_item
+  if item->has_key('user_data') && !empty(item.user_data)
+    lspserver.resolveCompletion(item.user_data)
+  endif
+enddef
+
+# If the completion popup documentation window displays 'markdown' content,
+# then set the 'filetype' to 'lspgfm'.
+def LspSetFileType()
+  var item = v:event.completed_item
+  if !item->has_key('user_data') || empty(item.user_data)
+    return
+  endif
+
+  var cItem = item.user_data
+  if !cItem->has_key('documentation') || cItem->type() != v:t_dict
+                               || cItem.documentation.kind != 'markdown'
+    return
+  endif
+
+  var id = popup_findinfo()
+  if id > 0
+    var bnum = id->winbufnr()
+    setbufvar(bnum, '&ft', 'lspgfm')
+  endif
+enddef
+
+# complete done handler (LSP server-initiated actions after completion)
+def LspCompleteDone()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
+  if lspserver->empty()
+    return
+  endif
+
+  if v:completed_item->type() != v:t_dict
+    return
+  endif
+
+  var completionData: any = v:completed_item->get('user_data', '')
+  if completionData->type() != v:t_dict
+      || !completionData->has_key('additionalTextEdits')
+    return
+  endif
+
+  var bnr: number = bufnr()
+  textedit.ApplyTextEdits(bnr, completionData.additionalTextEdits)
+enddef
+
+# Add buffer-local autocmds for completion
+def AddAutocmds(lspserver: dict<any>, bnr: number)
+  var acmds: list<dict<any>> = []
+
+  # Insert-mode completion autocmds (if configured)
+  if opt.lspOptions.autoComplete
+    # Trigger 24x7 insert mode completion when text is changed
+    acmds->add({bufnr: bnr,
+               event: 'TextChangedI',
+               group: 'LSPBufferAutocmds',
+               cmd: 'LspComplete()'})
+    if lspserver.completionLazyDoc
+      # resolve additional documentation for a selected item
+      acmds->add({bufnr: bnr,
+                 event: 'CompleteChanged',
+                 group: 'LSPBufferAutocmds',
+                 cmd: 'LspResolve()'})
+    endif
+  endif
+
+  acmds->add({bufnr: bnr,
+                 event: 'CompleteChanged',
+                 group: 'LSPBufferAutocmds',
+                 cmd: 'LspSetFileType()'})
+
+  # Execute LSP server initiated text edits after completion
+  acmds->add({bufnr: bnr,
+             event: 'CompleteDone',
+             group: 'LSPBufferAutocmds',
+             cmd: 'LspCompleteDone()'})
+
+  autocmd_add(acmds)
+enddef
+
+# Initialize buffer-local completion options and autocmds
+export def BufferInit(lspserver: dict<any>, bnr: number, ftype: string)
+  # set options for insert mode completion
+  if opt.lspOptions.autoComplete
+    if lspserver.completionLazyDoc
+      setbufvar(bnr, '&completeopt', 'menuone,popuphidden,noinsert,noselect')
+      setbufvar(bnr, '&completepopup', 'width:80,highlight:Pmenu,align:menu,border:off')
+    else
+      setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect')
+      setbufvar(bnr, '&completepopup', 'border:off')
+    endif
+    # <Enter> in insert mode stops completion and inserts a <Enter>
+    if !opt.lspOptions.noNewlineInCompletion
+      inoremap <expr> <buffer> <CR> pumvisible() ? "\<C-Y>\<CR>" : "\<CR>"
+    endif
+  else
+    if LspOmniComplEnabled(ftype)
+      setbufvar(bnr, '&omnifunc', 'LspOmniFunc')
+    endif
+  endif
+
+  AddAutocmds(lspserver, bnr)
+enddef
+
+# vim: tabstop=8 shiftwidth=2 softtabstop=2
index cfe7ec73e43eb5569b9b3150fa2a59e2ebfdec5c..0f1a19c7a5b51f29b87be489f52cc9e4b4f92a14 100644 (file)
@@ -11,6 +11,7 @@ import './options.vim' as opt
 import './lspserver.vim' as lserver
 import './util.vim'
 import './buffer.vim' as buf
+import './completion.vim'
 import './textedit.vim'
 import './diag.vim'
 import './symbol.vim'
@@ -24,9 +25,6 @@ var lspServers: list<dict<any>> = []
 # filetype to LSP server map
 var ftypeServerMap: dict<dict<any>> = {}
 
-# per-filetype omni-completion enabled/disabled table
-var ftypeOmniCtrlMap: dict<bool> = {}
-
 var lspInitializedOnce = false
 
 def LspInitOnce()
@@ -53,47 +51,11 @@ def LspGetServer(ftype: string): dict<any>
   return ftypeServerMap->get(ftype, {})
 enddef
 
-# Returns the LSP server for the current buffer if it is running and is ready.
-# Returns an empty dict if the server is not found or is not ready.
-def CurbufGetServerChecked(): dict<any>
-  var fname: string = @%
-  if fname == ''
-    return {}
-  endif
-
-  var lspserver: dict<any> = buf.CurbufGetServer()
-  if lspserver->empty()
-    util.ErrMsg($'Error: Language server for "{&filetype}" file type is not found')
-    return {}
-  endif
-  if !lspserver.running
-    util.ErrMsg($'Error: Language server for "{&filetype}" file type is not running')
-    return {}
-  endif
-  if !lspserver.ready
-    util.ErrMsg($'Error: Language server for "{&filetype}" file type is not ready')
-    return {}
-  endif
-
-  return lspserver
-enddef
-
 # Add a LSP server for a filetype
 def LspAddServer(ftype: string, lspsrv: dict<any>)
   ftypeServerMap->extend({[ftype]: lspsrv})
 enddef
 
-# Returns true if omni-completion is enabled for filetype 'ftype'.
-# Otherwise, returns false.
-def LspOmniComplEnabled(ftype: string): bool
-  return ftypeOmniCtrlMap->get(ftype, v:false)
-enddef
-
-# Enables or disables omni-completion for filetype 'fype'
-def LspOmniComplSet(ftype: string, enabled: bool)
-  ftypeOmniCtrlMap->extend({[ftype]: enabled})
-enddef
-
 # Enable/disable the logging of the language server protocol messages
 export def ServerDebug(arg: string)
   if arg !=? 'on' && arg !=? 'off'
@@ -136,7 +98,7 @@ enddef
 
 # Go to a definition using "textDocument/definition" LSP request
 export def GotoDefinition(peek: bool, cmdmods: string)
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -146,7 +108,7 @@ enddef
 
 # Go to a declaration using "textDocument/declaration" LSP request
 export def GotoDeclaration(peek: bool, cmdmods: string)
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -156,7 +118,7 @@ enddef
 
 # Go to a type definition using "textDocument/typeDefinition" LSP request
 export def GotoTypedef(peek: bool, cmdmods: string)
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -166,7 +128,7 @@ enddef
 
 # Go to a implementation using "textDocument/implementation" LSP request
 export def GotoImplementation(peek: bool, cmdmods: string)
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -177,7 +139,7 @@ enddef
 # Switch source header using "textDocument/switchSourceHeader" LSP request
 # (Clangd specifc extension)
 export def SwitchSourceHeader()
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -188,7 +150,7 @@ enddef
 # Show the signature using "textDocument/signatureHelp" LSP method
 # Invoked from an insert-mode mapping, so return an empty string.
 def g:LspShowSignature(): string
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return ''
   endif
@@ -277,33 +239,6 @@ def AddBufLocalAutocmds(lspserver: dict<any>, bnr: number): void
              group: 'LSPBufferAutocmds',
              cmd: 'LspLeftInsertMode()'})
 
-  # Insert-mode completion autocmds (if configured)
-  if opt.lspOptions.autoComplete
-    # Trigger 24x7 insert mode completion when text is changed
-    acmds->add({bufnr: bnr,
-               event: 'TextChangedI',
-               group: 'LSPBufferAutocmds',
-               cmd: 'LspComplete()'})
-    if lspserver.completionLazyDoc
-      # resolve additional documentation for a selected item
-      acmds->add({bufnr: bnr,
-                 event: 'CompleteChanged',
-                 group: 'LSPBufferAutocmds',
-                 cmd: 'LspResolve()'})
-    endif
-  endif
-
-  acmds->add({bufnr: bnr,
-                 event: 'CompleteChanged',
-                 group: 'LSPBufferAutocmds',
-                 cmd: 'LspSetFileType()'})
-
-  # Execute LSP server initiated text edits after completion
-  acmds->add({bufnr: bnr,
-             event: 'CompleteDone',
-             group: 'LSPBufferAutocmds',
-             cmd: 'LspCompleteDone()'})
-
   # Auto highlight all the occurrences of the current keyword
   if opt.lspOptions.autoHighlight &&
                        lspserver.isDocumentHighlightProvider
@@ -329,24 +264,7 @@ def BufferInit(bnr: number): void
   # add a listener to track changes to this buffer
   listener_add(Bufchange_listener, bnr)
 
-  # set options for insert mode completion
-  if opt.lspOptions.autoComplete
-    if lspserver.completionLazyDoc
-      setbufvar(bnr, '&completeopt', 'menuone,popuphidden,noinsert,noselect')
-      setbufvar(bnr, '&completepopup', 'width:80,highlight:Pmenu,align:menu,border:off')
-    else
-      setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect')
-      setbufvar(bnr, '&completepopup', 'border:off')
-    endif
-    # <Enter> in insert mode stops completion and inserts a <Enter>
-    if !opt.lspOptions.noNewlineInCompletion
-      inoremap <expr> <buffer> <CR> pumvisible() ? "\<C-Y>\<CR>" : "\<CR>"
-    endif
-  else
-    if LspOmniComplEnabled(ftype)
-      setbufvar(bnr, '&omnifunc', 'LspOmniFunc')
-    endif
-  endif
+  completion.BufferInit(lspserver, bnr, ftype)
 
   setbufvar(bnr, '&balloonexpr', 'g:LspDiagExpr()')
 
@@ -432,7 +350,7 @@ enddef
 
 # Restart the LSP server for the current buffer
 export def RestartServer()
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -457,7 +375,7 @@ enddef
 # Add the LSP server for files with 'filetype' as "ftype".
 def AddServerForFiltype(lspserver: dict<any>, ftype: string, omnicompl: bool)
   LspAddServer(ftype, lspserver)
-  LspOmniComplSet(ftype, omnicompl)
+  completion.OmniComplSet(ftype, omnicompl)
 
   # If a buffer of this file type is already present, then send it to the LSP
   # server now.
@@ -551,7 +469,7 @@ export def ServerTraceSet(traceVal: string)
     return
   endif
 
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -562,7 +480,7 @@ enddef
 # Display the diagnostic messages from the LSP server for the current buffer
 # in a quickfix list
 export def ShowDiagnostics(): void
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -572,7 +490,7 @@ enddef
 
 # Show the diagnostic message for the current line
 export def LspShowCurrentDiag()
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -613,7 +531,7 @@ enddef
 
 # jump to the next/previous/first diagnostic message in the current buffer
 export def JumpToDiag(which: string): void
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -621,143 +539,6 @@ export def JumpToDiag(which: string): void
   diag.LspDiagsJump(lspserver, which)
 enddef
 
-# Insert mode completion handler. Used when 24x7 completion is enabled
-# (default).
-def LspComplete()
-  var lspserver: dict<any> = buf.CurbufGetServer()
-  if lspserver->empty() || !lspserver.running || !lspserver.ready
-    return
-  endif
-
-  var cur_col: number = col('.')
-  var line: string = getline('.')
-
-  if cur_col == 0 || line->empty()
-    return
-  endif
-
-  # Trigger kind is 1 for 24x7 code complete or manual invocation
-  var triggerKind: number = 1
-  var triggerChar: string = ''
-
-  # If the character before the cursor is not a keyword character or is not
-  # one of the LSP completion trigger characters, then do nothing.
-  if line[cur_col - 2] !~ '\k'
-    var trigidx = lspserver.completionTriggerChars->index(line[cur_col - 2])
-    if trigidx == -1
-      return
-    endif
-    # completion triggered by one of the trigger characters
-    triggerKind = 2
-    triggerChar = lspserver.completionTriggerChars[trigidx]
-  endif
-
-  # first send all the changes in the current buffer to the LSP server
-  listener_flush()
-
-  # initiate a request to LSP server to get list of completions
-  lspserver.getCompletion(triggerKind, triggerChar)
-
-  return
-enddef
-
-# Lazy complete documentation handler
-def LspResolve()
-  var lspserver: dict<any> = CurbufGetServerChecked()
-  if lspserver->empty()
-    return
-  endif
-
-  var item = v:event.completed_item
-  if item->has_key('user_data') && !empty(item.user_data)
-    lspserver.resolveCompletion(item.user_data)
-  endif
-enddef
-
-# If the completion popup documentation window displays 'markdown' content,
-# then set the 'filetype' to 'lspgfm'.
-def LspSetFileType()
-  var item = v:event.completed_item
-  if !item->has_key('user_data') || empty(item.user_data)
-    return
-  endif
-
-  var cItem = item.user_data
-  if !cItem->has_key('documentation') || cItem->type() != v:t_dict
-                               || cItem.documentation.kind != 'markdown'
-    return
-  endif
-
-  var id = popup_findinfo()
-  if id > 0
-    var bnum = id->winbufnr()
-    setbufvar(bnum, '&ft', 'lspgfm')
-  endif
-enddef
-
-# omni complete handler
-def g:LspOmniFunc(findstart: number, base: string): any
-  var lspserver: dict<any> = CurbufGetServerChecked()
-  if lspserver->empty()
-    return -2
-  endif
-
-  if findstart
-    # first send all the changes in the current buffer to the LSP server
-    listener_flush()
-
-    lspserver.omniCompletePending = v:true
-    lspserver.completeItems = []
-    # initiate a request to LSP server to get list of completions
-    lspserver.getCompletion(1, '')
-
-    # locate the start of the word
-    var line = getline('.')
-    var start = charcol('.') - 1
-    while start > 0 && line[start - 1] =~ '\k'
-      start -= 1
-    endwhile
-    return start
-  else
-    # Wait for the list of matches from the LSP server
-    var count: number = 0
-    while lspserver.omniCompletePending && count < 1000
-      if complete_check()
-       return v:none
-      endif
-      sleep 2m
-      count += 1
-    endwhile
-
-    var res: list<dict<any>> = []
-    for item in lspserver.completeItems
-      res->add(item)
-    endfor
-    return res->empty() ? v:none : res
-  endif
-enddef
-
-# complete done handler (LSP server-initiated actions after completion)
-def LspCompleteDone()
-  var lspserver: dict<any> = CurbufGetServerChecked()
-  if lspserver->empty()
-    return
-  endif
-
-  if v:completed_item->type() != v:t_dict
-    return
-  endif
-
-  var completionData: any = v:completed_item->get('user_data', '')
-  if completionData->type() != v:t_dict
-      || !completionData->has_key('additionalTextEdits')
-    return
-  endif
-
-  var bnr: number = bufnr()
-  textedit.ApplyTextEdits(bnr, completionData.additionalTextEdits)
-enddef
-
 # Display the hover message from the LSP server for the current cursor
 # location
 export def Hover()
@@ -771,7 +552,7 @@ enddef
 
 # show symbol references
 export def ShowReferences(peek: bool)
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -781,7 +562,7 @@ enddef
 
 # highlight all the places where a symbol is referenced
 def g:LspDocHighlight()
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -827,7 +608,7 @@ export def TextDocFormat(range_args: number, line1: number, line2: number)
     return
   endif
 
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -846,7 +627,7 @@ enddef
 # Display all the locations where the current symbol is called from.
 # Uses LSP "callHierarchy/incomingCalls" request
 export def IncomingCalls()
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -857,7 +638,7 @@ enddef
 # Display all the symbols used by the current symbol.
 # Uses LSP "callHierarchy/outgoingCalls" request
 export def OutgoingCalls()
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -868,7 +649,7 @@ enddef
 # Rename a symbol
 # Uses LSP "textDocument/rename" request
 export def Rename()
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -888,7 +669,7 @@ enddef
 # Perform a code action
 # Uses LSP "textDocument/codeAction" request
 export def CodeAction(line1: number, line2: number)
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -900,7 +681,7 @@ enddef
 # Perform a workspace wide symbol lookup
 # Uses LSP "workspace/symbol" request
 export def SymbolSearch(queryArg: string)
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -919,7 +700,7 @@ enddef
 
 # Display the list of workspace folders
 export def ListWorkspaceFolders()
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -929,7 +710,7 @@ enddef
 
 # Add a workspace folder. Default is to use the current folder.
 export def AddWorkspaceFolder(dirArg: string)
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -952,7 +733,7 @@ enddef
 
 # Remove a workspace folder. Default is to use the current folder.
 export def RemoveWorkspaceFolder(dirArg: string)
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -975,7 +756,7 @@ enddef
 
 # expand the previous selection or start a new selection
 export def SelectionExpand()
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -985,7 +766,7 @@ enddef
 
 # shrink the previous selection or start a new selection
 export def SelectionShrink()
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -995,7 +776,7 @@ enddef
 
 # fold the entire document
 export def FoldDocument()
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -1021,7 +802,7 @@ enddef
 
 # Display the LSP server capabilities
 export def ShowServerCapabilities()
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return
   endif
@@ -1031,7 +812,7 @@ enddef
 
 # Function to use with the 'tagfunc' option.
 export def TagFunc(pat: string, flags: string, info: dict<any>): any
-  var lspserver: dict<any> = CurbufGetServerChecked()
+  var lspserver: dict<any> = buf.CurbufGetServerChecked()
   if lspserver->empty()
     return v:null
   endif
index a22d2f3c3f3a9c8ca671640c1df5ae9094fe6c49..9b747395d48d6a2d695bc8f81068ffe0ce0d48a1 100644 (file)
@@ -11,6 +11,7 @@ import './diag.vim'
 import './selection.vim'
 import './symbol.vim'
 import './textedit.vim'
+import './completion.vim'
 import './hover.vim'
 import './signature.vim'
 import './codeaction.vim'
@@ -754,165 +755,6 @@ def GetLspTextDocPosition(): dict<dict<any>>
          position: GetLspPosition()}
 enddef
 
-# Map LSP complete item kind to a character
-def LspCompleteItemKindChar(kind: number): string
-  var kindMap: list<string> = ['',
-               't', # Text
-               'm', # Method
-               'f', # Function
-               'C', # Constructor
-               'F', # Field
-               'v', # Variable
-               'c', # Class
-               'i', # Interface
-               'M', # Module
-               'p', # Property
-               'u', # Unit
-               'V', # Value
-               'e', # Enum
-               'k', # Keyword
-               'S', # Snippet
-               'C', # Color
-               'f', # File
-               'r', # Reference
-               'F', # Folder
-               'E', # EnumMember
-               'd', # Contant
-               's', # Struct
-               'E', # Event
-               'o', # Operator
-               'T'  # TypeParameter
-       ]
-  if kind > 25
-    return ''
-  endif
-  return kindMap[kind]
-enddef
-
-# Remove all the snippet placeholders from 'str' and return the value.
-# Based on a similar function in the vim-lsp plugin.
-def MakeValidWord(str_arg: string): string
-  var str = substitute(str_arg, '\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g')
-  str = substitute(str, '\\\(.\)', '\1', 'g')
-  var valid = matchstr(str, '^[^"'' (<{\[\t\r\n]\+')
-  if empty(valid)
-    return str
-  endif
-  if valid =~# ':$'
-    return valid[: -2]
-  endif
-  return valid
-enddef
-
-# process the 'textDocument/completion' reply from the LSP server
-# Result: CompletionItem[] | CompletionList | null
-def CompletionReply(lspserver: dict<any>, cItems: any)
-  if cItems->empty()
-    return
-  endif
-
-  var items: list<dict<any>>
-  if cItems->type() == v:t_list
-    items = cItems
-  else
-    items = cItems.items
-  endif
-
-  var completeItems: list<dict<any>> = []
-  for item in items
-    var d: dict<any> = {}
-    if item->has_key('textEdit') && item.textEdit->has_key('newText')
-      d.word = item.textEdit.newText
-    elseif item->has_key('insertText')
-      d.word = item.insertText
-    else
-      d.word = item.label
-    endif
-    if item->get('insertTextFormat', 1) == 2
-      # snippet completion.  Needs a snippet plugin to expand the snippet.
-      # Remove all the snippet placeholders
-      d.word = MakeValidWord(d.word)
-    endif
-    d.abbr = item.label
-    d.dup = 1
-    if item->has_key('kind')
-      # namespace CompletionItemKind
-      # map LSP kind to complete-item-kind
-      d.kind = LspCompleteItemKindChar(item.kind)
-    endif
-    if lspserver.completionLazyDoc
-      d.info = 'Lazy doc'
-    else
-      if item->has_key('detail') && item.detail != ''
-        # Solve a issue where if a server send a detail field
-        # with a "\n", on the menu will be everything joined with
-        # a "^@" separating it. (example: clangd)
-        d.menu = item.detail->split("\n")[0]
-      endif
-      if item->has_key('documentation')
-        if item.documentation->type() == v:t_string && item.documentation != ''
-          d.info = item.documentation
-        elseif item.documentation->type() == v:t_dict
-            && item.documentation.value->type() == v:t_string
-          d.info = item.documentation.value
-        endif
-      endif
-    endif
-    d.user_data = item
-    completeItems->add(d)
-  endfor
-
-  if opt.lspOptions.autoComplete && !lspserver.omniCompletePending
-    if completeItems->empty()
-      # no matches
-      return
-    endif
-
-    var m = mode()
-    if m != 'i' && m != 'R' && m != 'Rv'
-      # If not in insert or replace mode, then don't start the completion
-      return
-    endif
-
-    if completeItems->len() == 1
-       && getline('.')->matchstr(completeItems[0].word .. '\>') != ''
-      # only one complete match. No need to show the completion popup
-      return
-    endif
-
-    var start_col: number = 0
-
-    # FIXME: The following doesn't work with typescript as one of the
-    # completion item has a start column that is before the special character.
-    # For example, when completing the methods for "str.", the dot is removed.
-    #
-    # # Find the start column for the completion.  If any of the entries
-    # # returned by the LSP server has a starting position, then use that.
-    # for item in items
-    #   if item->has_key('textEdit')
-    #     start_col = item.textEdit.range.start.character + 1
-    #     break
-    #   endif
-    # endfor
-
-    # LSP server didn't return a starting position for completion, search
-    # backwards from the current cursor position for a non-keyword character.
-    if start_col == 0
-      var line: string = getline('.')
-      var start = col('.') - 1
-      while start > 0 && line[start - 1] =~ '\k'
-       start -= 1
-      endwhile
-      start_col = start + 1
-    endif
-
-    complete(start_col, completeItems)
-  else
-    lspserver.completeItems = completeItems
-    lspserver.omniCompletePending = false
-  endif
-enddef
-
 # Get a list of completion items.
 # Request: "textDocument/completion"
 # Param: CompletionParams
@@ -934,79 +776,8 @@ def GetCompletion(lspserver: dict<any>, triggerKind_arg: number, triggerChar: st
   #   interface CompletionContext
   params.context = {triggerKind: triggerKind_arg, triggerCharacter: triggerChar}
 
-  lspserver.rpc_a('textDocument/completion', params, CompletionReply)
-enddef
-
-# process the 'completionItem/resolve' reply from the LSP server
-# Result: CompletionItem
-def CompletionResolveReply(lspserver: dict<any>, cItem: dict<any>)
-  if cItem->empty()
-    return
-  endif
-
-  # check if completion item is still selected
-  var cInfo = complete_info()
-  if cInfo->empty()
-      || !cInfo.pum_visible
-      || cInfo.selected == -1
-      || cInfo.items[cInfo.selected].user_data.label != cItem.label
-    return
-  endif
-
-  var infoText: list<string>
-  var infoKind: string
-
-  if cItem->has_key('detail')
-    # Solve a issue where if a server send the detail field with "\n",
-    # on the completion popup, everything will be joined with "^@"
-    # (example: typescript-language-server)
-    infoText->extend(cItem.detail->split("\n"))
-  endif
-
-  if cItem->has_key('documentation')
-    if !infoText->empty()
-      infoText->extend(['- - -'])
-    endif
-    if cItem.documentation->type() == v:t_dict
-      # MarkupContent
-      if cItem.documentation.kind == 'plaintext'
-        infoText->extend(cItem.documentation.value->split("\n"))
-        infoKind = 'text'
-      elseif cItem.documentation.kind == 'markdown'
-        infoText->extend(cItem.documentation.value->split("\n"))
-        infoKind = 'lspgfm'
-      else
-        util.ErrMsg($'Error: Unsupported documentation type ({cItem.documentation.kind})')
-        return
-      endif
-    elseif cItem.documentation->type() == v:t_string
-      infoText->extend(cItem.documentation->split("\n"))
-    else
-      util.ErrMsg($'Error: Unsupported documentation ({cItem.documentation->string()})')
-      return
-    endif
-  endif
-
-  if infoText->empty()
-    return
-  endif
-
-  # check if completion item is changed in meantime
-  cInfo = complete_info()
-  if cInfo->empty()
-      || !cInfo.pum_visible
-      || cInfo.selected == -1
-      || cInfo.items[cInfo.selected].user_data.label != cItem.label
-    return
-  endif
-
-  var id = popup_findinfo()
-  if id > 0
-    var bufnr = id->winbufnr()
-    id->popup_settext(infoText)
-    infoKind->setbufvar(bufnr, '&ft')
-    id->popup_show()
-  endif
+  lspserver.rpc_a('textDocument/completion', params,
+                       completion.CompletionReply)
 enddef
 
 # Get lazy properties for a completion item.
@@ -1020,9 +791,8 @@ def ResolveCompletion(lspserver: dict<any>, item: dict<any>): void
   endif
 
   # interface CompletionItem
-  var params = item
-
-  lspserver.rpc_a('completionItem/resolve', params, CompletionResolveReply)
+  lspserver.rpc_a('completionItem/resolve', item,
+                       completion.CompletionResolveReply)
 enddef
 
 # Jump to or peek a symbol location.