]> Sergey Matveev's repositories - vim-lsp.git/commitdiff
Refactor the code
authorYegappan Lakshmanan <yegappan@yahoo.com>
Thu, 31 Dec 2020 03:27:04 +0000 (19:27 -0800)
committerYegappan Lakshmanan <yegappan@yahoo.com>
Thu, 31 Dec 2020 03:27:04 +0000 (19:27 -0800)
autoload/handlers.vim [new file with mode: 0644]
autoload/lsp.vim
autoload/lspserver.vim [new file with mode: 0644]
autoload/util.vim [new file with mode: 0644]

diff --git a/autoload/handlers.vim b/autoload/handlers.vim
new file mode 100644 (file)
index 0000000..1276d94
--- /dev/null
@@ -0,0 +1,904 @@
+vim9script
+
+import {WarnMsg, ErrMsg, LspUriToFile} from './util.vim'
+
+# process the 'initialize' method reply from the LSP server
+def s:processInitializeReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
+  if reply.result->len() <= 0
+    return
+  endif
+
+  # interface 'InitializeResult'
+  var caps: dict<any> = reply.result.capabilities
+  lspserver.caps = caps
+
+  # TODO: Check all the buffers with filetype corresponding to this LSP server
+  # and then setup the below mapping for those buffers.
+
+  # map characters that trigger signature help
+  if caps->has_key('signatureHelpProvider')
+    var triggers = caps.signatureHelpProvider.triggerCharacters
+    for ch in triggers
+      exe 'inoremap <buffer> <silent> ' .. ch .. ' ' .. ch .. "<C-R>=lsp#showSignature()<CR>"
+    endfor
+  endif
+
+  # map characters that trigger insert mode completion
+  if caps->has_key('completionProvider')
+    var triggers = caps.completionProvider.triggerCharacters
+    for ch in triggers
+      exe 'inoremap <buffer> <silent> ' .. ch .. ' ' .. ch .. "<C-X><C-U>"
+    endfor
+  endif
+
+  # send a "initialized" notification to server
+  lspserver.sendInitializedNotif()
+enddef
+
+# process the 'textDocument/definition' / 'textDocument/declaration' /
+# 'textDocument/typeDefinition' and 'textDocument/implementation' replies from
+# the LSP server
+def s:processDefDeclReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
+  if reply.result->empty()
+    WarnMsg("Error: definition is not found")
+    # pop the tag stack
+    var tagstack: dict<any> = gettagstack()
+    if tagstack.length > 0
+      settagstack(winnr(), {'curidx': tagstack.length}, 't')
+    endif
+    return
+  endif
+
+  var result: dict<any> = reply.result[0]
+  var file = LspUriToFile(result.uri)
+  var wid = bufwinid(file)
+  if wid != -1
+    win_gotoid(wid)
+  else
+    exe 'split ' .. file
+  endif
+  # Set the previous cursor location mark
+  setpos("'`", getcurpos())
+  cursor(result.range.start.line + 1, result.range.start.character + 1)
+  redraw!
+enddef
+
+# process the 'textDocument/signatureHelp' reply from the LSP server
+def s:processSignaturehelpReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
+  var result: dict<any> = reply.result
+  if result.signatures->len() <= 0
+    WarnMsg('No signature help available')
+    return
+  endif
+
+  var sig: dict<any> = result.signatures[result.activeSignature]
+  var text = sig.label
+  var hllen = 0
+  var startcol = 0
+  if sig->has_key('parameters')
+    var params_len = sig.parameters->len()
+    if params_len > 0 && result.activeParameter < params_len
+      var label = sig.parameters[result.activeParameter].label
+      hllen = label->len()
+      startcol = text->stridx(label)
+    endif
+  endif
+  var popupID = popup_atcursor(text, {})
+  prop_type_add('signature', {'bufnr': popupID->winbufnr(), 'highlight': 'Title'})
+  if hllen > 0
+    prop_add(1, startcol + 1, {'bufnr': popupID->winbufnr(), 'length': hllen, 'type': 'signature'})
+  endif
+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
+
+# process the 'textDocument/completion' reply from the LSP server
+def s:processCompletionReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
+
+  var items: list<dict<any>>
+  if type(reply.result) == v:t_list
+    items = reply.result
+  else
+    items = reply.result.items
+  endif
+
+  for item in items
+    var d: dict<any> = {}
+    if item->has_key('insertText')
+      d.word = item.insertText
+    elseif item->has_key('textEdit')
+      d.word = item.textEdit.newText
+    else
+      d.word = item.label
+    endif
+    if item->has_key('kind')
+      # namespace CompletionItemKind
+      # map LSP kind to complete-item-kind
+      d.kind = LspCompleteItemKindChar(item.kind)
+    endif
+    if item->has_key('detail')
+      d.menu = item.detail
+    endif
+    if item->has_key('documentation')
+      d.info = item.documentation
+    endif
+    lspserver.completeItems->add(d)
+  endfor
+
+  lspserver.completePending = v:false
+enddef
+
+# process the 'textDocument/hover' reply from the LSP server
+def s:processHoverReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
+  if type(reply.result) == v:t_none
+    return
+  endif
+
+  var hoverText: list<string>
+
+  if type(reply.result.contents) == v:t_dict
+    if reply.result.contents->has_key('kind')
+      # MarkupContent
+      if reply.result.contents.kind == 'plaintext'
+        hoverText = reply.result.contents.value->split("\n")
+      elseif reply.result.contents.kind == 'markdown'
+        hoverText = reply.result.contents.value->split("\n")
+      else
+        ErrMsg('Error: Unsupported hover contents type (' .. reply.result.contents.kind .. ')')
+        return
+      endif
+    elseif reply.result.contents->has_key('value')
+      # MarkedString
+      hoverText = reply.result.contents.value
+    else
+      ErrMsg('Error: Unsupported hover contents (' .. reply.result.contents .. ')')
+      return
+    endif
+  elseif type(reply.result.contents) == v:t_list
+    # interface MarkedString[]
+    for e in reply.result.contents
+      if type(e) == v:t_string
+        hoverText->extend(e->split("\n"))
+      else
+        hoverText->extend(e.value->split("\n"))
+      endif
+    endfor
+  elseif type(reply.result.contents) == v:t_string
+    if reply.result.contents->empty()
+      return
+    endif
+    hoverText->add(reply.result.contents)
+  else
+    ErrMsg('Error: Unsupported hover contents (' .. reply.result.contents .. ')')
+    return
+  endif
+  hoverText->popup_atcursor({'moved': 'word'})
+enddef
+
+# process the 'textDocument/references' reply from the LSP server
+def s:processReferencesReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
+  if type(reply.result) == v:t_none || reply.result->empty()
+    WarnMsg('Error: No references found')
+    return
+  endif
+
+  # create a quickfix list with the location of the references
+  var locations: list<dict<any>> = reply.result
+  var qflist: list<dict<any>> = []
+  for loc in locations
+    var fname: string = LspUriToFile(loc.uri)
+    var bnr: number = fname->bufnr()
+    if bnr == -1
+      bnr = fname->bufadd()
+      bnr->bufload()
+    endif
+    var text: string = bnr->getbufline(loc.range.start.line + 1)[0]
+                                               ->trim("\t ", 1)
+    qflist->add({'filename': fname,
+                    'lnum': loc.range.start.line + 1,
+                    'col': loc.range.start.character + 1,
+                    'text': text})
+  endfor
+  setqflist([], ' ', {'title': 'Language Server', 'items': qflist})
+  var save_winid = win_getid()
+  copen
+  win_gotoid(save_winid)
+enddef
+
+# process the 'textDocument/documentHighlight' reply from the LSP server
+def s:processDocHighlightReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
+  if reply.result->empty()
+    return
+  endif
+
+  var fname: string = LspUriToFile(req.params.textDocument.uri)
+  var bnr = bufnr(fname)
+
+  for docHL in reply.result
+    var kind: number = docHL->get('kind', 1)
+    var propName: string
+    if kind == 2
+      # Read-access
+      propName = 'LspReadRef'
+    elseif kind == 3
+      # Write-access
+      propName = 'LspWriteRef'
+    else
+      # textual reference
+      propName = 'LspTextRef'
+    endif
+    prop_add(docHL.range.start.line + 1, docHL.range.start.character + 1,
+               {'end_lnum': docHL.range.end.line + 1,
+                'end_col': docHL.range.end.character + 1,
+                'bufnr': bnr,
+                'type': propName})
+  endfor
+enddef
+
+# map the LSP symbol kind number to string
+def LspSymbolKindToName(symkind: number): string
+  var symbolMap: list<string> = ['', 'File', 'Module', 'Namespace', 'Package',
+       'Class', 'Method', 'Property', 'Field', 'Constructor', 'Enum',
+        'Interface', 'Function', 'Variable', 'Constant', 'String', 'Number',
+        'Boolean', 'Array', 'Object', 'Key', 'Null', 'EnumMember', 'Struct',
+        'Event', 'Operator', 'TypeParameter']
+  if symkind > 26
+    return ''
+  endif
+  return symbolMap[symkind]
+enddef
+
+# jump to a symbol selected in the symbols window
+def handlers#jumpToSymbol()
+  var lnum: number = line('.') - 1
+  if w:lsp_info.data[lnum]->empty()
+    return
+  endif
+
+  var slnum: number = w:lsp_info.data[lnum].lnum
+  var scol: number = w:lsp_info.data[lnum].col
+  var fname: string = w:lsp_info.filename
+
+  # If the file is already opened in a window, jump to it. Otherwise open it
+  # in another window
+  var wid: number = fname->bufwinid()
+  if wid == -1
+    # Find a window showing a normal buffer and use it
+    for w in getwininfo()
+      if w.winid->getwinvar('&buftype') == ''
+       wid = w.winid
+       wid->win_gotoid()
+       break
+      endif
+    endfor
+    if wid == -1
+      var symWinid: number = win_getid()
+      :rightbelow vnew
+      # retain the fixed symbol window width
+      win_execute(symWinid, 'vertical resize 20')
+    endif
+
+    exe 'edit ' .. fname
+  else
+    wid->win_gotoid()
+  endif
+  [slnum, scol]->cursor()
+enddef
+
+# display the list of document symbols from the LSP server in a window as a
+# tree
+def s:showSymbols(symTable: list<dict<any>>)
+  var symbols: dict<list<dict<any>>>
+  var symbolType: string
+  var fname: string
+
+  for symbol in symTable
+    if symbol->has_key('location')
+      fname = LspUriToFile(symbol.location.uri)
+      symbolType = LspSymbolKindToName(symbol.kind)
+      if !symbols->has_key(symbolType)
+       symbols[symbolType] = []
+      endif
+      var name: string = symbol.name
+      if symbol->has_key('containerName')
+       if symbol.containerName != ''
+         name ..= ' [' .. symbol.containerName .. ']'
+       endif
+      endif
+      symbols[symbolType]->add({'name': name,
+                       'lnum': symbol.location.range.start.line + 1,
+                        'col': symbol.location.range.start.character + 1})
+    endif
+  endfor
+
+  var wid: number = bufwinid('LSP-Symbols')
+  if wid == -1
+    :20vnew LSP-Symbols
+  else
+    win_gotoid(wid)
+  endif
+
+  :setlocal modifiable
+  :setlocal noreadonly
+  :silent! :%d _
+  :setlocal buftype=nofile
+  :setlocal bufhidden=delete
+  :setlocal noswapfile nobuflisted
+  :setlocal nonumber norelativenumber fdc=0 nowrap winfixheight winfixwidth
+  setline(1, ['# Language Server Symbols', '# ' .. fname])
+  # First two lines in the buffer display comment information
+  var lnumMap: list<dict<number>> = [{}, {}]
+  var text: list<string> = []
+  for [symType, syms] in items(symbols)
+    text->extend(['', symType])
+    lnumMap->extend([{}, {}])
+    for s in syms
+      text->add('  ' .. s.name)
+      lnumMap->add({'lnum': s.lnum, 'col': s.col})
+    endfor
+  endfor
+  append(line('$'), text)
+  w:lsp_info = {'filename': fname, 'data': lnumMap}
+  :nnoremap <silent> <buffer> q :quit<CR>
+  :nnoremap <silent> <buffer> <CR> :call handlers#jumpToSymbol()<CR>
+  :setlocal nomodifiable
+enddef
+
+# process the 'textDocument/documentSymbol' reply from the LSP server
+# Open a symbols window and display the symbols as a tree
+def s:processDocSymbolReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
+  if reply.result->empty()
+    WarnMsg('No symbols are found')
+    return
+  endif
+
+  s:showSymbols(reply.result)
+enddef
+
+# Returns the byte number of the specified line/col position.  Returns a
+# zero-indexed column.  'pos' is LSP "interface position".
+def s:get_line_byte_from_position(bnr: number, pos: dict<number>): number
+  # LSP's line and characters are 0-indexed
+  # Vim's line and columns are 1-indexed
+  var col: number = pos.character
+  # When on the first character, we can ignore the difference between byte and
+  # character
+  if col > 0
+    if !bnr->bufloaded()
+      bnr->bufload()
+    endif
+
+    var ltext: list<string> = bnr->getbufline(pos.line + 1)
+    if !ltext->empty()
+      var bidx = ltext[0]->byteidx(col)
+      if bidx != -1
+       return bidx
+      endif
+    endif
+  endif
+
+  return col
+enddef
+
+# sort the list of edit operations in the descending order of line and column
+# numbers.
+# 'a': {'A': [lnum, col], 'B': [lnum, col]}
+# 'b': {'A': [lnum, col], 'B': [lnum, col]}
+def s:edit_sort_func(a: dict<any>, b: dict<any>): number
+  # line number
+  if a.A[0] != b.A[0]
+    return b.A[0] - a.A[0]
+  endif
+  # column number
+  if a.A[1] != b.A[1]
+    return b.A[1] - a.A[1]
+  endif
+
+  return 0
+enddef
+
+# Replaces text in a range with new text.
+#
+# CAUTION: Changes in-place!
+#
+# 'lines': Original list of strings
+# 'A': Start position; [line, col]
+# 'B': End position [line, col]
+# 'new_lines' A list of strings to replace the original
+#
+# returns the modified 'lines'
+def s:set_lines(lines: list<string>, A: list<number>, B: list<number>,
+                                       new_lines: list<string>): list<string>
+  var i_0: number = A[0]
+
+  # If it extends past the end, truncate it to the end. This is because the
+  # way the LSP describes the range including the last newline is by
+  # specifying a line number after what we would call the last line.
+  var numlines: number = lines->len()
+  var i_n = [B[0], numlines - 1]->min()
+
+  if i_0 < 0 || i_0 >= numlines || i_n < 0 || i_n >= numlines
+    WarnMsg("set_lines: Invalid range, A = " .. string(A)
+               .. ", B = " ..  string(B) .. ", numlines = " .. numlines
+               .. ", new lines = " .. string(new_lines))
+    return lines
+  endif
+
+  # save the prefix and suffix text before doing the replacements
+  var prefix: string = ''
+  var suffix: string = lines[i_n][B[1] :]
+  if A[1] > 0
+    prefix = lines[i_0][0 : A[1] - 1]
+  endif
+
+  var new_lines_len: number = new_lines->len()
+
+  #echomsg 'i_0 = ' .. i_0 .. ', i_n = ' .. i_n .. ', new_lines = ' .. string(new_lines)
+  var n: number = i_n - i_0 + 1
+  if n != new_lines_len
+    if n > new_lines_len
+      # remove the deleted lines
+      lines->remove(i_0, i_0 + n - new_lines_len - 1)
+    else
+      # add empty lines for newly the added lines (will be replaced with the
+      # actual lines below)
+      lines->extend(repeat([''], new_lines_len - n), i_0)
+    endif
+  endif
+  #echomsg "lines(1) = " .. string(lines)
+
+  # replace the previous lines with the new lines
+  for i in range(new_lines_len)
+    lines[i_0 + i] = new_lines[i]
+  endfor
+  #echomsg "lines(2) = " .. string(lines)
+
+  # append the suffix (if any) to the last line
+  if suffix != ''
+    var i = i_0 + new_lines_len - 1
+    lines[i] = lines[i] .. suffix
+  endif
+  #echomsg "lines(3) = " .. string(lines)
+
+  # prepend the prefix (if any) to the first line
+  if prefix != ''
+    lines[i_0] = prefix .. lines[i_0]
+  endif
+  #echomsg "lines(4) = " .. string(lines)
+
+  return lines
+enddef
+
+# Apply set of text edits to the specified buffer
+# The text edit logic is ported from the Neovim lua implementation
+def s:applyTextEdits(bnr: number, text_edits: list<dict<any>>): void
+  if text_edits->empty()
+    return
+  endif
+
+  # if the buffer is not loaded, load it and make it a listed buffer
+  if !bnr->bufloaded()
+    bnr->bufload()
+  endif
+  bnr->setbufvar('&buflisted', v:true)
+
+  var start_line: number = 4294967295           # 2 ^ 32
+  var finish_line: number = -1
+  var updated_edits: list<dict<any>> = []
+  var start_row: number
+  var start_col: number
+  var end_row: number
+  var end_col: number
+
+  # create a list of buffer positions where the edits have to be applied.
+  for e in text_edits
+    # Adjust the start and end columns for multibyte characters
+    start_row = e.range.start.line
+    start_col = s:get_line_byte_from_position(bnr, e.range.start)
+    end_row = e.range.end.line
+    end_col = s:get_line_byte_from_position(bnr, e.range.end)
+    start_line = [e.range.start.line, start_line]->min()
+    finish_line = [e.range.end.line, finish_line]->max()
+
+    updated_edits->add({'A': [start_row, start_col],
+                       'B': [end_row, end_col],
+                       'lines': e.newText->split("\n", v:true)})
+  endfor
+
+  # Reverse sort the edit operations by descending line and column numbers so
+  # that they can be applied without interfering with each other.
+  updated_edits->sort('s:edit_sort_func')
+
+  var lines: list<string> = bnr->getbufline(start_line + 1, finish_line + 1)
+  var fix_eol: number = bnr->getbufvar('&fixeol')
+  var set_eol = fix_eol && bnr->getbufinfo()[0].linecount <= finish_line + 1
+  if set_eol && lines[-1]->len() != 0
+    lines->add('')
+  endif
+
+  #echomsg 'lines(1) = ' .. string(lines)
+  #echomsg updated_edits
+
+  for e in updated_edits
+    var A: list<number> = [e.A[0] - start_line, e.A[1]]
+    var B: list<number> = [e.B[0] - start_line, e.B[1]]
+    lines = s:set_lines(lines, A, B, e.lines)
+  endfor
+
+  #echomsg 'lines(2) = ' .. string(lines)
+
+  # If the last line is empty and we need to set EOL, then remove it.
+  if set_eol && lines[-1]->len() == 0
+    lines->remove(-1)
+  endif
+
+  #echomsg 'applyTextEdits: start_line = ' .. start_line .. ', finish_line = ' .. finish_line
+  #echomsg 'lines = ' .. string(lines)
+
+  # Delete all the lines that need to be modified
+  bnr->deletebufline(start_line + 1, finish_line + 1)
+
+  # if the buffer is empty, appending lines before the first line adds an
+  # extra empty line at the end. Delete the empty line after appending the
+  # lines.
+  var dellastline: bool = v:false
+  if start_line == 0 && bnr->getbufinfo()[0].linecount == 1 &&
+                                    bnr->getbufline(1)[0] == ''
+    dellastline = v:true
+  endif
+
+  # Append the updated lines
+  appendbufline(bnr, start_line, lines)
+
+  if dellastline
+    bnr->deletebufline(bnr->getbufinfo()[0].linecount)
+  endif
+enddef
+
+# interface TextDocumentEdit
+def s:applyTextDocumentEdit(textDocEdit: dict<any>)
+  var bnr: number = bufnr(LspUriToFile(textDocEdit.textDocument.uri))
+  if bnr == -1
+    ErrMsg('Error: Text Document edit, buffer ' .. textDocEdit.textDocument.uri .. ' is not found')
+    return
+  endif
+  s:applyTextEdits(bnr, textDocEdit.edits)
+enddef
+
+# interface WorkspaceEdit
+def s:applyWorkspaceEdit(workspaceEdit: dict<any>)
+  if workspaceEdit->has_key('documentChanges')
+    for change in workspaceEdit.documentChanges
+      if change->has_key('kind')
+       ErrMsg('Error: Unsupported change in workspace edit [' .. change.kind .. ']')
+      else
+       s:applyTextDocumentEdit(change)
+      endif
+    endfor
+    return
+  endif
+
+  if !workspaceEdit->has_key('changes')
+    return
+  endif
+
+  var save_cursor: list<number> = getcurpos()
+  for [uri, changes] in items(workspaceEdit.changes)
+    var fname: string = LspUriToFile(uri)
+    var bnr: number = bufnr(fname)
+    if bnr == -1
+      # file is already removed
+      continue
+    endif
+
+    # interface TextEdit
+    s:applyTextEdits(bnr, changes)
+  endfor
+  save_cursor->setpos('.')
+enddef
+
+# process the 'textDocument/formatting' reply from the LSP server
+def s:processFormatReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>)
+  if reply.result->empty()
+    # nothing to format
+    return
+  endif
+
+  # result: TextEdit[]
+
+  var fname: string = LspUriToFile(req.params.textDocument.uri)
+  var bnr: number = bufnr(fname)
+  if bnr == -1
+    # file is already removed
+    return
+  endif
+
+  # interface TextEdit
+  # Apply each of the text edit operations
+  var save_cursor: list<number> = getcurpos()
+  s:applyTextEdits(bnr, reply.result)
+  save_cursor->setpos('.')
+enddef
+
+# process the 'textDocument/rename' reply from the LSP server
+def s:processRenameReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>)
+  if reply.result->empty()
+    # nothing to rename
+    return
+  endif
+
+  # result: WorkspaceEdit
+  s:applyWorkspaceEdit(reply.result)
+enddef
+
+# interface ExecuteCommandParams
+def s:executeCommand(lspserver: dict<any>, cmd: dict<any>)
+  var req = lspserver.createRequest('workspace/executeCommand')
+  req.params->extend(cmd)
+  lspserver.sendMessage(req)
+enddef
+
+# process the 'textDocument/codeAction' reply from the LSP server
+# params: interface Command[] | interface CodeAction[]
+def s:processCodeActionReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>)
+  if reply.result->empty()
+    # no action can be performed
+    WarnMsg('No code action is available')
+    return
+  endif
+
+  var actions: list<dict<any>> = reply.result
+
+  var prompt: list<string> = ['Code Actions:']
+  var act: dict<any>
+  for i in range(actions->len())
+    act = actions[i]
+    var t: string = act.title->substitute('\r\n', '\\r\\n', 'g')
+    t = t->substitute('\n', '\\n', 'g')
+    prompt->add(printf("%d. %s", i + 1, t))
+  endfor
+  var choice = inputlist(prompt)
+  if choice < 1 || choice > prompt->len()
+    return
+  endif
+
+  var selAction = actions[choice - 1]
+
+  # textDocument/codeAction can return either Command[] or CodeAction[].
+  # If it is a CodeAction, it can have either an edit, a command or both.
+  # Edits should be executed first.
+  if selAction->has_key('edit') || selAction->has_key('command')
+    if selAction->has_key('edit')
+      # apply edit first
+      s:applyWorkspaceEdit(selAction.edit)
+    endif
+    if selAction->has_key('command')
+      s:executeCommand(lspserver, selAction)
+    endif
+  else
+    s:executeCommand(lspserver, selAction)
+  endif
+enddef
+
+# process the 'workspace/executeCommand' reply from the LSP server
+def s:processWorkspaceExecuteReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>)
+  if reply.result->empty()
+    return
+  endif
+
+  # Nothing to do for the reply
+enddef
+
+# process the 'workspace/symbol' reply from the LSP server
+def s:processWorkspaceSymbolReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>)
+  if reply.result->empty()
+    WarnMsg('Error: Symbol not found')
+    return
+  endif
+
+  s:showSymbols(reply.result)
+enddef
+
+# Process various reply messages from the LSP server
+export def ProcessReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
+  var lsp_reply_handlers: dict<func> =
+    {
+      'initialize': function('s:processInitializeReply'),
+      'textDocument/definition': function('s:processDefDeclReply'),
+      'textDocument/declaration': function('s:processDefDeclReply'),
+      'textDocument/typeDefinition': function('s:processDefDeclReply'),
+      'textDocument/implementation': function('s:processDefDeclReply'),
+      'textDocument/signatureHelp': function('s:processSignaturehelpReply'),
+      'textDocument/completion': function('s:processCompletionReply'),
+      'textDocument/hover': function('s:processHoverReply'),
+      'textDocument/references': function('s:processReferencesReply'),
+      'textDocument/documentHighlight': function('s:processDocHighlightReply'),
+      'textDocument/documentSymbol': function('s:processDocSymbolReply'),
+      'textDocument/formatting': function('s:processFormatReply'),
+      'textDocument/rangeFormatting': function('s:processFormatReply'),
+      'textDocument/rename': function('s:processRenameReply'),
+      'textDocument/codeAction': function('s:processCodeActionReply'),
+      'workspace/executeCommand': function('s:processWorkspaceExecuteReply'),
+      'workspace/symbol': function('s:processWorkspaceSymbolReply')
+    }
+
+  if lsp_reply_handlers->has_key(req.method)
+    lsp_reply_handlers[req.method](lspserver, req, reply)
+  else
+    ErrMsg("Error: Unsupported reply received from LSP server: " .. string(reply))
+  endif
+enddef
+
+# process a diagnostic notification message from the LSP server
+# params: interface PublishDiagnosticsParams
+def s:processDiagNotif(lspserver: dict<any>, reply: dict<any>): void
+  var fname: string = LspUriToFile(reply.params.uri)
+
+  # store the diagnostic for each line separately
+  var diag_by_lnum: dict<dict<any>> = {}
+  for diag in reply.params.diagnostics
+    diag_by_lnum[diag.range.start.line + 1] = diag
+  endfor
+
+  lspserver.diagsMap->extend({[fname]: diag_by_lnum})
+enddef
+
+# process a log notification message from the LSP server
+def s:processLogMsgNotif(lspserver: dict<any>, reply: dict<any>)
+  # interface LogMessageParams
+  var msgType: list<string> = ['', 'Error: ', 'Warning: ', 'Info: ', 'Log: ']
+  if reply.params.type == 4
+    # ignore log messages from the LSP server (too chatty)
+    # TODO: Add a configuration to control the message level that will be
+    # displayed. Also store these messages and provide a command to display
+    # them.
+    return
+  endif
+  var mtype: string = 'Log: '
+  if reply.params.type > 0 && reply.params.type < 5
+    mtype = msgType[reply.params.type]
+  endif
+
+  :echomsg 'Lsp ' .. mtype .. reply.params.message
+enddef
+
+# process notification messages from the LSP server
+export def ProcessNotif(lspserver: dict<any>, reply: dict<any>): void
+  var lsp_notif_handlers: dict<func> =
+    {
+      'textDocument/publishDiagnostics': function('s:processDiagNotif'),
+      'window/showMessage': function('s:processLogMsgNotif'),
+      'window/logMessage': function('s:processLogMsgNotif')
+    }
+
+  if lsp_notif_handlers->has_key(reply.method)
+    lsp_notif_handlers[reply.method](lspserver, reply)
+  else
+    ErrMsg('Error: Unsupported notification received from LSP server ' .. string(reply))
+  endif
+enddef
+
+# process the workspace/applyEdit LSP server request
+def s:processApplyEditReq(lspserver: dict<any>, request: dict<any>)
+  # interface ApplyWorkspaceEditParams
+  if !request->has_key('params')
+    return
+  endif
+  var workspaceEditParams: dict<any> = request.params
+  if workspaceEditParams->has_key('label')
+    :echomsg "Workspace edit" .. workspaceEditParams.label
+  endif
+  s:applyWorkspaceEdit(workspaceEditParams.edit)
+  # TODO: Need to return the proper result of the edit operation
+  lspserver.sendResponse(request, {'applied': v:true}, v:null)
+enddef
+
+# process a request message from the server
+export def ProcessRequest(lspserver: dict<any>, request: dict<any>)
+  var lspRequestHandlers: dict<func> =
+    {
+      'workspace/applyEdit': function('s:processApplyEditReq')
+    }
+
+  if lspRequestHandlers->has_key(request.method)
+    lspRequestHandlers[request.method](lspserver, request)
+  else
+    ErrMsg('Error: Unsupported request received from LSP server ' ..
+                                                       string(request))
+  endif
+enddef
+
+# process LSP server messages
+export def ProcessMessages(lspserver: dict<any>): void
+  while lspserver.data->len() > 0
+    var idx = stridx(lspserver.data, 'Content-Length: ')
+    if idx == -1
+      return
+    endif
+
+    var len = str2nr(lspserver.data[idx + 16:])
+    if len == 0
+      ErrMsg("Error: Content length is zero")
+      return
+    endif
+
+    # Header and contents are separated by '\r\n\r\n'
+    idx = stridx(lspserver.data, "\r\n\r\n")
+    if idx == -1
+      ErrMsg("Error: Content separator is not found")
+      return
+    endif
+
+    idx = idx + 4
+
+    if lspserver.data->len() - idx < len
+      # message is not fully received. Process the message after more data is
+      # received
+      return
+    endif
+
+    var content = lspserver.data[idx : idx + len - 1]
+    var msg = content->json_decode()
+
+    if msg->has_key('result') || msg->has_key('error')
+      # response message from the server
+      var req = lspserver.requests->get(string(msg.id))
+      # Remove the corresponding stored request message
+      lspserver.requests->remove(string(msg.id))
+
+      if msg->has_key('result')
+        lspserver.processReply(req, msg)
+      else
+       # request failed
+        var emsg: string = msg.error.message
+       emsg ..= ', code = ' .. msg.code
+        if msg.error->has_key('data')
+          emsg = emsg .. ', data = ' .. string(msg.error.data)
+        endif
+        ErrMsg("Error: request " .. req.method .. " failed (" .. emsg .. ")")
+      endif
+    elseif msg->has_key('id')
+      # request message from the server
+      lspserver.processRequest(msg)
+    else
+      # notification message from the server
+      lspserver.processNotif(msg)
+    endif
+
+    lspserver.data = lspserver.data[idx + len :]
+  endwhile
+enddef
+
index c6f3eafd0e55762d11a84ad19f42996bb22416de..e93007f8abab455b4cda26b8451ee4cd2aea89d1 100644 (file)
@@ -2,1340 +2,63 @@ vim9script
 
 # Vim9 LSP client
 
-# Needs Vim 8.2.2082 and higher
-if v:version < 802 || !has('patch-8.2.2082')
-  finish
-endif
-
-# LSP server information
-var lspServers: list<dict<any>> = []
-
-# filetype to LSP server map
-var ftypeServerMap: dict<dict<any>> = {}
-
-# Buffer number to LSP server map
-var bufnrToServer: dict<dict<any>> = {}
-
-# List of diagnostics for each opened file
-var diagsMap: dict<dict<any>> = {}
-
-prop_type_add('LspTextRef', {'highlight': 'Search'})
-prop_type_add('LspReadRef', {'highlight': 'DiffChange'})
-prop_type_add('LspWriteRef', {'highlight': 'DiffDelete'})
-
-# Display a warning message
-def WarnMsg(msg: string)
-  :echohl WarningMsg
-  :echomsg msg
-  :echohl None
-enddef
-
-# Display an error message
-def ErrMsg(msg: string)
-  :echohl Error
-  :echomsg msg
-  :echohl None
-enddef
-
-# Return the LSP server for the a specific filetype. Returns a null dict if
-# the server is not found.
-def s:lspGetServer(ftype: string): dict<any>
-  return ftypeServerMap->get(ftype, {})
-enddef
-
-# Add a LSP server for a filetype
-def s:lspAddServer(ftype: string, lspserver: dict<any>)
-  ftypeServerMap->extend({[ftype]: lspserver})
-enddef
-
-# Lsp server trace log directory
-var lsp_log_dir: string
-if has('unix')
-  lsp_log_dir = '/tmp/'
-else
-  lsp_log_dir = $TEMP .. '\\'
-endif
-var lsp_server_trace: bool = v:false
-
-def lsp#enableServerTrace()
-  lsp_server_trace = v:true
-enddef
-
-# Log a message from the LSP server. stderr is v:true for logging messages
-# from the standard error and v:false for stdout.
-def s:traceLog(stderr: bool, msg: string)
-  if !lsp_server_trace
-    return
-  endif
-  if stderr
-    writefile(split(msg, "\n"), lsp_log_dir .. 'lsp_server.err', 'a')
-  else
-    writefile(split(msg, "\n"), lsp_log_dir .. 'lsp_server.out', 'a')
-  endif
-enddef
-
-# Empty out the LSP server trace logs
-def s:clearTraceLogs()
-  if !lsp_server_trace
-    return
-  endif
-  writefile([], lsp_log_dir .. 'lsp_server.out')
-  writefile([], lsp_log_dir .. 'lsp_server.err')
-enddef
-
-# Show information about all the LSP servers
-def lsp#showServers()
-  for [ftype, lspserver] in items(ftypeServerMap)
-    var msg = ftype .. "    "
-    if lspserver.running
-      msg ..= 'running'
-    else
-      msg ..= 'not running'
-    endif
-    msg ..= '    ' .. lspserver.path
-    :echomsg msg
-  endfor
-enddef
-
-# Convert a LSP file URI (file://<absolute_path>) to a Vim file name
-def LspUriToFile(uri: string): string
-  # Replace all the %xx numbers (e.g. %20 for space) in the URI to character
-  var uri_decoded: string = substitute(uri, '%\(\x\x\)',
-                             '\=nr2char(str2nr(submatch(1), 16))', 'g')
-
-  # File URIs on MS-Windows start with file:///[a-zA-Z]:'
-  if uri_decoded =~? '^file:///\a:'
-    # MS-Windows URI
-    uri_decoded = uri_decoded[8:]
-    uri_decoded = uri_decoded->substitute('/', '\\', 'g')
-  else
-    uri_decoded = uri_decoded[7:]
-  endif
-
-  return uri_decoded
-enddef
-
-# Convert a Vim filenmae to an LSP URI (file://<absolute_path>)
-def LspFileToUri(fname: string): string
-  var uri: string = fnamemodify(fname, ':p')
-
-  var on_windows: bool = v:false
-  if uri =~? '^\a:'
-    on_windows = v:true
-  endif
-
-  if on_windows
-    # MS-Windows
-    uri = uri->substitute('\\', '/', 'g')
-  endif
-
-  uri = uri->substitute('\([^A-Za-z0-9-._~:/]\)',
-                     '\=printf("%%%02x", char2nr(submatch(1)))', 'g')
-
-  if on_windows
-    uri = 'file:///' .. uri
-  else
-    uri = 'file://' .. uri
-  endif
-
-  return uri
-enddef
-
-# process the 'initialize' method reply from the LSP server
-def s:processInitializeReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
-  if reply.result->len() <= 0
-    return
-  endif
-
-  # interface 'InitializeResult'
-  var caps: dict<any> = reply.result.capabilities
-  lspserver.caps = caps
-
-  # TODO: Check all the buffers with filetype corresponding to this LSP server
-  # and then setup the below mapping for those buffers.
-
-  # map characters that trigger signature help
-  if caps->has_key('signatureHelpProvider')
-    var triggers = caps.signatureHelpProvider.triggerCharacters
-    for ch in triggers
-      exe 'inoremap <buffer> <silent> ' .. ch .. ' ' .. ch .. "<C-R>=lsp#showSignature()<CR>"
-    endfor
-  endif
-
-  # map characters that trigger insert mode completion
-  if caps->has_key('completionProvider')
-    var triggers = caps.completionProvider.triggerCharacters
-    for ch in triggers
-      exe 'inoremap <buffer> <silent> ' .. ch .. ' ' .. ch .. "<C-X><C-U>"
-    endfor
-  endif
-
-  # send a "initialized" notification to server
-  lspserver.sendInitializedNotif()
-enddef
-
-# process the 'textDocument/definition' / 'textDocument/declaration' /
-# 'textDocument/typeDefinition' and 'textDocument/implementation' replies from
-# the LSP server
-def s:processDefDeclReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
-  if reply.result->empty()
-    WarnMsg("Error: definition is not found")
-    # pop the tag stack
-    var tagstack: dict<any> = gettagstack()
-    if tagstack.length > 0
-      settagstack(winnr(), {'curidx': tagstack.length}, 't')
-    endif
-    return
-  endif
-
-  var result: dict<any> = reply.result[0]
-  var file = LspUriToFile(result.uri)
-  var wid = bufwinid(file)
-  if wid != -1
-    win_gotoid(wid)
-  else
-    exe 'split ' .. file
-  endif
-  # Set the previous cursor location mark
-  setpos("'`", getcurpos())
-  cursor(result.range.start.line + 1, result.range.start.character + 1)
-  redraw!
-enddef
-
-# process the 'textDocument/signatureHelp' reply from the LSP server
-def s:processSignaturehelpReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
-  var result: dict<any> = reply.result
-  if result.signatures->len() <= 0
-    WarnMsg('No signature help available')
-    return
-  endif
-
-  var sig: dict<any> = result.signatures[result.activeSignature]
-  var text = sig.label
-  var hllen = 0
-  var startcol = 0
-  if sig->has_key('parameters')
-    var params_len = sig.parameters->len()
-    if params_len > 0 && result.activeParameter < params_len
-      var label = sig.parameters[result.activeParameter].label
-      hllen = label->len()
-      startcol = text->stridx(label)
-    endif
-  endif
-  var popupID = popup_atcursor(text, {})
-  prop_type_add('signature', {'bufnr': popupID->winbufnr(), 'highlight': 'Title'})
-  if hllen > 0
-    prop_add(1, startcol + 1, {'bufnr': popupID->winbufnr(), 'length': hllen, 'type': 'signature'})
-  endif
-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
-
-# process the 'textDocument/completion' reply from the LSP server
-def s:processCompletionReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
-
-  var items: list<dict<any>>
-  if type(reply.result) == v:t_list
-    items = reply.result
-  else
-    items = reply.result.items
-  endif
-
-  for item in items
-    var d: dict<any> = {}
-    if item->has_key('insertText')
-      d.word = item.insertText
-    elseif item->has_key('textEdit')
-      d.word = item.textEdit.newText
-    else
-      d.word = item.label
-    endif
-    if item->has_key('kind')
-      # namespace CompletionItemKind
-      # map LSP kind to complete-item-kind
-      d.kind = LspCompleteItemKindChar(item.kind)
-    endif
-    if item->has_key('detail')
-      d.menu = item.detail
-    endif
-    if item->has_key('documentation')
-      d.info = item.documentation
-    endif
-    lspserver.completeItems->add(d)
-  endfor
-
-  lspserver.completePending = v:false
-enddef
-
-# process the 'textDocument/hover' reply from the LSP server
-def s:processHoverReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
-  if type(reply.result) == v:t_none
-    return
-  endif
-
-  var hoverText: list<string>
-
-  if type(reply.result.contents) == v:t_dict
-    if reply.result.contents->has_key('kind')
-      # MarkupContent
-      if reply.result.contents.kind == 'plaintext'
-        hoverText = reply.result.contents.value->split("\n")
-      elseif reply.result.contents.kind == 'markdown'
-        hoverText = reply.result.contents.value->split("\n")
-      else
-        ErrMsg('Error: Unsupported hover contents type (' .. reply.result.contents.kind .. ')')
-        return
-      endif
-    elseif reply.result.contents->has_key('value')
-      # MarkedString
-      hoverText = reply.result.contents.value
-    else
-      ErrMsg('Error: Unsupported hover contents (' .. reply.result.contents .. ')')
-      return
-    endif
-  elseif type(reply.result.contents) == v:t_list
-    # interface MarkedString[]
-    for e in reply.result.contents
-      if type(e) == v:t_string
-        hoverText->extend(e->split("\n"))
-      else
-        hoverText->extend(e.value->split("\n"))
-      endif
-    endfor
-  elseif type(reply.result.contents) == v:t_string
-    if reply.result.contents->empty()
-      return
-    endif
-    hoverText->add(reply.result.contents)
-  else
-    ErrMsg('Error: Unsupported hover contents (' .. reply.result.contents .. ')')
-    return
-  endif
-  hoverText->popup_atcursor({'moved': 'word'})
-enddef
-
-# process the 'textDocument/references' reply from the LSP server
-def s:processReferencesReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
-  if type(reply.result) == v:t_none || reply.result->empty()
-    WarnMsg('Error: No references found')
-    return
-  endif
-
-  # create a quickfix list with the location of the references
-  var locations: list<dict<any>> = reply.result
-  var qflist: list<dict<any>> = []
-  for loc in locations
-    var fname: string = LspUriToFile(loc.uri)
-    var bnr: number = fname->bufnr()
-    if bnr == -1
-      bnr = fname->bufadd()
-      bnr->bufload()
-    endif
-    var text: string = bnr->getbufline(loc.range.start.line + 1)[0]
-                                               ->trim("\t ", 1)
-    qflist->add({'filename': fname,
-                    'lnum': loc.range.start.line + 1,
-                    'col': loc.range.start.character + 1,
-                    'text': text})
-  endfor
-  setqflist([], ' ', {'title': 'Language Server', 'items': qflist})
-  var save_winid = win_getid()
-  copen
-  win_gotoid(save_winid)
-enddef
-
-# process the 'textDocument/documentHighlight' reply from the LSP server
-def s:processDocHighlightReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
-  if reply.result->empty()
-    return
-  endif
-
-  var fname: string = LspUriToFile(req.params.textDocument.uri)
-  var bnr = bufnr(fname)
-
-  for docHL in reply.result
-    var kind: number = docHL->get('kind', 1)
-    var propName: string
-    if kind == 2
-      # Read-access
-      propName = 'LspReadRef'
-    elseif kind == 3
-      # Write-access
-      propName = 'LspWriteRef'
-    else
-      # textual reference
-      propName = 'LspTextRef'
-    endif
-    prop_add(docHL.range.start.line + 1, docHL.range.start.character + 1,
-               {'end_lnum': docHL.range.end.line + 1,
-                'end_col': docHL.range.end.character + 1,
-                'bufnr': bnr,
-                'type': propName})
-  endfor
-enddef
-
-# map the LSP symbol kind number to string
-def LspSymbolKindToName(symkind: number): string
-  var symbolMap: list<string> = ['', 'File', 'Module', 'Namespace', 'Package',
-       'Class', 'Method', 'Property', 'Field', 'Constructor', 'Enum',
-        'Interface', 'Function', 'Variable', 'Constant', 'String', 'Number',
-        'Boolean', 'Array', 'Object', 'Key', 'Null', 'EnumMember', 'Struct',
-        'Event', 'Operator', 'TypeParameter']
-  if symkind > 26
-    return ''
-  endif
-  return symbolMap[symkind]
-enddef
-
-# jump to a symbol selected in the symbols window
-def lsp#jumpToSymbol()
-  var lnum: number = line('.') - 1
-  if w:lsp_info.data[lnum]->empty()
-    return
-  endif
-
-  var slnum: number = w:lsp_info.data[lnum].lnum
-  var scol: number = w:lsp_info.data[lnum].col
-  var fname: string = w:lsp_info.filename
-
-  # If the file is already opened in a window, jump to it. Otherwise open it
-  # in another window
-  var wid: number = fname->bufwinid()
-  if wid == -1
-    # Find a window showing a normal buffer and use it
-    for w in getwininfo()
-      if w.winid->getwinvar('&buftype') == ''
-       wid = w.winid
-       wid->win_gotoid()
-       break
-      endif
-    endfor
-    if wid == -1
-      var symWinid: number = win_getid()
-      :rightbelow vnew
-      # retain the fixed symbol window width
-      win_execute(symWinid, 'vertical resize 20')
-    endif
-
-    exe 'edit ' .. fname
-  else
-    wid->win_gotoid()
-  endif
-  [slnum, scol]->cursor()
-enddef
-
-# display the list of document symbols from the LSP server in a window as a
-# tree
-def s:showSymbols(symTable: list<dict<any>>)
-  var symbols: dict<list<dict<any>>>
-  var symbolType: string
-  var fname: string
-
-  for symbol in symTable
-    if symbol->has_key('location')
-      fname = LspUriToFile(symbol.location.uri)
-      symbolType = LspSymbolKindToName(symbol.kind)
-      if !symbols->has_key(symbolType)
-       symbols[symbolType] = []
-      endif
-      var name: string = symbol.name
-      if symbol->has_key('containerName')
-       if symbol.containerName != ''
-         name ..= ' [' .. symbol.containerName .. ']'
-       endif
-      endif
-      symbols[symbolType]->add({'name': name,
-                       'lnum': symbol.location.range.start.line + 1,
-                        'col': symbol.location.range.start.character + 1})
-    endif
-  endfor
-
-  var wid: number = bufwinid('LSP-Symbols')
-  if wid == -1
-    :20vnew LSP-Symbols
-  else
-    win_gotoid(wid)
-  endif
-
-  :setlocal modifiable
-  :setlocal noreadonly
-  :silent! :%d _
-  :setlocal buftype=nofile
-  :setlocal bufhidden=delete
-  :setlocal noswapfile nobuflisted
-  :setlocal nonumber norelativenumber fdc=0 nowrap winfixheight winfixwidth
-  setline(1, ['# Language Server Symbols', '# ' .. fname])
-  # First two lines in the buffer display comment information
-  var lnumMap: list<dict<number>> = [{}, {}]
-  var text: list<string> = []
-  for [symType, syms] in items(symbols)
-    text->extend(['', symType])
-    lnumMap->extend([{}, {}])
-    for s in syms
-      text->add('  ' .. s.name)
-      lnumMap->add({'lnum': s.lnum, 'col': s.col})
-    endfor
-  endfor
-  append(line('$'), text)
-  w:lsp_info = {'filename': fname, 'data': lnumMap}
-  :nnoremap <silent> <buffer> q :quit<CR>
-  :nnoremap <silent> <buffer> <CR> :call lsp#jumpToSymbol()<CR>
-  :setlocal nomodifiable
-enddef
-
-# process the 'textDocument/documentSymbol' reply from the LSP server
-# Open a symbols window and display the symbols as a tree
-def s:processDocSymbolReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
-  if reply.result->empty()
-    WarnMsg('No symbols are found')
-    return
-  endif
-
-  s:showSymbols(reply.result)
-enddef
-
-# Returns the byte number of the specified line/col position.  Returns a
-# zero-indexed column.  'pos' is LSP "interface position".
-def s:get_line_byte_from_position(bnr: number, pos: dict<number>): number
-  # LSP's line and characters are 0-indexed
-  # Vim's line and columns are 1-indexed
-  var col: number = pos.character
-  # When on the first character, we can ignore the difference between byte and
-  # character
-  if col > 0
-    if !bnr->bufloaded()
-      bnr->bufload()
-    endif
-
-    var ltext: list<string> = bnr->getbufline(pos.line + 1)
-    if !ltext->empty()
-      var bidx = ltext[0]->byteidx(col)
-      if bidx != -1
-       return bidx
-      endif
-    endif
-  endif
-
-  return col
-enddef
-
-# sort the list of edit operations in the descending order of line and column
-# numbers.
-# 'a': {'A': [lnum, col], 'B': [lnum, col]}
-# 'b': {'A': [lnum, col], 'B': [lnum, col]}
-def s:edit_sort_func(a: dict<any>, b: dict<any>): number
-  # line number
-  if a.A[0] != b.A[0]
-    return b.A[0] - a.A[0]
-  endif
-  # column number
-  if a.A[1] != b.A[1]
-    return b.A[1] - a.A[1]
-  endif
-
-  return 0
-enddef
-
-# Replaces text in a range with new text.
-#
-# CAUTION: Changes in-place!
-#
-# 'lines': Original list of strings
-# 'A': Start position; [line, col]
-# 'B': End position [line, col]
-# 'new_lines' A list of strings to replace the original
-#
-# returns the modified 'lines'
-def s:set_lines(lines: list<string>, A: list<number>, B: list<number>,
-                                       new_lines: list<string>): list<string>
-  var i_0: number = A[0]
-
-  # If it extends past the end, truncate it to the end. This is because the
-  # way the LSP describes the range including the last newline is by
-  # specifying a line number after what we would call the last line.
-  var numlines: number = lines->len()
-  var i_n = [B[0], numlines - 1]->min()
-
-  if i_0 < 0 || i_0 >= numlines || i_n < 0 || i_n >= numlines
-    WarnMsg("set_lines: Invalid range, A = " .. string(A)
-               .. ", B = " ..  string(B) .. ", numlines = " .. numlines
-               .. ", new lines = " .. string(new_lines))
-    return lines
-  endif
-
-  # save the prefix and suffix text before doing the replacements
-  var prefix: string = ''
-  var suffix: string = lines[i_n][B[1] :]
-  if A[1] > 0
-    prefix = lines[i_0][0 : A[1] - 1]
-  endif
-
-  var new_lines_len: number = new_lines->len()
-
-  #echomsg 'i_0 = ' .. i_0 .. ', i_n = ' .. i_n .. ', new_lines = ' .. string(new_lines)
-  var n: number = i_n - i_0 + 1
-  if n != new_lines_len
-    if n > new_lines_len
-      # remove the deleted lines
-      lines->remove(i_0, i_0 + n - new_lines_len - 1)
-    else
-      # add empty lines for newly the added lines (will be replaced with the
-      # actual lines below)
-      lines->extend(repeat([''], new_lines_len - n), i_0)
-    endif
-  endif
-  #echomsg "lines(1) = " .. string(lines)
-
-  # replace the previous lines with the new lines
-  for i in range(new_lines_len)
-    lines[i_0 + i] = new_lines[i]
-  endfor
-  #echomsg "lines(2) = " .. string(lines)
-
-  # append the suffix (if any) to the last line
-  if suffix != ''
-    var i = i_0 + new_lines_len - 1
-    lines[i] = lines[i] .. suffix
-  endif
-  #echomsg "lines(3) = " .. string(lines)
-
-  # prepend the prefix (if any) to the first line
-  if prefix != ''
-    lines[i_0] = prefix .. lines[i_0]
-  endif
-  #echomsg "lines(4) = " .. string(lines)
-
-  return lines
-enddef
-
-# Apply set of text edits to the specified buffer
-# The text edit logic is ported from the Neovim lua implementation
-def s:applyTextEdits(bnr: number, text_edits: list<dict<any>>): void
-  if text_edits->empty()
-    return
-  endif
-
-  # if the buffer is not loaded, load it and make it a listed buffer
-  if !bnr->bufloaded()
-    bnr->bufload()
-  endif
-  bnr->setbufvar('&buflisted', v:true)
-
-  var start_line: number = 4294967295           # 2 ^ 32
-  var finish_line: number = -1
-  var updated_edits: list<dict<any>> = []
-  var start_row: number
-  var start_col: number
-  var end_row: number
-  var end_col: number
-
-  # create a list of buffer positions where the edits have to be applied.
-  for e in text_edits
-    # Adjust the start and end columns for multibyte characters
-    start_row = e.range.start.line
-    start_col = s:get_line_byte_from_position(bnr, e.range.start)
-    end_row = e.range.end.line
-    end_col = s:get_line_byte_from_position(bnr, e.range.end)
-    start_line = [e.range.start.line, start_line]->min()
-    finish_line = [e.range.end.line, finish_line]->max()
-
-    updated_edits->add({'A': [start_row, start_col],
-                       'B': [end_row, end_col],
-                       'lines': e.newText->split("\n", v:true)})
-  endfor
-
-  # Reverse sort the edit operations by descending line and column numbers so
-  # that they can be applied without interfering with each other.
-  updated_edits->sort('s:edit_sort_func')
-
-  var lines: list<string> = bnr->getbufline(start_line + 1, finish_line + 1)
-  var fix_eol: number = bnr->getbufvar('&fixeol')
-  var set_eol = fix_eol && bnr->getbufinfo()[0].linecount <= finish_line + 1
-  if set_eol && lines[-1]->len() != 0
-    lines->add('')
-  endif
-
-  #echomsg 'lines(1) = ' .. string(lines)
-  #echomsg updated_edits
-
-  for e in updated_edits
-    var A: list<number> = [e.A[0] - start_line, e.A[1]]
-    var B: list<number> = [e.B[0] - start_line, e.B[1]]
-    lines = s:set_lines(lines, A, B, e.lines)
-  endfor
-
-  #echomsg 'lines(2) = ' .. string(lines)
-
-  # If the last line is empty and we need to set EOL, then remove it.
-  if set_eol && lines[-1]->len() == 0
-    lines->remove(-1)
-  endif
-
-  #echomsg 'applyTextEdits: start_line = ' .. start_line .. ', finish_line = ' .. finish_line
-  #echomsg 'lines = ' .. string(lines)
-
-  # Delete all the lines that need to be modified
-  bnr->deletebufline(start_line + 1, finish_line + 1)
-
-  # if the buffer is empty, appending lines before the first line adds an
-  # extra empty line at the end. Delete the empty line after appending the
-  # lines.
-  var dellastline: bool = v:false
-  if start_line == 0 && bnr->getbufinfo()[0].linecount == 1 &&
-                                    bnr->getbufline(1)[0] == ''
-    dellastline = v:true
-  endif
-
-  # Append the updated lines
-  appendbufline(bnr, start_line, lines)
-
-  if dellastline
-    bnr->deletebufline(bnr->getbufinfo()[0].linecount)
-  endif
-enddef
-
-# interface TextDocumentEdit
-def s:applyTextDocumentEdit(textDocEdit: dict<any>)
-  var bnr: number = bufnr(LspUriToFile(textDocEdit.textDocument.uri))
-  if bnr == -1
-    ErrMsg('Error: Text Document edit, buffer ' .. textDocEdit.textDocument.uri .. ' is not found')
-    return
-  endif
-  s:applyTextEdits(bnr, textDocEdit.edits)
-enddef
-
-# interface WorkspaceEdit
-def s:applyWorkspaceEdit(workspaceEdit: dict<any>)
-  if workspaceEdit->has_key('documentChanges')
-    for change in workspaceEdit.documentChanges
-      if change->has_key('kind')
-       ErrMsg('Error: Unsupported change in workspace edit [' .. change.kind .. ']')
-      else
-       s:applyTextDocumentEdit(change)
-      endif
-    endfor
-    return
-  endif
-
-  if !workspaceEdit->has_key('changes')
-    return
-  endif
-
-  var save_cursor: list<number> = getcurpos()
-  for [uri, changes] in items(workspaceEdit.changes)
-    var fname: string = LspUriToFile(uri)
-    var bnr: number = bufnr(fname)
-    if bnr == -1
-      # file is already removed
-      continue
-    endif
-
-    # interface TextEdit
-    s:applyTextEdits(bnr, changes)
-  endfor
-  save_cursor->setpos('.')
-enddef
-
-# process the 'textDocument/formatting' reply from the LSP server
-def s:processFormatReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>)
-  if reply.result->empty()
-    # nothing to format
-    return
-  endif
-
-  # result: TextEdit[]
-
-  var fname: string = LspUriToFile(req.params.textDocument.uri)
-  var bnr: number = bufnr(fname)
-  if bnr == -1
-    # file is already removed
-    return
-  endif
-
-  # interface TextEdit
-  # Apply each of the text edit operations
-  var save_cursor: list<number> = getcurpos()
-  s:applyTextEdits(bnr, reply.result)
-  save_cursor->setpos('.')
-enddef
-
-# process the 'textDocument/rename' reply from the LSP server
-def s:processRenameReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>)
-  if reply.result->empty()
-    # nothing to rename
-    return
-  endif
-
-  # result: WorkspaceEdit
-  s:applyWorkspaceEdit(reply.result)
-enddef
-
-# interface ExecuteCommandParams
-def s:executeCommand(lspserver: dict<any>, cmd: dict<any>)
-  var req = lspserver.createRequest('workspace/executeCommand')
-  req.params->extend(cmd)
-  lspserver.sendMessage(req)
-enddef
-
-# process the 'textDocument/codeAction' reply from the LSP server
-# params: interface Command[] | interface CodeAction[]
-def s:processCodeActionReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>)
-  if reply.result->empty()
-    # no action can be performed
-    WarnMsg('No code action is available')
-    return
-  endif
-
-  var actions: list<dict<any>> = reply.result
-
-  var prompt: list<string> = ['Code Actions:']
-  var act: dict<any>
-  for i in range(actions->len())
-    act = actions[i]
-    var t: string = act.title->substitute('\r\n', '\\r\\n', 'g')
-    t = t->substitute('\n', '\\n', 'g')
-    prompt->add(printf("%d. %s", i + 1, t))
-  endfor
-  var choice = inputlist(prompt)
-  if choice < 1 || choice > prompt->len()
-    return
-  endif
-
-  var selAction = actions[choice - 1]
-
-  # textDocument/codeAction can return either Command[] or CodeAction[].
-  # If it is a CodeAction, it can have either an edit, a command or both.
-  # Edits should be executed first.
-  if selAction->has_key('edit') || selAction->has_key('command')
-    if selAction->has_key('edit')
-      # apply edit first
-      s:applyWorkspaceEdit(selAction.edit)
-    endif
-    if selAction->has_key('command')
-      s:executeCommand(lspserver, selAction)
-    endif
-  else
-    s:executeCommand(lspserver, selAction)
-  endif
-enddef
-
-# process the 'workspace/executeCommand' reply from the LSP server
-def s:processWorkspaceExecuteReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>)
-  if reply.result->empty()
-    return
-  endif
-
-  # Nothing to do for the reply
-enddef
-
-# process the 'workspace/symbol' reply from the LSP server
-def s:processWorkspaceSymbolReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>)
-  if reply.result->empty()
-    WarnMsg('Error: Symbol not found')
-    return
-  endif
-
-  s:showSymbols(reply.result)
-enddef
-
-# Process various reply messages from the LSP server
-def s:processReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void
-  var lsp_reply_handlers: dict<func> =
-    {
-      'initialize': function('s:processInitializeReply'),
-      'textDocument/definition': function('s:processDefDeclReply'),
-      'textDocument/declaration': function('s:processDefDeclReply'),
-      'textDocument/typeDefinition': function('s:processDefDeclReply'),
-      'textDocument/implementation': function('s:processDefDeclReply'),
-      'textDocument/signatureHelp': function('s:processSignaturehelpReply'),
-      'textDocument/completion': function('s:processCompletionReply'),
-      'textDocument/hover': function('s:processHoverReply'),
-      'textDocument/references': function('s:processReferencesReply'),
-      'textDocument/documentHighlight': function('s:processDocHighlightReply'),
-      'textDocument/documentSymbol': function('s:processDocSymbolReply'),
-      'textDocument/formatting': function('s:processFormatReply'),
-      'textDocument/rangeFormatting': function('s:processFormatReply'),
-      'textDocument/rename': function('s:processRenameReply'),
-      'textDocument/codeAction': function('s:processCodeActionReply'),
-      'workspace/executeCommand': function('s:processWorkspaceExecuteReply'),
-      'workspace/symbol': function('s:processWorkspaceSymbolReply')
-    }
-
-  if lsp_reply_handlers->has_key(req.method)
-    lsp_reply_handlers[req.method](lspserver, req, reply)
-  else
-    ErrMsg("Error: Unsupported reply received from LSP server: " .. string(reply))
-  endif
-enddef
-
-# process a diagnostic notification message from the LSP server
-# params: interface PublishDiagnosticsParams
-def s:processDiagNotif(lspserver: dict<any>, reply: dict<any>): void
-  var fname: string = LspUriToFile(reply.params.uri)
-
-  # store the diagnostic for each line separately
-  var diag_by_lnum: dict<dict<any>> = {}
-  for diag in reply.params.diagnostics
-    diag_by_lnum[diag.range.start.line + 1] = diag
-  endfor
-
-  diagsMap->extend({[fname]: diag_by_lnum})
-enddef
-
-# process a log notification message from the LSP server
-def s:processLogMsgNotif(lspserver: dict<any>, reply: dict<any>)
-  # interface LogMessageParams
-  var msgType: list<string> = ['', 'Error: ', 'Warning: ', 'Info: ', 'Log: ']
-  if reply.params.type == 4
-    # ignore log messages from the LSP server (too chatty)
-    # TODO: Add a configuration to control the message level that will be
-    # displayed. Also store these messages and provide a command to display
-    # them.
-    return
-  endif
-  var mtype: string = 'Log: '
-  if reply.params.type > 0 && reply.params.type < 5
-    mtype = msgType[reply.params.type]
-  endif
-
-  :echomsg 'Lsp ' .. mtype .. reply.params.message
-enddef
-
-# process notification messages from the LSP server
-def s:processNotif(lspserver: dict<any>, reply: dict<any>): void
-  var lsp_notif_handlers: dict<func> =
-    {
-      'textDocument/publishDiagnostics': function('s:processDiagNotif'),
-      'window/showMessage': function('s:processLogMsgNotif'),
-      'window/logMessage': function('s:processLogMsgNotif')
-    }
-
-  if lsp_notif_handlers->has_key(reply.method)
-    lsp_notif_handlers[reply.method](lspserver, reply)
-  else
-    ErrMsg('Error: Unsupported notification received from LSP server ' .. string(reply))
-  endif
-enddef
-
-# send a response message to the server
-def s:sendResponse(lspserver: dict<any>, request: dict<any>, result: dict<any>, error: dict<any>)
-  var resp: dict<any> = lspserver.createResponse(request.id)
-  if type(result) != v:t_none
-    resp->extend({'result': result})
-  else
-    resp->extend({'error': error})
-  endif
-  lspserver.sendMessage(resp)
-enddef
-
-# process the workspace/applyEdit LSP server request
-def s:processApplyEditReq(lspserver: dict<any>, request: dict<any>)
-  # interface ApplyWorkspaceEditParams
-  if !request->has_key('params')
-    return
-  endif
-  var workspaceEditParams: dict<any> = request.params
-  if workspaceEditParams->has_key('label')
-    :echomsg "Workspace edit" .. workspaceEditParams.label
-  endif
-  s:applyWorkspaceEdit(workspaceEditParams.edit)
-  # TODO: Need to return the proper result of the edit operation
-  lspserver.sendResponse(request, {'applied': v:true}, v:null)
-enddef
-
-# process a request message from the server
-def s:processRequest(lspserver: dict<any>, request: dict<any>)
-  var lspRequestHandlers: dict<func> =
-    {
-      'workspace/applyEdit': function('s:processApplyEditReq')
-    }
-
-  if lspRequestHandlers->has_key(request.method)
-    lspRequestHandlers[request.method](lspserver, request)
-  else
-    ErrMsg('Error: Unsupported request received from LSP server ' ..
-                                                       string(request))
-  endif
-enddef
-
-# process LSP server messages
-def s:processMessages(lspserver: dict<any>): void
-  while lspserver.data->len() > 0
-    var idx = stridx(lspserver.data, 'Content-Length: ')
-    if idx == -1
-      return
-    endif
-
-    var len = str2nr(lspserver.data[idx + 16:])
-    if len == 0
-      ErrMsg("Error: Content length is zero")
-      return
-    endif
-
-    # Header and contents are separated by '\r\n\r\n'
-    idx = stridx(lspserver.data, "\r\n\r\n")
-    if idx == -1
-      ErrMsg("Error: Content separator is not found")
-      return
-    endif
-
-    idx = idx + 4
-
-    if lspserver.data->len() - idx < len
-      # message is not fully received. Process the message after more data is
-      # received
-      return
-    endif
-
-    var content = lspserver.data[idx : idx + len - 1]
-    var msg = content->json_decode()
-
-    if msg->has_key('result') || msg->has_key('error')
-      # response message from the server
-      var req = lspserver.requests->get(string(msg.id))
-      # Remove the corresponding stored request message
-      lspserver.requests->remove(string(msg.id))
-
-      if msg->has_key('result')
-        lspserver.processReply(req, msg)
-      else
-       # request failed
-        var emsg: string = msg.error.message
-       emsg ..= ', code = ' .. msg.code
-        if msg.error->has_key('data')
-          emsg = emsg .. ', data = ' .. string(msg.error.data)
-        endif
-        ErrMsg("Error: request " .. req.method .. " failed (" .. emsg .. ")")
-      endif
-    elseif msg->has_key('id')
-      # request message from the server
-      lspserver.processRequest(msg)
-    else
-      # notification message from the server
-      lspserver.processNotif(msg)
-    endif
-
-    lspserver.data = lspserver.data[idx + len :]
-  endwhile
-enddef
-
-# LSP server standard output handler
-def lsp#output_cb(lspserver: dict<any>, chan: channel, msg: string): void
-  s:traceLog(v:false, msg)
-  lspserver.data = lspserver.data .. msg
-  lspserver.processMessages()
-enddef
-
-# LSP server error output handler
-def lsp#error_cb(lspserver: dict<any>, chan: channel, emsg: string,): void
-  s:traceLog(v:true, emsg)
-enddef
-
-# LSP server exit callback
-def lsp#exit_cb(lspserver: dict<any>, job: job, status: number): void
-  WarnMsg("LSP server exited with status " .. status)
-  lspserver.job = v:none
-  lspserver.running = v:false
-  lspserver.requests = {}
-enddef
-
-# Return the next id for a LSP server request message
-def s:nextReqID(lspserver: dict<any>): number
-  var id = lspserver.nextID
-  lspserver.nextID = id + 1
-  return id
-enddef
-
-# Send a request message to LSP server
-def s:sendMessage(lspserver: dict<any>, content: dict<any>): void
-  var payload_js: string = content->json_encode()
-  var msg = "Content-Length: " .. payload_js->len() .. "\r\n\r\n"
-  var ch = lspserver.job->job_getchannel()
-  ch->ch_sendraw(msg)
-  ch->ch_sendraw(payload_js)
-enddef
-
-# create a LSP server request message
-def s:createRequest(lspserver: dict<any>, method: string): dict<any>
-  var req = {}
-  req.jsonrpc = '2.0'
-  req.id = lspserver.nextReqID()
-  req.method = method
-  req.params = {}
-
-  # Save the request, so that the corresponding response can be processed
-  lspserver.requests->extend({[string(req.id)]: req})
-
-  return req
-enddef
-
-# create a LSP server response message
-def s:createResponse(lspserver: dict<any>, req_id: number): dict<any>
-  var resp = {}
-  resp.jsonrpc = '2.0'
-  resp.id = req_id
-
-  return resp
-enddef
-
-# create a LSP server notification message
-def s:createNotification(lspserver: dict<any>, notif: string): dict<any>
-  var req = {}
-  req.jsonrpc = '2.0'
-  req.method = notif
-  req.params = {}
-
-  return req
-enddef
-
-# Send a "initialize" LSP request
-def s:initServer(lspserver: dict<any>)
-  var req = lspserver.createRequest('initialize')
-
-  var clientCaps: dict<any> = {
-       'workspace': {
-           'workspaceFolders': v:true,
-           'applyEdit': v:true,
-       },
-       'textDocument': {},
-       'window': {},
-       'general': {}
-    }
-
-  # interface 'InitializeParams'
-  var initparams: dict<any> = {}
-  initparams.processId = getpid()
-  initparams.clientInfo = {
-       'name': 'Vim',
-       'version': string(v:versionlong),
-      }
-  initparams.rootPath = getcwd()
-  initparams.rootUri = LspFileToUri(getcwd())
-  initparams.workspaceFolders = {
-       'uri': LspFileToUri(getcwd()),
-       'name': getcwd()
-      }
-  initparams.capabilities = clientCaps
-  req.params->extend(initparams)
-
-  lspserver.sendMessage(req)
-enddef
-
-# Send a "initialized" LSP notification
-def s:sendInitializedNotif(lspserver: dict<any>)
-  var notif: dict<any> = lspserver.createNotification('initialized')
-  lspserver.sendMessage(notif)
-enddef
-
-# Start a LSP server
-def s:startServer(lspserver: dict<any>): number
-  if lspserver.running
-    WarnMsg("LSP server for is already running")
-    return 0
-  endif
-
-  var cmd = [lspserver.path]
-  cmd->extend(lspserver.args)
-
-  var opts = {'in_mode': 'raw',
-              'out_mode': 'raw',
-              'err_mode': 'raw',
-              'noblock': 1,
-              'out_cb': function('lsp#output_cb', [lspserver]),
-              'err_cb': function('lsp#error_cb', [lspserver]),
-              'exit_cb': function('lsp#exit_cb', [lspserver])}
-
-  s:clearTraceLogs()
-  lspserver.data = ''
-  lspserver.caps = {}
-  lspserver.nextID = 1
-  lspserver.requests = {}
-  lspserver.completePending = v:false
-  lspserver.workspaceFolders = [getcwd()]
-
-  var job = job_start(cmd, opts)
-  if job->job_status() == 'fail'
-    ErrMsg("Error: Failed to start LSP server " .. lspserver.path)
-    return 1
-  endif
-
-  # wait for the LSP server to start
-  sleep 10m
-
-  lspserver.job = job
-  lspserver.running = v:true
-
-  lspserver.initServer()
-
-  return 0
-enddef
-
-# Send a 'shutdown' request to the LSP server
-def s:shutdownServer(lspserver: dict<any>): void
-  var req = lspserver.createRequest('shutdown')
-  lspserver.sendMessage(req)
-enddef
-
-# Send a 'exit' notification to the LSP server
-def s:exitServer(lspserver: dict<any>): void
-  var notif: dict<any> = lspserver.createNotification('exit')
-  lspserver.sendMessage(notif)
-enddef
-
-# Stop a LSP server
-def s:stopServer(lspserver: dict<any>): number
-  if !lspserver.running
-    WarnMsg("LSP server is not running")
-    return 0
-  endif
-
-  lspserver.shutdownServer()
-
-  # Wait for the server to process the shutodwn request
-  sleep 1
-
-  lspserver.exitServer()
-
-  lspserver.job->job_stop()
-  lspserver.job = v:none
-  lspserver.running = v:false
-  lspserver.requests = {}
-  return 0
-enddef
-
-# set the LSP server trace level using $/setTrace notification
-def s:setTrace(lspserver: dict<any>, traceVal: string)
-  var notif: dict<any> = lspserver.createNotification('$/setTrace')
-  notif.params->extend({'value': traceVal})
-  lspserver.sendMessage(notif)
-enddef
-
-# Send a LSP "textDocument/didOpen" notification
-def s:textdocDidOpen(lspserver: dict<any>, bnr: number, ftype: string): void
-  var notif: dict<any> = lspserver.createNotification('textDocument/didOpen')
-
-  # interface DidOpenTextDocumentParams
-  # interface TextDocumentItem
-  var tdi = {}
-  tdi.uri = LspFileToUri(bufname(bnr))
-  tdi.languageId = ftype
-  tdi.version = 1
-  tdi.text = getbufline(bnr, 1, '$')->join("\n") .. "\n"
-  notif.params->extend({'textDocument': tdi})
-
-  lspserver.sendMessage(notif)
-enddef
-
-# Send a LSP "textDocument/didClose" notification
-def s:textdocDidClose(lspserver: dict<any>, bnr: number): void
-  var notif: dict<any> = lspserver.createNotification('textDocument/didClose')
+import NewLspServer from './lspserver.vim'
+import {WarnMsg, ErrMsg, lsp_server_trace} from './util.vim'
 
-  # interface DidCloseTextDocumentParams
-  #   interface TextDocumentIdentifier
-  var tdid = {}
-  tdid.uri = LspFileToUri(bufname(bnr))
-  notif.params->extend({'textDocument': tdid})
+# Needs Vim 8.2.2082 and higher
+if v:version < 802 || !has('patch-8.2.2082')
+  finish
+endif
 
-  lspserver.sendMessage(notif)
-enddef
+# LSP server information
+var lspServers: list<dict<any>> = []
 
-# Return the current cursor position as a LSP position.
-# LSP line and column numbers start from zero, whereas Vim line and column
-# numbers start from one. The LSP column number is the character index in the
-# line and not the byte index in the line.
-def s:getLspPosition(): dict<number>
-  var lnum: number = line('.') - 1
-  #var col: number = strchars(getline('.')[: col('.') - 1]) - 1
-  var col: number = col('.') - 1
-  return {'line': lnum, 'character': col}
-enddef
+# filetype to LSP server map
+var ftypeServerMap: dict<dict<any>> = {}
 
-# Return the current file name and current cursor position as a LSP
-# TextDocumentPositionParams structure
-def s:getLspTextDocPosition(): dict<dict<any>>
-  # interface TextDocumentIdentifier
-  # interface Position
-  return {'textDocument': {'uri': LspFileToUri(@%)},
-         'position': s:getLspPosition()}
-enddef
+# Buffer number to LSP server map
+var bufnrToServer: dict<dict<any>> = {}
 
-def s:gotoDefinition(lspserver: dict<any>): void
-  var req = lspserver.createRequest('textDocument/definition')
+# List of diagnostics for each opened file
+#var diagsMap: dict<dict<any>> = {}
 
-  # interface DefinitionParams
-  #   interface TextDocumentPositionParams
-  req.params->extend(s:getLspTextDocPosition())
+prop_type_add('LspTextRef', {'highlight': 'Search'})
+prop_type_add('LspReadRef', {'highlight': 'DiffChange'})
+prop_type_add('LspWriteRef', {'highlight': 'DiffDelete'})
 
-  lspserver.sendMessage(req)
+# Return the LSP server for the a specific filetype. Returns a null dict if
+# the server is not found.
+def s:lspGetServer(ftype: string): dict<any>
+  return ftypeServerMap->get(ftype, {})
 enddef
 
-def s:gotoDeclaration(lspserver: dict<any>): void
-  var req = lspserver.createRequest('textDocument/declaration')
-
-  # interface DeclarationParams
-  #   interface TextDocumentPositionParams
-  req.params->extend(s:getLspTextDocPosition())
-
-  lspserver.sendMessage(req)
+# Add a LSP server for a filetype
+def s:lspAddServer(ftype: string, lspserver: dict<any>)
+  ftypeServerMap->extend({[ftype]: lspserver})
 enddef
 
-def s:gotoTypeDef(lspserver: dict<any>): void
-  var req = lspserver.createRequest('textDocument/typeDefinition')
-
-  # interface TypeDefinitionParams
-  #   interface TextDocumentPositionParams
-  req.params->extend(s:getLspTextDocPosition())
-
-  lspserver.sendMessage(req)
+def lsp#enableServerTrace()
+  lsp_server_trace = v:true
 enddef
 
-def s:gotoImplementation(lspserver: dict<any>): void
-  var req = lspserver.createRequest('textDocument/implementation')
-
-  # interface ImplementationParams
-  #   interface TextDocumentPositionParams
-  req.params->extend(s:getLspTextDocPosition())
-
-  lspserver.sendMessage(req)
+# Show information about all the LSP servers
+def lsp#showServers()
+  for [ftype, lspserver] in items(ftypeServerMap)
+    var msg = ftype .. "    "
+    if lspserver.running
+      msg ..= 'running'
+    else
+      msg ..= 'not running'
+    endif
+    msg ..= '    ' .. lspserver.path
+    :echomsg msg
+  endfor
 enddef
 
 # Go to a definition using "textDocument/definition" LSP request
 def lsp#gotoDefinition()
   var ftype: string = &filetype
-  if ftype == ''
+  if ftype == '' || @% == ''
     return
   endif
 
@@ -1348,25 +71,6 @@ def lsp#gotoDefinition()
     ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running')
     return
   endif
-  # Check whether LSP server supports jumping to a definition
-  if !lspserver.caps->has_key('definitionProvider')
-              || !lspserver.caps.definitionProvider
-    ErrMsg("Error: LSP server does not support jumping to a definition")
-    return
-  endif
-
-  var fname: string = @%
-  if fname == ''
-    return
-  endif
-
-  # push the current location on to the tag stack
-  settagstack(winnr(), {'items':
-                         [{'bufnr': bufnr(),
-                           'from': getpos('.'),
-                           'matchnr': 1,
-                           'tagname': expand('<cword>')}
-                         ]}, 'a')
 
   lspserver.gotoDefinition()
 enddef
@@ -1387,25 +91,6 @@ def lsp#gotoDeclaration()
     ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running')
     return
   endif
-  # Check whether LSP server supports jumping to a declaration
-  if !lspserver.caps->has_key('declarationProvider')
-              || !lspserver.caps.declarationProvider
-    ErrMsg("Error: LSP server does not support jumping to a declaration")
-    return
-  endif
-
-  var fname: string = @%
-  if fname == ''
-    return
-  endif
-
-  # push the current location on to the tag stack
-  settagstack(winnr(), {'items':
-                         [{'bufnr': bufnr(),
-                           'from': getpos('.'),
-                           'matchnr': 1,
-                           'tagname': expand('<cword>')}
-                         ]}, 'a')
 
   lspserver.gotoDeclaration()
 enddef
@@ -1426,25 +111,6 @@ def lsp#gotoTypedef()
     ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running')
     return
   endif
-  # Check whether LSP server supports jumping to a type definition
-  if !lspserver.caps->has_key('typeDefinitionProvider')
-              || !lspserver.caps.typeDefinitionProvider
-    ErrMsg("Error: LSP server does not support jumping to a type definition")
-    return
-  endif
-
-  var fname: string = @%
-  if fname == ''
-    return
-  endif
-
-  # push the current location on to the tag stack
-  settagstack(winnr(), {'items':
-                         [{'bufnr': bufnr(),
-                           'from': getpos('.'),
-                           'matchnr': 1,
-                           'tagname': expand('<cword>')}
-                         ]}, 'a')
 
   lspserver.gotoTypeDef()
 enddef
@@ -1465,44 +131,10 @@ def lsp#gotoImplementation()
     ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running')
     return
   endif
-  # Check whether LSP server supports jumping to a implementation
-  if !lspserver.caps->has_key('implementationProvider')
-              || !lspserver.caps.implementationProvider
-    ErrMsg("Error: LSP server does not support jumping to an implementation")
-    return
-  endif
-
-  var fname: string = @%
-  if fname == ''
-    return
-  endif
-
-  # push the current location on to the tag stack
-  settagstack(winnr(), {'items':
-                         [{'bufnr': bufnr(),
-                           'from': getpos('.'),
-                           'matchnr': 1,
-                           'tagname': expand('<cword>')}
-                         ]}, 'a')
 
   lspserver.gotoImplementation()
 enddef
 
-def s:showSignature(lspserver: dict<any>): void
-  # Check whether LSP server supports signature help
-  if !lspserver.caps->has_key('signatureHelpProvider')
-    ErrMsg("Error: LSP server does not support signature help")
-    return
-  endif
-
-  var req = lspserver.createRequest('textDocument/signatureHelp')
-  # interface SignatureHelpParams
-  #   interface TextDocumentPositionParams
-  req.params->extend(s:getLspTextDocPosition())
-
-  lspserver.sendMessage(req)
-enddef
-
 # Show the signature using "textDocument/signatureHelp" LSP method
 # Invoked from an insert-mode mapping, so return an empty string.
 def lsp#showSignature(): string
@@ -1540,64 +172,7 @@ def lsp#bufchange_listener(bnr: number, start: number, end: number, added: numbe
     return
   endif
 
-  var notif: dict<any> = lspserver.createNotification('textDocument/didChange')
-
-  # interface DidChangeTextDocumentParams
-  #   interface VersionedTextDocumentIdentifier
-  var vtdid: dict<any> = {}
-  vtdid.uri = LspFileToUri(bufname(bnr))
-  # Use Vim 'changedtick' as the LSP document version number
-  vtdid.version = bnr->getbufvar('changedtick')
-  notif.params->extend({'textDocument': vtdid})
-  #   interface TextDocumentContentChangeEvent
-  var changeset: list<dict<any>>
-
-  ##### FIXME: Sending specific buffer changes to the LSP server doesn't
-  ##### work properly as the computed line range numbers is not correct.
-  ##### For now, send the entire buffer content to LSP server.
-  # #     Range
-  # for change in changes
-  #   var lines: string
-  #   var start_lnum: number
-  #   var end_lnum: number
-  #   var start_col: number
-  #   var end_col: number
-  #   if change.added == 0
-  #     # lines changed
-  #     start_lnum =  change.lnum - 1
-  #     end_lnum = change.end - 1
-  #     lines = getbufline(bnr, change.lnum, change.end - 1)->join("\n") .. "\n"
-  #     start_col = 0
-  #     end_col = 0
-  #   elseif change.added > 0
-  #     # lines added
-  #     start_lnum = change.lnum - 1
-  #     end_lnum = change.lnum - 1
-  #     start_col = 0
-  #     end_col = 0
-  #     lines = getbufline(bnr, change.lnum, change.lnum + change.added - 1)->join("\n") .. "\n"
-  #   else
-  #     # lines removed
-  #     start_lnum = change.lnum - 1
-  #     end_lnum = change.lnum + (-change.added) - 1
-  #     start_col = 0
-  #     end_col = 0
-  #     lines = ''
-  #   endif
-  #   var range: dict<dict<number>> = {'start': {'line': start_lnum, 'character': start_col}, 'end': {'line': end_lnum, 'character': end_col}}
-  #   changeset->add({'range': range, 'text': lines})
-  # endfor
-  changeset->add({'text': getbufline(bnr, 1, '$')->join("\n") .. "\n"})
-  notif.params->extend({'contentChanges': changeset})
-
-  lspserver.sendMessage(notif)
-enddef
-
-def s:didSaveFile(lspserver: dict<any>): void
-  var notif: dict<any> = lspserver.createNotification('textDocument/didSave')
-  # interface: DidSaveTextDocumentParams
-  notif.params->extend({'textDocument': {'uri': LspFileToUri(bufname(bnr))}})
-  lspserver.sendMessage(notif)
+  lspserver.textdocDidChange(bnr, start, end, added, changes)
 enddef
 
 # A buffer is saved. Send the "textDocument/didSave" LSP notification
@@ -1609,15 +184,6 @@ def s:lspSavedFile()
     return
   endif
 
-  # Check whether the LSP server supports the didSave notification
-  if !lspserver.caps->has_key('textDocumentSync')
-               || lspserver.caps.textDocumentSync->type() == v:t_number
-               || !lspserver.caps.textDocumentSync->has_key('save')
-               || !lspserver.caps.textDocumentSync.save
-    # LSP server doesn't support text document synchronization
-    return
-  endif
-
   lspserver.didSaveFile()
 enddef
 
@@ -1688,8 +254,8 @@ def lsp#removeFile(bnr: number): void
     return
   endif
   lspserver.textdocDidClose(bnr)
-  if diagsMap->has_key(fname)
-    diagsMap->remove(fname)
+  if lspserver.diagsMap->has_key(fname)
+    lspserver.diagsMap->remove(fname)
   endif
   remove(bufnrToServer, bnr)
 enddef
@@ -1720,54 +286,7 @@ def lsp#addServer(serverList: list<dict<any>>)
       return
     endif
 
-    var lspserver: dict<any> = {
-      path: server.path,
-      args: server.args,
-      running: v:false,
-      job: v:none,
-      data: '',
-      nextID: 1,
-      caps: {},
-      requests: {},
-      completePending: v:false
-    }
-    # Add the LSP server functions
-    lspserver->extend({
-        'startServer': function('s:startServer', [lspserver]),
-        'initServer': function('s:initServer', [lspserver]),
-        'stopServer': function('s:stopServer', [lspserver]),
-        'shutdownServer': function('s:shutdownServer', [lspserver]),
-        'exitServer': function('s:exitServer', [lspserver]),
-       'setTrace': function('s:setTrace', [lspserver]),
-        'nextReqID': function('s:nextReqID', [lspserver]),
-        'createRequest': function('s:createRequest', [lspserver]),
-        'createResponse': function('s:createResponse', [lspserver]),
-        'createNotification': function('s:createNotification', [lspserver]),
-        'sendResponse': function('s:sendResponse', [lspserver]),
-        'sendMessage': function('s:sendMessage', [lspserver]),
-        'processReply': function('s:processReply', [lspserver]),
-        'processNotif': function('s:processNotif', [lspserver]),
-       'processRequest': function('s:processRequest', [lspserver]),
-        'processMessages': function('s:processMessages', [lspserver]),
-        'textdocDidOpen': function('s:textdocDidOpen', [lspserver]),
-        'textdocDidClose': function('s:textdocDidClose', [lspserver]),
-        'sendInitializedNotif': function('s:sendInitializedNotif', [lspserver]),
-        'getCompletion': function('s:getCompletion', [lspserver]),
-       'gotoDefinition': function('s:gotoDefinition', [lspserver]),
-       'gotoDeclaration': function('s:gotoDeclaration', [lspserver]),
-       'gotoTypeDef': function('s:gotoTypeDef', [lspserver]),
-       'gotoImplementation': function('s:gotoImplementation', [lspserver]),
-       'showSignature': function('s:showSignature', [lspserver]),
-       'didSaveFile': function('s:didSaveFile', [lspserver]),
-       'hover': function('s:hover', [lspserver]),
-       'showReferences': function('s:showReferences', [lspserver]),
-       'docHighlight': function('s:docHighlight', [lspserver]),
-       'showDocSymbols': function('s:showDocSymbols', [lspserver]),
-       'renameSymbol': function('s:renameSymbol', [lspserver]),
-       'workspaceSymbols': function('s:workspaceSymbols', [lspserver]),
-       'addWorkspaceFolder': function('s:addWorkspaceFolder', [lspserver]),
-       'removeWorkspaceFolder': function('s:removeWorkspaceFolder', [lspserver])
-      })
+    var lspserver: dict<any> = NewLspServer(server.path, server.args)
 
     if type(server.filetype) == v:t_string
       s:lspAddServer(server.filetype, lspserver)
@@ -1821,12 +340,27 @@ enddef
 
 # Display the diagnostic messages from the LSP server for the current buffer
 def lsp#showDiagnostics(): void
+  var ftype = &filetype
+  if ftype == ''
+    return
+  endif
+
+  var lspserver: dict<any> = s:lspGetServer(ftype)
+  if lspserver->empty()
+    ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not found')
+    return
+  endif
+  if !lspserver.running
+    ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running')
+    return
+  endif
+
   var fname: string = expand('%:p')
   if fname == ''
     return
   endif
 
-  if !diagsMap->has_key(fname) || diagsMap[fname]->empty()
+  if !lspserver.diagsMap->has_key(fname) || lspserver.diagsMap[fname]->empty()
     WarnMsg('No diagnostic messages found for ' .. fname)
     return
   endif
@@ -1834,7 +368,7 @@ def lsp#showDiagnostics(): void
   var qflist: list<dict<any>> = []
   var text: string
 
-  for [lnum, diag] in items(diagsMap[fname])
+  for [lnum, diag] in items(lspserver.diagsMap[fname])
     text = diag.message->substitute("\n\\+", "\n", 'g')
     qflist->add({'filename': fname,
                     'lnum': diag.range.start.line + 1,
@@ -1846,27 +380,6 @@ def lsp#showDiagnostics(): void
   :copen
 enddef
 
-def s:getCompletion(lspserver: dict<any>): void
-  # Check whether LSP server supports completion
-  if !lspserver.caps->has_key('completionProvider')
-    ErrMsg("Error: LSP server does not support completion")
-    return
-  endif
-
-  var fname = @%
-  if fname == ''
-    return
-  endif
-
-  var req = lspserver.createRequest('textDocument/completion')
-
-  # interface CompletionParams
-  #   interface TextDocumentPositionParams
-  req.params->extend(s:getLspTextDocPosition())
-
-  lspserver.sendMessage(req)
-enddef
-
 # Insert mode completion handler
 def lsp#completeFunc(findstart: number, base: string): any
   var ftype: string = &filetype
@@ -1913,20 +426,6 @@ def lsp#completeFunc(findstart: number, base: string): any
   endif
 enddef
 
-def s:hover(lspserver: dict<any>): void
-  # Check whether LSP server supports getting hover information
-  if !lspserver.caps->has_key('hoverProvider')
-              || !lspserver.caps.hoverProvider
-    return
-  endif
-
-  var req = lspserver.createRequest('textDocument/hover')
-  # interface HoverParams
-  #   interface TextDocumentPositionParams
-  req.params->extend(s:getLspTextDocPosition())
-  lspserver.sendMessage(req)
-enddef
-
 # Display the hover message from the LSP server for the current cursor
 # location
 def LspHover()
@@ -1951,23 +450,6 @@ def LspHover()
   lspserver.hover()
 enddef
 
-def s:showReferences(lspserver: dict<any>): void
-  # Check whether LSP server supports getting reference information
-  if !lspserver.caps->has_key('referencesProvider')
-              || !lspserver.caps.referencesProvider
-    ErrMsg("Error: LSP server does not support showing references")
-    return
-  endif
-
-  var req = lspserver.createRequest('textDocument/references')
-  # interface ReferenceParams
-  #   interface TextDocumentPositionParams
-  req.params->extend(s:getLspTextDocPosition())
-  req.params->extend({'context': {'includeDeclaration': v:true}})
-
-  lspserver.sendMessage(req)
-enddef
-
 # show symbol references
 def lsp#showReferences()
   var ftype = &filetype
@@ -1993,21 +475,6 @@ def lsp#showReferences()
   lspserver.showReferences()
 enddef
 
-def s:docHighlight(lspserver: dict<any>): void
-  # Check whether LSP server supports getting highlight information
-  if !lspserver.caps->has_key('documentHighlightProvider')
-              || !lspserver.caps.documentHighlightProvider
-    ErrMsg("Error: LSP server does not support document highlight")
-    return
-  endif
-
-  var req = lspserver.createRequest('textDocument/documentHighlight')
-  # interface DocumentHighlightParams
-  #   interface TextDocumentPositionParams
-  req.params->extend(s:getLspTextDocPosition())
-  lspserver.sendMessage(req)
-enddef
-
 # highlight all the places where a symbol is referenced
 def lsp#docHighlight()
   var ftype = &filetype
@@ -2040,21 +507,6 @@ def lsp#docHighlightClear()
   prop_remove({'type': 'LspWriteRef', 'all': v:true}, 1, line('$'))
 enddef
 
-def s:showDocSymbols(lspserver: dict<any>, fname: string): void
-  # Check whether LSP server supports getting document symbol information
-  if !lspserver.caps->has_key('documentSymbolProvider')
-              || !lspserver.caps.documentSymbolProvider
-    ErrMsg("Error: LSP server does not support getting list of symbols")
-    return
-  endif
-
-  var req = lspserver.createRequest('textDocument/documentSymbol')
-  # interface DocumentSymbolParams
-  # interface TextDocumentIdentifier
-  req.params->extend({'textDocument': {'uri': LspFileToUri(fname)}})
-  lspserver.sendMessage(req)
-enddef
-
 # open a window and display all the symbols in a file
 def lsp#showDocSymbols()
   var ftype = &filetype
@@ -2102,51 +554,16 @@ def lsp#textDocFormat(range_args: number, line1: number, line2: number)
     return
   endif
 
-  # Check whether LSP server supports formatting documents
-  if !lspserver.caps->has_key('documentFormattingProvider')
-              || !lspserver.caps.documentFormattingProvider
-    ErrMsg("Error: LSP server does not support formatting documents")
-    return
-  endif
-
   var fname = @%
   if fname == ''
     return
   endif
 
-  var cmd: string
   if range_args > 0
-    cmd = 'textDocument/rangeFormatting'
-  else
-    cmd = 'textDocument/formatting'
-  endif
-  var req = lspserver.createRequest(cmd)
-
-  # interface DocumentFormattingParams
-  # interface TextDocumentIdentifier
-  req.params->extend({'textDocument': {'uri': LspFileToUri(fname)}})
-  var tabsz: number
-  if &sts > 0
-    tabsz = &sts
-  elseif &sts < 0
-    tabsz = &shiftwidth
+    lspserver.textDocFormat(fname, v:true, line1, line2)
   else
-    tabsz = &tabstop
-  endif
-  # interface FormattingOptions
-  var fmtopts: dict<any> = {
-    tabSize: tabsz,
-    insertSpaces: &expandtab ? v:true : v:false,
-  }
-  req.params->extend({'options': fmtopts})
-  if range_args > 0
-    var r: dict<dict<number>> = {
-       'start': {'line': line1 - 1, 'character': 0},
-       'end': {'line': line2, 'character': 0}}
-    req.params->extend({'range': r})
+    lspserver.textDocFormat(fname, v:false, 0, 0)
   endif
-
-  lspserver.sendMessage(req)
 enddef
 
 # TODO: Add support for textDocument.onTypeFormatting?
@@ -2155,28 +572,6 @@ enddef
 # Display all the locations where the current symbol is called from.
 # Uses LSP "callHierarchy/incomingCalls" request
 def lsp#incomingCalls()
-  var ftype = &filetype
-  if ftype == ''
-    return
-  endif
-
-  var lspserver: dict<any> = s:lspGetServer(ftype)
-  if lspserver->empty()
-    ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not found')
-    return
-  endif
-  if !lspserver.running
-    ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running')
-    return
-  endif
-
-  # Check whether LSP server supports document highlight
-  if !lspserver.caps->has_key('documentHighlightProvider')
-              || !lspserver.caps.documentHighlightProvider
-    ErrMsg("Error: LSP server does not support document highlight")
-    return
-  endif
-
   :echomsg 'Error: Not implemented yet'
 enddef
 
@@ -2186,22 +581,6 @@ def lsp#outgoingCalls()
   :echomsg 'Error: Not implemented yet'
 enddef
 
-def s:renameSymbol(lspserver: dict<any>, newName: string)
-  # Check whether LSP server supports rename operation
-  if !lspserver.caps->has_key('renameProvider')
-              || !lspserver.caps.renameProvider
-    ErrMsg("Error: LSP server does not support rename operation")
-    return
-  endif
-
-  var req = lspserver.createRequest('textDocument/rename')
-  # interface RenameParams
-  #   interface TextDocumentPositionParams
-  req.params->extend(s:getLspTextDocPosition())
-  req.params->extend({'newName': newName})
-  lspserver.sendMessage(req)
-enddef
-
 # Rename a symbol
 # Uses LSP "textDocument/rename" request
 def lsp#rename()
@@ -2251,48 +630,12 @@ def lsp#codeAction()
     return
   endif
 
-  # Check whether LSP server supports code action operation
-  if !lspserver.caps->has_key('codeActionProvider')
-              || !lspserver.caps.codeActionProvider
-    ErrMsg("Error: LSP server does not support code action operation")
-    return
-  endif
-
   var fname = @%
   if fname == ''
     return
   endif
 
-  var req = lspserver.createRequest('textDocument/codeAction')
-
-  # interface CodeActionParams
-  req.params->extend({'textDocument': {'uri': LspFileToUri(fname)}})
-  var r: dict<dict<number>> = {
-                 'start': {'line': line('.') - 1, 'character': col('.') - 1},
-                 'end': {'line': line('.') - 1, 'character': col('.') - 1}}
-  req.params->extend({'range': r})
-  var diag: list<dict<any>> = []
-  var lnum = line('.')
-  fname = fnamemodify(fname, ':p')
-  if diagsMap->has_key(fname) && diagsMap[fname]->has_key(lnum)
-    diag->add(diagsMap[fname][lnum])
-  endif
-  req.params->extend({'context': {'diagnostics': diag}})
-
-  lspserver.sendMessage(req)
-enddef
-
-def s:workspaceSymbols(lspserver: dict<any>, sym: string)
-  # Check whether the LSP server supports listing workspace symbols
-  if !lspserver.caps->has_key('workspaceSymbolProvider')
-              || !lspserver.caps.workspaceSymbolProvider
-    ErrMsg("Error: LSP server does not support listing workspace symbols")
-    return
-  endif
-
-  var req = lspserver.createRequest('workspace/symbol')
-  req.params->extend({'query': sym})
-  lspserver.sendMessage(req)
+  lspserver.codeAction(fname)
 enddef
 
 # Perform a workspace wide symbol lookup
@@ -2346,53 +689,6 @@ def lsp#listWorkspaceFolders()
   echomsg 'Workspace Folders: ' .. string(lspserver.workspaceFolders)
 enddef
 
-def s:addWorkspaceFolder(lspserver: dict<any>, dirName: string): void
-  if !lspserver.caps->has_key('workspace')
-         || !lspserver.caps.workspace->has_key('workspaceFolders')
-         || !lspserver.caps.workspace.workspaceFolders->has_key('supported')
-         || !lspserver.caps.workspace.workspaceFolders.supported
-      ErrMsg('Error: LSP server does not support workspace folders')
-    return
-  endif
-
-  if lspserver.workspaceFolders->index(dirName) != -1
-    ErrMsg('Error: ' .. dirName .. ' is already part of this workspace')
-    return
-  endif
-
-  var notif: dict<any> =
-       lspserver.createNotification('workspace/didChangeWorkspaceFolders')
-  # interface DidChangeWorkspaceFoldersParams
-  notif.params->extend({'event': {'added': [dirName], 'removed': []}})
-  lspserver.sendMessage(notif)
-
-  lspserver.workspaceFolders->add(dirName)
-enddef
-
-def s:removeWorkspaceFolder(lspserver: dict<any>, dirName: string): void
-  if !lspserver.caps->has_key('workspace')
-         || !lspserver.caps.workspace->has_key('workspaceFolders')
-         || !lspserver.caps.workspace.workspaceFolders->has_key('supported')
-         || !lspserver.caps.workspace.workspaceFolders.supported
-      ErrMsg('Error: LSP server does not support workspace folders')
-    return
-  endif
-
-  var idx: number = lspserver.workspaceFolders->index(dirName)
-  if idx == -1
-    ErrMsg('Error: ' .. dirName .. ' is not currently part of this workspace')
-    return
-  endif
-
-  var notif: dict<any> =
-       lspserver.createNotification('workspace/didChangeWorkspaceFolders')
-  # interface DidChangeWorkspaceFoldersParams
-  notif.params->extend({'event': {'added': [], 'removed': [dirName]}})
-  lspserver.sendMessage(notif)
-
-  lspserver.workspaceFolders->remove(idx)
-enddef
-
 # Add a workspace folder. Default is to use the current folder.
 def lsp#addWorkspaceFolder(dirArg: string)
   var ftype = &filetype
diff --git a/autoload/lspserver.vim b/autoload/lspserver.vim
new file mode 100644 (file)
index 0000000..8340879
--- /dev/null
@@ -0,0 +1,715 @@
+vim9script
+
+# LSP server functions
+
+import {ProcessReply, ProcessNotif, ProcessRequest, ProcessMessages} from './handlers.vim'
+import {WarnMsg, ErrMsg, ClearTraceLogs, TraceLog, LspUriToFile, LspFileToUri} from './util.vim'
+
+# LSP server standard output handler
+def s:output_cb(lspserver: dict<any>, chan: channel, msg: string): void
+  TraceLog(v:false, msg)
+  lspserver.data = lspserver.data .. msg
+  lspserver.processMessages()
+enddef
+
+# LSP server error output handler
+def s:error_cb(lspserver: dict<any>, chan: channel, emsg: string,): void
+  TraceLog(v:true, emsg)
+enddef
+
+# LSP server exit callback
+def s:exit_cb(lspserver: dict<any>, job: job, status: number): void
+  WarnMsg("LSP server exited with status " .. status)
+  lspserver.job = v:none
+  lspserver.running = v:false
+  lspserver.requests = {}
+enddef
+
+# Start a LSP server
+def s:startServer(lspserver: dict<any>): number
+  if lspserver.running
+    WarnMsg("LSP server for is already running")
+    return 0
+  endif
+
+  var cmd = [lspserver.path]
+  cmd->extend(lspserver.args)
+
+  var opts = {in_mode: 'raw',
+              out_mode: 'raw',
+              err_mode: 'raw',
+              noblock: 1,
+              out_cb: function('s:output_cb', [lspserver]),
+              err_cb: function('s:error_cb', [lspserver]),
+              exit_cb: function('s:exit_cb', [lspserver])}
+
+  ClearTraceLogs()
+  lspserver.data = ''
+  lspserver.caps = {}
+  lspserver.nextID = 1
+  lspserver.requests = {}
+  lspserver.completePending = v:false
+  lspserver.workspaceFolders = [getcwd()]
+
+  var job = job_start(cmd, opts)
+  if job->job_status() == 'fail'
+    ErrMsg("Error: Failed to start LSP server " .. lspserver.path)
+    return 1
+  endif
+
+  # wait for the LSP server to start
+  sleep 10m
+
+  lspserver.job = job
+  lspserver.running = v:true
+
+  lspserver.initServer()
+
+  return 0
+enddef
+
+# Send a "initialize" LSP request
+def s:initServer(lspserver: dict<any>)
+  var req = lspserver.createRequest('initialize')
+
+  var clientCaps: dict<any> = {
+       workspace: {
+           workspaceFolders: v:true,
+           applyEdit: v:true,
+       },
+       textDocument: {},
+       window: {},
+       general: {}
+    }
+
+  # interface 'InitializeParams'
+  var initparams: dict<any> = {}
+  initparams.processId = getpid()
+  initparams.clientInfo = {
+       name: 'Vim',
+       version: string(v:versionlong),
+      }
+  initparams.rootPath = getcwd()
+  initparams.rootUri = LspFileToUri(getcwd())
+  initparams.workspaceFolders = {
+       uri: LspFileToUri(getcwd()),
+       name: getcwd()
+      }
+  initparams.capabilities = clientCaps
+  req.params->extend(initparams)
+
+  lspserver.sendMessage(req)
+enddef
+
+# Send a "initialized" LSP notification
+def s:sendInitializedNotif(lspserver: dict<any>)
+  var notif: dict<any> = lspserver.createNotification('initialized')
+  lspserver.sendMessage(notif)
+enddef
+
+# Send a 'shutdown' request to the LSP server
+def s:shutdownServer(lspserver: dict<any>): void
+  var req = lspserver.createRequest('shutdown')
+  lspserver.sendMessage(req)
+enddef
+
+# Send a 'exit' notification to the LSP server
+def s:exitServer(lspserver: dict<any>): void
+  var notif: dict<any> = lspserver.createNotification('exit')
+  lspserver.sendMessage(notif)
+enddef
+
+# Stop a LSP server
+def s:stopServer(lspserver: dict<any>): number
+  if !lspserver.running
+    WarnMsg("LSP server is not running")
+    return 0
+  endif
+
+  lspserver.shutdownServer()
+
+  # Wait for the server to process the shutodwn request
+  sleep 1
+
+  lspserver.exitServer()
+
+  lspserver.job->job_stop()
+  lspserver.job = v:none
+  lspserver.running = v:false
+  lspserver.requests = {}
+  return 0
+enddef
+
+# set the LSP server trace level using $/setTrace notification
+def s:setTrace(lspserver: dict<any>, traceVal: string)
+  var notif: dict<any> = lspserver.createNotification('$/setTrace')
+  notif.params->extend({value: traceVal})
+  lspserver.sendMessage(notif)
+enddef
+
+# Return the next id for a LSP server request message
+def s:nextReqID(lspserver: dict<any>): number
+  var id = lspserver.nextID
+  lspserver.nextID = id + 1
+  return id
+enddef
+
+# create a LSP server request message
+def s:createRequest(lspserver: dict<any>, method: string): dict<any>
+  var req = {}
+  req.jsonrpc = '2.0'
+  req.id = lspserver.nextReqID()
+  req.method = method
+  req.params = {}
+
+  # Save the request, so that the corresponding response can be processed
+  lspserver.requests->extend({[string(req.id)]: req})
+
+  return req
+enddef
+
+# create a LSP server response message
+def s:createResponse(lspserver: dict<any>, req_id: number): dict<any>
+  var resp = {}
+  resp.jsonrpc = '2.0'
+  resp.id = req_id
+
+  return resp
+enddef
+
+# create a LSP server notification message
+def s:createNotification(lspserver: dict<any>, notif: string): dict<any>
+  var req = {}
+  req.jsonrpc = '2.0'
+  req.method = notif
+  req.params = {}
+
+  return req
+enddef
+
+# send a response message to the server
+def s:sendResponse(lspserver: dict<any>, request: dict<any>, result: dict<any>, error: dict<any>)
+  var resp: dict<any> = lspserver.createResponse(request.id)
+  if type(result) != v:t_none
+    resp->extend({result: result})
+  else
+    resp->extend({error: error})
+  endif
+  lspserver.sendMessage(resp)
+enddef
+
+# Send a request message to LSP server
+def s:sendMessage(lspserver: dict<any>, content: dict<any>): void
+  var payload_js: string = content->json_encode()
+  var msg = "Content-Length: " .. payload_js->len() .. "\r\n\r\n"
+  var ch = lspserver.job->job_getchannel()
+  ch->ch_sendraw(msg)
+  ch->ch_sendraw(payload_js)
+enddef
+
+# Send a LSP "textDocument/didOpen" notification
+def s:textdocDidOpen(lspserver: dict<any>, bnr: number, ftype: string): void
+  var notif: dict<any> = lspserver.createNotification('textDocument/didOpen')
+
+  # interface DidOpenTextDocumentParams
+  # interface TextDocumentItem
+  var tdi = {}
+  tdi.uri = LspFileToUri(bufname(bnr))
+  tdi.languageId = ftype
+  tdi.version = 1
+  tdi.text = getbufline(bnr, 1, '$')->join("\n") .. "\n"
+  notif.params->extend({textDocument: tdi})
+
+  lspserver.sendMessage(notif)
+enddef
+
+# Send a LSP "textDocument/didClose" notification
+def s:textdocDidClose(lspserver: dict<any>, bnr: number): void
+  var notif: dict<any> = lspserver.createNotification('textDocument/didClose')
+
+  # interface DidCloseTextDocumentParams
+  #   interface TextDocumentIdentifier
+  var tdid = {}
+  tdid.uri = LspFileToUri(bufname(bnr))
+  notif.params->extend({textDocument: tdid})
+
+  lspserver.sendMessage(notif)
+enddef
+
+# Send a LSP "textDocument/didChange" notification
+def s:textdocDidChange(lspserver: dict<any>, bnr: number, start: number,
+                       end: number, added: number,
+                       changes: list<dict<number>>): void
+  var notif: dict<any> = lspserver.createNotification('textDocument/didChange')
+
+  # interface DidChangeTextDocumentParams
+  #   interface VersionedTextDocumentIdentifier
+  var vtdid: dict<any> = {}
+  vtdid.uri = LspFileToUri(bufname(bnr))
+  # Use Vim 'changedtick' as the LSP document version number
+  vtdid.version = bnr->getbufvar('changedtick')
+  notif.params->extend({textDocument: vtdid})
+  #   interface TextDocumentContentChangeEvent
+  var changeset: list<dict<any>>
+
+  ##### FIXME: Sending specific buffer changes to the LSP server doesn't
+  ##### work properly as the computed line range numbers is not correct.
+  ##### For now, send the entire buffer content to LSP server.
+  # #     Range
+  # for change in changes
+  #   var lines: string
+  #   var start_lnum: number
+  #   var end_lnum: number
+  #   var start_col: number
+  #   var end_col: number
+  #   if change.added == 0
+  #     # lines changed
+  #     start_lnum =  change.lnum - 1
+  #     end_lnum = change.end - 1
+  #     lines = getbufline(bnr, change.lnum, change.end - 1)->join("\n") .. "\n"
+  #     start_col = 0
+  #     end_col = 0
+  #   elseif change.added > 0
+  #     # lines added
+  #     start_lnum = change.lnum - 1
+  #     end_lnum = change.lnum - 1
+  #     start_col = 0
+  #     end_col = 0
+  #     lines = getbufline(bnr, change.lnum, change.lnum + change.added - 1)->join("\n") .. "\n"
+  #   else
+  #     # lines removed
+  #     start_lnum = change.lnum - 1
+  #     end_lnum = change.lnum + (-change.added) - 1
+  #     start_col = 0
+  #     end_col = 0
+  #     lines = ''
+  #   endif
+  #   var range: dict<dict<number>> = {'start': {'line': start_lnum, 'character': start_col}, 'end': {'line': end_lnum, 'character': end_col}}
+  #   changeset->add({'range': range, 'text': lines})
+  # endfor
+  changeset->add({text: getbufline(bnr, 1, '$')->join("\n") .. "\n"})
+  notif.params->extend({contentChanges: changeset})
+
+  lspserver.sendMessage(notif)
+enddef
+
+# Return the current cursor position as a LSP position.
+# LSP line and column numbers start from zero, whereas Vim line and column
+# numbers start from one. The LSP column number is the character index in the
+# line and not the byte index in the line.
+def s:getLspPosition(): dict<number>
+  var lnum: number = line('.') - 1
+  #var col: number = strchars(getline('.')[: col('.') - 1]) - 1
+  var col: number = col('.') - 1
+  return {line: lnum, character: col}
+enddef
+
+# Return the current file name and current cursor position as a LSP
+# TextDocumentPositionParams structure
+def s:getLspTextDocPosition(): dict<dict<any>>
+  # interface TextDocumentIdentifier
+  # interface Position
+  return {textDocument: {uri: LspFileToUri(@%)},
+         position: s:getLspPosition()}
+enddef
+
+def s:getCompletion(lspserver: dict<any>): void
+  # Check whether LSP server supports completion
+  if !lspserver.caps->has_key('completionProvider')
+    ErrMsg("Error: LSP server does not support completion")
+    return
+  endif
+
+  var fname = @%
+  if fname == ''
+    return
+  endif
+
+  var req = lspserver.createRequest('textDocument/completion')
+
+  # interface CompletionParams
+  #   interface TextDocumentPositionParams
+  req.params->extend(s:getLspTextDocPosition())
+
+  lspserver.sendMessage(req)
+enddef
+
+# push the current location on to the tag stack
+def s:pushCursorToTagStack()
+  settagstack(winnr(), {items: [
+                        {
+                          bufnr: bufnr(),
+                          from: getpos('.'),
+                          matchnr: 1,
+                          tagname: expand('<cword>')
+                        }]}, 'a')
+enddef
+
+def s:gotoDefinition(lspserver: dict<any>): void
+  # Check whether LSP server supports jumping to a definition
+  if !lspserver.caps->has_key('definitionProvider')
+              || !lspserver.caps.definitionProvider
+    ErrMsg("Error: LSP server does not support jumping to a definition")
+    return
+  endif
+
+  s:pushCursorToTagStack()
+  var req = lspserver.createRequest('textDocument/definition')
+  # interface DefinitionParams
+  #   interface TextDocumentPositionParams
+  req.params->extend(s:getLspTextDocPosition())
+  lspserver.sendMessage(req)
+enddef
+
+def s:gotoDeclaration(lspserver: dict<any>): void
+  # Check whether LSP server supports jumping to a declaration
+  if !lspserver.caps->has_key('declarationProvider')
+              || !lspserver.caps.declarationProvider
+    ErrMsg("Error: LSP server does not support jumping to a declaration")
+    return
+  endif
+
+  s:pushCursorToTagStack()
+  var req = lspserver.createRequest('textDocument/declaration')
+
+  # interface DeclarationParams
+  #   interface TextDocumentPositionParams
+  req.params->extend(s:getLspTextDocPosition())
+
+  lspserver.sendMessage(req)
+enddef
+
+def s:gotoTypeDef(lspserver: dict<any>): void
+  # Check whether LSP server supports jumping to a type definition
+  if !lspserver.caps->has_key('typeDefinitionProvider')
+              || !lspserver.caps.typeDefinitionProvider
+    ErrMsg("Error: LSP server does not support jumping to a type definition")
+    return
+  endif
+
+  s:pushCursorToTagStack()
+  var req = lspserver.createRequest('textDocument/typeDefinition')
+
+  # interface TypeDefinitionParams
+  #   interface TextDocumentPositionParams
+  req.params->extend(s:getLspTextDocPosition())
+
+  lspserver.sendMessage(req)
+enddef
+
+def s:gotoImplementation(lspserver: dict<any>): void
+  # Check whether LSP server supports jumping to a implementation
+  if !lspserver.caps->has_key('implementationProvider')
+              || !lspserver.caps.implementationProvider
+    ErrMsg("Error: LSP server does not support jumping to an implementation")
+    return
+  endif
+
+  s:pushCursorToTagStack()
+  var req = lspserver.createRequest('textDocument/implementation')
+
+  # interface ImplementationParams
+  #   interface TextDocumentPositionParams
+  req.params->extend(s:getLspTextDocPosition())
+
+  lspserver.sendMessage(req)
+enddef
+
+def s:showSignature(lspserver: dict<any>): void
+  # Check whether LSP server supports signature help
+  if !lspserver.caps->has_key('signatureHelpProvider')
+    ErrMsg("Error: LSP server does not support signature help")
+    return
+  endif
+
+  var req = lspserver.createRequest('textDocument/signatureHelp')
+  # interface SignatureHelpParams
+  #   interface TextDocumentPositionParams
+  req.params->extend(s:getLspTextDocPosition())
+
+  lspserver.sendMessage(req)
+enddef
+
+def s:didSaveFile(lspserver: dict<any>): void
+  # Check whether the LSP server supports the didSave notification
+  if !lspserver.caps->has_key('textDocumentSync')
+               || lspserver.caps.textDocumentSync->type() == v:t_number
+               || !lspserver.caps.textDocumentSync->has_key('save')
+               || !lspserver.caps.textDocumentSync.save
+    # LSP server doesn't support text document synchronization
+    return
+  endif
+
+  var notif: dict<any> = lspserver.createNotification('textDocument/didSave')
+  # interface: DidSaveTextDocumentParams
+  notif.params->extend({textDocument: {uri: LspFileToUri(bufname(bnr))}})
+  lspserver.sendMessage(notif)
+enddef
+
+def s:hover(lspserver: dict<any>): void
+  # Check whether LSP server supports getting hover information
+  if !lspserver.caps->has_key('hoverProvider')
+              || !lspserver.caps.hoverProvider
+    return
+  endif
+
+  var req = lspserver.createRequest('textDocument/hover')
+  # interface HoverParams
+  #   interface TextDocumentPositionParams
+  req.params->extend(s:getLspTextDocPosition())
+  lspserver.sendMessage(req)
+enddef
+
+def s:showReferences(lspserver: dict<any>): void
+  # Check whether LSP server supports getting reference information
+  if !lspserver.caps->has_key('referencesProvider')
+              || !lspserver.caps.referencesProvider
+    ErrMsg("Error: LSP server does not support showing references")
+    return
+  endif
+
+  var req = lspserver.createRequest('textDocument/references')
+  # interface ReferenceParams
+  #   interface TextDocumentPositionParams
+  req.params->extend(s:getLspTextDocPosition())
+  req.params->extend({context: {includeDeclaration: v:true}})
+
+  lspserver.sendMessage(req)
+enddef
+
+def s:docHighlight(lspserver: dict<any>): void
+  # Check whether LSP server supports getting highlight information
+  if !lspserver.caps->has_key('documentHighlightProvider')
+              || !lspserver.caps.documentHighlightProvider
+    ErrMsg("Error: LSP server does not support document highlight")
+    return
+  endif
+
+  var req = lspserver.createRequest('textDocument/documentHighlight')
+  # interface DocumentHighlightParams
+  #   interface TextDocumentPositionParams
+  req.params->extend(s:getLspTextDocPosition())
+  lspserver.sendMessage(req)
+enddef
+
+def s:showDocSymbols(lspserver: dict<any>, fname: string): void
+  # Check whether LSP server supports getting document symbol information
+  if !lspserver.caps->has_key('documentSymbolProvider')
+              || !lspserver.caps.documentSymbolProvider
+    ErrMsg("Error: LSP server does not support getting list of symbols")
+    return
+  endif
+
+  var req = lspserver.createRequest('textDocument/documentSymbol')
+  # interface DocumentSymbolParams
+  # interface TextDocumentIdentifier
+  req.params->extend({textDocument: {uri: LspFileToUri(fname)}})
+  lspserver.sendMessage(req)
+enddef
+
+def s:textDocFormat(lspserver: dict<any>, fname: string, rangeFormat: bool,
+                               start_lnum: number, end_lnum: number)
+  # Check whether LSP server supports formatting documents
+  if !lspserver.caps->has_key('documentFormattingProvider')
+              || !lspserver.caps.documentFormattingProvider
+    ErrMsg("Error: LSP server does not support formatting documents")
+    return
+  endif
+
+  var cmd: string
+  if rangeFormat
+    cmd = 'textDocument/rangeFormatting'
+  else
+    cmd = 'textDocument/formatting'
+  endif
+  var req = lspserver.createRequest(cmd)
+
+  # interface DocumentFormattingParams
+  # interface TextDocumentIdentifier
+  req.params->extend({textDocument: {uri: LspFileToUri(fname)}})
+  var tabsz: number
+  if &sts > 0
+    tabsz = &sts
+  elseif &sts < 0
+    tabsz = &shiftwidth
+  else
+    tabsz = &tabstop
+  endif
+  # interface FormattingOptions
+  var fmtopts: dict<any> = {
+    tabSize: tabsz,
+    insertSpaces: &expandtab ? v:true : v:false,
+  }
+  req.params->extend({options: fmtopts})
+  if rangeFormat
+    var r: dict<dict<number>> = {
+       start: {line: start_lnum - 1, character: 0},
+       end: {line: end_lnum, character: 0}}
+    req.params->extend({range: r})
+  endif
+
+  lspserver.sendMessage(req)
+enddef
+
+def s:renameSymbol(lspserver: dict<any>, newName: string)
+  # Check whether LSP server supports rename operation
+  if !lspserver.caps->has_key('renameProvider')
+              || !lspserver.caps.renameProvider
+    ErrMsg("Error: LSP server does not support rename operation")
+    return
+  endif
+
+  var req = lspserver.createRequest('textDocument/rename')
+  # interface RenameParams
+  #   interface TextDocumentPositionParams
+  req.params->extend(s:getLspTextDocPosition())
+  req.params->extend({newName: newName})
+  lspserver.sendMessage(req)
+enddef
+
+def s:codeAction(lspserver: dict<any>, fname_arg: string)
+  # Check whether LSP server supports code action operation
+  if !lspserver.caps->has_key('codeActionProvider')
+              || !lspserver.caps.codeActionProvider
+    ErrMsg("Error: LSP server does not support code action operation")
+    return
+  endif
+
+  var req = lspserver.createRequest('textDocument/codeAction')
+
+  # interface CodeActionParams
+  var fname: string = fnamemodify(fname_arg, ':p')
+  req.params->extend({textDocument: {uri: LspFileToUri(fname)}})
+  var r: dict<dict<number>> = {
+                 start: {line: line('.') - 1, character: col('.') - 1},
+                 end: {line: line('.') - 1, character: col('.') - 1}}
+  req.params->extend({range: r})
+  var diag: list<dict<any>> = []
+  var lnum = line('.')
+  if lspserver.diagsMap->has_key(fname) &&
+                               lspserver.diagsMap[fname]->has_key(lnum)
+    diag->add(lspserver.diagsMap[fname][lnum])
+  endif
+  req.params->extend({context: {diagnostics: diag}})
+
+  lspserver.sendMessage(req)
+enddef
+
+def s:workspaceSymbols(lspserver: dict<any>, sym: string)
+  # Check whether the LSP server supports listing workspace symbols
+  if !lspserver.caps->has_key('workspaceSymbolProvider')
+              || !lspserver.caps.workspaceSymbolProvider
+    ErrMsg("Error: LSP server does not support listing workspace symbols")
+    return
+  endif
+
+  var req = lspserver.createRequest('workspace/symbol')
+  req.params->extend({query: sym})
+  lspserver.sendMessage(req)
+enddef
+
+def s:addWorkspaceFolder(lspserver: dict<any>, dirName: string): void
+  if !lspserver.caps->has_key('workspace')
+         || !lspserver.caps.workspace->has_key('workspaceFolders')
+         || !lspserver.caps.workspace.workspaceFolders->has_key('supported')
+         || !lspserver.caps.workspace.workspaceFolders.supported
+      ErrMsg('Error: LSP server does not support workspace folders')
+    return
+  endif
+
+  if lspserver.workspaceFolders->index(dirName) != -1
+    ErrMsg('Error: ' .. dirName .. ' is already part of this workspace')
+    return
+  endif
+
+  var notif: dict<any> =
+       lspserver.createNotification('workspace/didChangeWorkspaceFolders')
+  # interface DidChangeWorkspaceFoldersParams
+  notif.params->extend({event: {added: [dirName], removed: []}})
+  lspserver.sendMessage(notif)
+
+  lspserver.workspaceFolders->add(dirName)
+enddef
+
+def s:removeWorkspaceFolder(lspserver: dict<any>, dirName: string): void
+  if !lspserver.caps->has_key('workspace')
+         || !lspserver.caps.workspace->has_key('workspaceFolders')
+         || !lspserver.caps.workspace.workspaceFolders->has_key('supported')
+         || !lspserver.caps.workspace.workspaceFolders.supported
+      ErrMsg('Error: LSP server does not support workspace folders')
+    return
+  endif
+
+  var idx: number = lspserver.workspaceFolders->index(dirName)
+  if idx == -1
+    ErrMsg('Error: ' .. dirName .. ' is not currently part of this workspace')
+    return
+  endif
+
+  var notif: dict<any> =
+       lspserver.createNotification('workspace/didChangeWorkspaceFolders')
+  # interface DidChangeWorkspaceFoldersParams
+  notif.params->extend({event: {added: [], removed: [dirName]}})
+  lspserver.sendMessage(notif)
+
+  lspserver.workspaceFolders->remove(idx)
+enddef
+
+export def NewLspServer(path: string, args: list<string>): dict<any>
+  var lspserver: dict<any> = {
+    path: path,
+    args: args,
+    running: v:false,
+    job: v:none,
+    data: '',
+    nextID: 1,
+    caps: {},
+    requests: {},
+    completePending: v:false,
+    diagsMap: {}
+  }
+  # Add the LSP server functions
+  lspserver->extend({
+    startServer: function('s:startServer', [lspserver]),
+    initServer: function('s:initServer', [lspserver]),
+    stopServer: function('s:stopServer', [lspserver]),
+    shutdownServer: function('s:shutdownServer', [lspserver]),
+    exitServer: function('s:exitServer', [lspserver]),
+    setTrace: function('s:setTrace', [lspserver]),
+    nextReqID: function('s:nextReqID', [lspserver]),
+    createRequest: function('s:createRequest', [lspserver]),
+    createResponse: function('s:createResponse', [lspserver]),
+    createNotification: function('s:createNotification', [lspserver]),
+    sendResponse: function('s:sendResponse', [lspserver]),
+    sendMessage: function('s:sendMessage', [lspserver]),
+    processReply: function('ProcessReply', [lspserver]),
+    processNotif: function('ProcessNotif', [lspserver]),
+    processRequest: function('ProcessRequest', [lspserver]),
+    processMessages: function('ProcessMessages', [lspserver]),
+    textdocDidOpen: function('s:textdocDidOpen', [lspserver]),
+    textdocDidClose: function('s:textdocDidClose', [lspserver]),
+    textdocDidChange: function('s:textdocDidChange', [lspserver]),
+    sendInitializedNotif: function('s:sendInitializedNotif', [lspserver]),
+    getCompletion: function('s:getCompletion', [lspserver]),
+    gotoDefinition: function('s:gotoDefinition', [lspserver]),
+    gotoDeclaration: function('s:gotoDeclaration', [lspserver]),
+    gotoTypeDef: function('s:gotoTypeDef', [lspserver]),
+    gotoImplementation: function('s:gotoImplementation', [lspserver]),
+    showSignature: function('s:showSignature', [lspserver]),
+    didSaveFile: function('s:didSaveFile', [lspserver]),
+    hover: function('s:hover', [lspserver]),
+    showReferences: function('s:showReferences', [lspserver]),
+    docHighlight: function('s:docHighlight', [lspserver]),
+    showDocSymbols: function('s:showDocSymbols', [lspserver]),
+    textDocFormat: function('s:textDocFormat', [lspserver]),
+    renameSymbol: function('s:renameSymbol', [lspserver]),
+    codeAction: function('s:codeAction', [lspserver]),
+    workspaceSymbols: function('s:workspaceSymbols', [lspserver]),
+    addWorkspaceFolder: function('s:addWorkspaceFolder', [lspserver]),
+    removeWorkspaceFolder: function('s:removeWorkspaceFolder', [lspserver])
+  })
+
+  return lspserver
+enddef
+
+# vim: shiftwidth=2 softtabstop=2
diff --git a/autoload/util.vim b/autoload/util.vim
new file mode 100644 (file)
index 0000000..7a75046
--- /dev/null
@@ -0,0 +1,91 @@
+vim9script
+
+# Display a warning message
+export def WarnMsg(msg: string)
+  :echohl WarningMsg
+  :echomsg msg
+  :echohl None
+enddef
+
+# Display an error message
+export def ErrMsg(msg: string)
+  :echohl Error
+  :echomsg msg
+  :echohl None
+enddef
+
+# Lsp server trace log directory
+var lsp_log_dir: string
+if has('unix')
+  lsp_log_dir = '/tmp/'
+else
+  lsp_log_dir = $TEMP .. '\\'
+endif
+export var lsp_server_trace: bool = v:false
+
+# Log a message from the LSP server. stderr is v:true for logging messages
+# from the standard error and v:false for stdout.
+export def TraceLog(stderr: bool, msg: string)
+  if !lsp_server_trace
+    return
+  endif
+  if stderr
+    writefile(split(msg, "\n"), lsp_log_dir .. 'lsp_server.err', 'a')
+  else
+    writefile(split(msg, "\n"), lsp_log_dir .. 'lsp_server.out', 'a')
+  endif
+enddef
+
+# Empty out the LSP server trace logs
+export def ClearTraceLogs()
+  if !lsp_server_trace
+    return
+  endif
+  writefile([], lsp_log_dir .. 'lsp_server.out')
+  writefile([], lsp_log_dir .. 'lsp_server.err')
+enddef
+
+# Convert a LSP file URI (file://<absolute_path>) to a Vim file name
+export def LspUriToFile(uri: string): string
+  # Replace all the %xx numbers (e.g. %20 for space) in the URI to character
+  var uri_decoded: string = substitute(uri, '%\(\x\x\)',
+                             '\=nr2char(str2nr(submatch(1), 16))', 'g')
+
+  # File URIs on MS-Windows start with file:///[a-zA-Z]:'
+  if uri_decoded =~? '^file:///\a:'
+    # MS-Windows URI
+    uri_decoded = uri_decoded[8:]
+    uri_decoded = uri_decoded->substitute('/', '\\', 'g')
+  else
+    uri_decoded = uri_decoded[7:]
+  endif
+
+  return uri_decoded
+enddef
+
+# Convert a Vim filenmae to an LSP URI (file://<absolute_path>)
+export def LspFileToUri(fname: string): string
+  var uri: string = fnamemodify(fname, ':p')
+
+  var on_windows: bool = v:false
+  if uri =~? '^\a:'
+    on_windows = v:true
+  endif
+
+  if on_windows
+    # MS-Windows
+    uri = uri->substitute('\\', '/', 'g')
+  endif
+
+  uri = uri->substitute('\([^A-Za-z0-9-._~:/]\)',
+                     '\=printf("%%%02x", char2nr(submatch(1)))', 'g')
+
+  if on_windows
+    uri = 'file:///' .. uri
+  else
+    uri = 'file://' .. uri
+  endif
+
+  return uri
+enddef
+