]> Sergey Matveev's repositories - vim-lsp.git/commitdiff
Add support for UTF-8 and UTF-16 offset encoding
authorYegappan Lakshmanan <yegappan@yahoo.com>
Sat, 17 Jun 2023 01:55:26 +0000 (18:55 -0700)
committerYegappan Lakshmanan <yegappan@yahoo.com>
Sat, 17 Jun 2023 01:55:26 +0000 (18:55 -0700)
autoload/lsp/capabilities.vim
autoload/lsp/codelens.vim
autoload/lsp/diag.vim
autoload/lsp/inlayhints.vim
autoload/lsp/lspserver.vim
autoload/lsp/offset.vim [new file with mode: 0644]
autoload/lsp/symbol.vim
doc/lsp.txt
test/clangd_offsetencoding.vim [new file with mode: 0644]
test/clangd_tests.vim
test/run_tests.sh

index 28a121526493c33d46bd1c6e08b1f5688136ae2b..9df58deded31714a3b23934964dcc673d86e8036 100644 (file)
@@ -7,6 +7,28 @@ import './options.vim' as opt
 # Process the server capabilities
 #   interface ServerCapabilities
 export def ProcessServerCaps(lspserver: dict<any>, caps: dict<any>)
+  var serverEncoding = 'utf-16'
+  if lspserver.caps->has_key('positionEncoding')
+    serverEncoding = lspserver.caps.positionEncoding
+  elseif lspserver.caps->has_key('~additionalInitResult_offsetEncoding')
+    serverEncoding = lspserver.caps['~additionalInitResult_offsetEncoding']
+  endif
+
+  # one of 'utf-8', 'utf-16' or 'utf-32'
+  if serverEncoding == 'utf-8'
+    lspserver.posEncoding = 8
+  elseif serverEncoding == 'utf-16'
+    lspserver.posEncoding = 16
+  else
+    lspserver.posEncoding = 32
+  endif
+
+  if has('patch-9.0.1629') && lspserver.posEncoding != 32
+    lspserver.needOffsetEncoding = true
+  else
+    lspserver.needOffsetEncoding = false
+  endif
+
   # completionProvider
   if lspserver.caps->has_key('completionProvider')
     lspserver.isCompletionProvider = true
@@ -338,6 +360,12 @@ export def GetClientCaps(): dict<any>
     offsetEncoding: ['utf-32', 'utf-16']
   }
 
+  # Vim patch 1629 is needed to properly encode/decode UTF-16 offsets
+  if has('patch-9.0.1629')
+    clientCaps.general.positionEncodings = ['utf-32', 'utf-16', 'utf-8']
+    clientCaps.offsetEncoding = ['utf-32', 'utf-16', 'utf-8']
+  endif
+
   return clientCaps
 enddef
 
index 257166540fe81ef15090d27279e012ffed03cecc..082edf7438ab7c842ac237966e5a7a8fca145b84 100644 (file)
@@ -4,13 +4,13 @@ import './codeaction.vim'
 
 # Functions related to handling LSP code lens
 
-export def ProcessCodeLens(lspserver: dict<any>, codeLensItems: list<dict<any>>)
+export def ProcessCodeLens(lspserver: dict<any>, bnr: number, codeLensItems: list<dict<any>>)
   var text: list<string> = []
   for i in codeLensItems->len()->range()
     var item = codeLensItems[i]
     if !item->has_key('command')
       # resolve the code lens
-      item = lspserver.resolveCodeLens(item)
+      item = lspserver.resolveCodeLens(bnr, item)
       if item->empty()
        continue
       endif
index 45cf8208538bf909dd4273116094939a7793a4a6..9eaa30c07f25713f05516c336e9ed4926d2e1dd8 100644 (file)
@@ -309,6 +309,15 @@ export def DiagNotification(lspserver: dict<any>, uri: string, diags_arg: list<d
   endif
 
   var newDiags: list<dict<any>> = diags_arg
+
+  if lspserver.needOffsetEncoding
+    # Decode the position encoding in all the diags
+    newDiags->map((_, dval) => {
+       lspserver.decodeRange(bnr, dval.range)
+       return dval
+      })
+  endif
+
   if lspserver.processDiagHandler != null_function
     newDiags = lspserver.processDiagHandler(diags_arg)
   endif
index 07c0a1bfa593ecb58f29130cbc1c5d4cee3b9f19..87cd8c620299d01207c88ba22ca5bb1d34f480da 100644 (file)
@@ -45,6 +45,7 @@ export def InlayHintsReply(lspserver: dict<any>, inlayHints: any)
 
     var kind = hint->has_key('kind') ? hint.kind->string() : '1'
     try
+      lspserver.decodePosition(bnr, hint.position)
       var byteIdx = util.GetLineByteFromPos(bnr, hint.position)
       if kind == "'type'" || kind == '1'
        prop_add(hint.position.line + 1, byteIdx + 1,
index c2d07639ea9296f6de7e8bef42e478c28a122ee1..78d814156e7f64cc11acc1f7c0e05de005f2e4a4 100644 (file)
@@ -12,6 +12,7 @@ import './options.vim' as opt
 import './handlers.vim'
 import './util.vim'
 import './capabilities.vim'
+import './offset.vim'
 import './diag.vim'
 import './selection.vim'
 import './symbol.vim'
@@ -354,6 +355,36 @@ def SendNotification(lspserver: dict<any>, method: string, params: any = {})
   lspserver.sendMessage(notif)
 enddef
 
+# Translate an LSP error code into a readable string
+def LspGetErrorMessage(errcode: number): string
+  var errmap = {
+    -32001: 'UnknownErrorCode',
+    -32002: 'ServerNotInitialized',
+    -32600: 'InvalidRequest',
+    -32601: 'MethodNotFound',
+    -32602: 'InvalidParams',
+    -32603: 'InternalError',
+    -32700: 'ParseError',
+    -32800: 'RequestCancelled',
+    -32801: 'ContentModified',
+    -32802: 'ServerCancelled',
+    -32803: 'RequestFailed'
+  }
+
+  return errmap->get(errcode, errcode->string())
+enddef
+
+# Process a LSP server response error and display an error message.
+def ProcessLspServerError(method: string, responseError: dict<any>)
+  # request failed
+  var emsg: string = responseError.message
+  emsg ..= $', error = {LspGetErrorMessage(responseError.code)}'
+  if responseError->has_key('data')
+    emsg ..= $', data = {responseError.data->string()}'
+  endif
+  util.ErrMsg($'request {method} failed ({emsg})')
+enddef
+
 # Send a sync RPC request message to the LSP server and return the received
 # reply.  In case of an error, an empty Dict is returned.
 def Rpc(lspserver: dict<any>, method: string, params: any, handleError: bool = true): dict<any>
@@ -382,12 +413,7 @@ def Rpc(lspserver: dict<any>, method: string, params: any, handleError: bool = t
 
   if reply->has_key('error') && handleError
     # request failed
-    var emsg: string = reply.error.message
-    emsg ..= $', code = {reply.error.code}'
-    if reply.error->has_key('data')
-      emsg ..= $', data = {reply.error.data->string()}'
-    endif
-    util.ErrMsg($'request {method} failed ({emsg})')
+    ProcessLspServerError(method, reply.error)
   endif
 
   return {}
@@ -403,12 +429,7 @@ def AsyncRpcCb(lspserver: dict<any>, method: string, RpcCb: func, chan: channel,
 
   if reply->has_key('error')
     # request failed
-    var emsg: string
-    emsg = $'{reply.error.message}, code = {reply.error.code}'
-    if reply.error->has_key('data')
-      emsg ..= $', data = {reply.error.data->string()}'
-    endif
-    util.ErrMsg($'request {method} failed ({emsg})')
+    ProcessLspServerError(method, reply.error)
     return
   endif
 
@@ -580,7 +601,7 @@ enddef
 # 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 GetLspPosition(find_ident: bool): dict<number>
+def GetPosition(lspserver: dict<any>, find_ident: bool): dict<number>
   var lnum: number = line('.') - 1
   var col: number = charcol('.') - 1
   var line = getline('.')
@@ -599,16 +620,19 @@ def GetLspPosition(find_ident: bool): dict<number>
 
   # Compute character index counting composing characters as separate
   # characters
-  return {line: lnum, character: util.GetCharIdxWithCompChar(line, col)}
+  var pos = {line: lnum, character: util.GetCharIdxWithCompChar(line, col)}
+  lspserver.encodePosition(bufnr(), pos)
+
+  return pos
 enddef
 
 # Return the current file name and current cursor position as a LSP
 # TextDocumentPositionParams structure
-def GetLspTextDocPosition(find_ident: bool): dict<dict<any>>
+def GetTextDocPosition(lspserver: dict<any>, find_ident: bool): dict<dict<any>>
   # interface TextDocumentIdentifier
   # interface Position
   return {textDocument: {uri: util.LspFileToUri(@%)},
-         position: GetLspPosition(find_ident)}
+         position: lspserver.getPosition(find_ident)}
 enddef
 
 # Get a list of completion items.
@@ -628,7 +652,7 @@ def GetCompletion(lspserver: dict<any>, triggerKind_arg: number, triggerChar: st
 
   # interface CompletionParams
   #   interface TextDocumentPositionParams
-  var params = GetLspTextDocPosition(false)
+  var params = lspserver.getTextDocPosition(false)
   #   interface CompletionContext
   params.context = {triggerKind: triggerKind_arg, triggerCharacter: triggerChar}
 
@@ -670,7 +694,7 @@ enddef
 # Result: Location | Location[] | LocationLink[] | null
 def GotoSymbolLoc(lspserver: dict<any>, msg: string, peekSymbol: bool,
                  cmdmods: string, count: number)
-  var reply = lspserver.rpc(msg, GetLspTextDocPosition(true), false)
+  var reply = lspserver.rpc(msg, lspserver.getTextDocPosition(true), false)
   if reply->empty() || reply.result->empty()
     var emsg: string
     if msg == 'textDocument/declaration'
@@ -687,12 +711,13 @@ def GotoSymbolLoc(lspserver: dict<any>, msg: string, peekSymbol: bool,
     return
   endif
 
+  var result = reply.result
   var location: dict<any>
-  if reply.result->type() == v:t_list
+  if result->type() == v:t_list
     if count == 0
       # When there are multiple symbol locations, and a specific one isn't
       # requested with 'count', display the locations in a location list.
-      if reply.result->len() > 1
+      if result->len() > 1
         var title: string = ''
         if msg == 'textDocument/declaration'
           title = 'Declarations'
@@ -704,20 +729,29 @@ def GotoSymbolLoc(lspserver: dict<any>, msg: string, peekSymbol: bool,
           title = 'Definitions'
         endif
 
-        symbol.ShowLocations(lspserver, reply.result, peekSymbol, title)
+       if lspserver.needOffsetEncoding
+         # Decode the position encoding in all the symbol locations
+         result->map((_, loc) => {
+             lspserver.decodeLocation(loc)
+             return loc
+           })
+       endif
+
+        symbol.ShowLocations(lspserver, result, peekSymbol, title)
         return
       endif
     endif
 
     # Select the location requested in 'count'
     var idx = count - 1
-    if idx >= reply.result->len()
-      idx = reply.result->len() - 1
+    if idx >= result->len()
+      idx = result->len() - 1
     endif
-    location = reply.result[idx]
+    location = result[idx]
   else
-    location = reply.result
+    location = result
   endif
+  lspserver.decodeLocation(location)
 
   symbol.GotoSymbol(lspserver, location, peekSymbol, cmdmods)
 enddef
@@ -814,7 +848,7 @@ def ShowSignature(lspserver: dict<any>): void
 
   # interface SignatureHelpParams
   #   interface TextDocumentPositionParams
-  var params = GetLspTextDocPosition(false)
+  var params = lspserver.getTextDocPosition(false)
   lspserver.rpc_a('textDocument/signatureHelp', params,
                        signature.SignatureHelp)
 enddef
@@ -847,7 +881,7 @@ def ShowHoverInfo(lspserver: dict<any>, cmdmods: string): void
 
   # interface HoverParams
   #   interface TextDocumentPositionParams
-  var params = GetLspTextDocPosition(false)
+  var params = lspserver.getTextDocPosition(false)
   lspserver.rpc_a('textDocument/hover', params, (_, reply) => {
     hover.HoverReply(lspserver, reply, cmdmods)
   })
@@ -865,7 +899,7 @@ def ShowReferences(lspserver: dict<any>, peek: bool): void
   # interface ReferenceParams
   #   interface TextDocumentPositionParams
   var param: dict<any>
-  param = GetLspTextDocPosition(true)
+  param = lspserver.getTextDocPosition(true)
   param.context = {includeDeclaration: true}
   var reply = lspserver.rpc('textDocument/references', param)
 
@@ -875,6 +909,14 @@ def ShowReferences(lspserver: dict<any>, peek: bool): void
     return
   endif
 
+  if lspserver.needOffsetEncoding
+    # Decode the position encoding in all the reference locations
+    reply.result->map((_, loc) => {
+      lspserver.decodeLocation(loc)
+      return loc
+    })
+  endif
+
   symbol.ShowLocations(lspserver, reply.result, peek, 'Symbol References')
 enddef
 
@@ -890,6 +932,7 @@ def DocHighlightReply(lspserver: dict<any>, docHighlightReply: any,
   endif
 
   for docHL in docHighlightReply
+    lspserver.decodeRange(bnr, docHL.range)
     var kind: number = docHL->get('kind', 1)
     var propName: string
     if kind == 2
@@ -928,7 +971,7 @@ def DocHighlight(lspserver: dict<any>, cmdmods: string): void
 
   # interface DocumentHighlightParams
   #   interface TextDocumentPositionParams
-  var params = GetLspTextDocPosition(false)
+  var params = lspserver.getTextDocPosition(false)
   lspserver.rpc_a('textDocument/documentHighlight', params, (_, reply) => {
     DocHighlightReply(lspserver, reply, bufnr(), cmdmods)
   })
@@ -1004,6 +1047,14 @@ def TextDocFormat(lspserver: dict<any>, fname: string, rangeFormat: bool,
     return
   endif
 
+  if lspserver.needOffsetEncoding
+    # Decode the position encoding in all the reference locations
+    reply.result->map((_, textEdit) => {
+      lspserver.decodeRange(bnr, textEdit.range)
+      return textEdit
+    })
+  endif
+
   # interface TextEdit
   # Apply each of the text edit operations
   var save_cursor: list<number> = getcurpos()
@@ -1016,7 +1067,7 @@ def PrepareCallHierarchy(lspserver: dict<any>): dict<any>
   # interface CallHierarchyPrepareParams
   #   interface TextDocumentPositionParams
   var param: dict<any>
-  param = GetLspTextDocPosition(false)
+  param = lspserver.getTextDocPosition(false)
   var reply = lspserver.rpc('textDocument/prepareCallHierarchy', param)
   if reply->empty() || reply.result->empty()
     return {}
@@ -1058,6 +1109,16 @@ def GetIncomingCalls(lspserver: dict<any>, item: dict<any>): any
   if reply->empty()
     return null
   endif
+
+  if lspserver.needOffsetEncoding
+    # Decode the position encoding in all the incoming call locations
+    var bnr = util.LspUriToBufnr(item.uri)
+    reply.result->map((_, hierItem) => {
+      lspserver.decodeRange(bnr, hierItem.from.range)
+      return hierItem
+    })
+  endif
+
   return reply.result
 enddef
 
@@ -1081,6 +1142,16 @@ def GetOutgoingCalls(lspserver: dict<any>, item: dict<any>): any
   if reply->empty()
     return null
   endif
+
+  if lspserver.needOffsetEncoding
+    # Decode the position encoding in all the outgoing call locations
+    var bnr = util.LspUriToBufnr(item.uri)
+    reply.result->map((_, hierItem) => {
+      lspserver.decodeRange(bnr, hierItem.to.range)
+      return hierItem
+    })
+  endif
+
   return reply.result
 enddef
 
@@ -1103,6 +1174,8 @@ def InlayHintsShow(lspserver: dict<any>)
       }
   }
 
+  lspserver.encodeRange(bufnr(), param.range)
+
   var msg: string
   if lspserver.isClangdInlayHintsProvider
     # clangd-style inlay hints
@@ -1113,6 +1186,28 @@ def InlayHintsShow(lspserver: dict<any>)
   var reply = lspserver.rpc_a(msg, param, inlayhints.InlayHintsReply)
 enddef
 
+def DecodeTypeHierarchy(lspserver: dict<any>, isSuper: bool, typeHier: dict<any>)
+  if !lspserver.needOffsetEncoding
+    return
+  endif
+  var bnr = util.LspUriToBufnr(typeHier.uri)
+  lspserver.decodeRange(bnr, typeHier.range)
+  lspserver.decodeRange(bnr, typeHier.selectionRange)
+  var subType: list<dict<any>>
+  if isSuper
+    subType = typeHier->get('parents', [])
+  else
+    subType = typeHier->get('children', [])
+  endif
+  if !subType->empty()
+    # Decode the position encoding in all the type hierarchy items
+    subType->map((_, typeHierItem) => {
+        DecodeTypeHierarchy(lspserver, isSuper, typeHierItem)
+       return typeHierItem
+      })
+  endif
+enddef
+
 # Request: "textDocument/typehierarchy"
 # Support the clangd version of type hierarchy retrieval method.
 # The method described in the LSP 3.17.0 standard is not supported as clangd
@@ -1127,7 +1222,7 @@ def TypeHiearchy(lspserver: dict<any>, direction: number)
   # interface TypeHierarchy
   #   interface TextDocumentPositionParams
   var param: dict<any>
-  param = GetLspTextDocPosition(false)
+  param = lspserver.getTextDocPosition(false)
   # 0: children, 1: parent, 2: both
   param.direction = direction
   param.resolve = 5
@@ -1137,7 +1232,47 @@ def TypeHiearchy(lspserver: dict<any>, direction: number)
     return
   endif
 
-  typehier.ShowTypeHierarchy(lspserver, direction == 1, reply.result)
+  var isSuper = (direction == 1)
+
+  DecodeTypeHierarchy(lspserver, isSuper, reply.result)
+
+  typehier.ShowTypeHierarchy(lspserver, isSuper, reply.result)
+enddef
+
+# Decode the ranges in "WorkspaceEdit"
+def DecodeWorkspaceEdit(lspserver: dict<any>, workspaceEdit: dict<any>)
+  if !lspserver.needOffsetEncoding
+    return
+  endif
+  if workspaceEdit->has_key('changes')
+    for [uri, changes] in workspaceEdit.changes->items()
+      var bnr: number = util.LspUriToBufnr(uri)
+      if bnr <= 0
+       continue
+      endif
+      # Decode the position encoding in all the text edit locations
+      changes->map((_, textEdit) => {
+       lspserver.decodeRange(bnr, textEdit.range)
+       return textEdit
+      })
+    endfor
+  endif
+
+  if workspaceEdit->has_key('documentChanges')
+    for change in workspaceEdit.documentChanges
+      if !change->has_key('kind')
+       var bnr: number = util.LspUriToBufnr(change.textDocument.uri)
+       if bnr <= 0
+         continue
+       endif
+       # Decode the position encoding in all the text edit locations
+       change.edits->map((_, textEdit) => {
+         lspserver.decodeRange(bnr, textEdit.range)
+         return textEdit
+       })
+      endif
+    endfor
+  endif
 enddef
 
 # Request: "textDocument/rename"
@@ -1152,7 +1287,7 @@ def RenameSymbol(lspserver: dict<any>, newName: string)
   # interface RenameParams
   #   interface TextDocumentPositionParams
   var param: dict<any> = {}
-  param = GetLspTextDocPosition(true)
+  param = lspserver.getTextDocPosition(true)
   param.newName = newName
 
   var reply = lspserver.rpc('textDocument/rename', param)
@@ -1164,9 +1299,23 @@ def RenameSymbol(lspserver: dict<any>, newName: string)
   endif
 
   # result: WorkspaceEdit
+  DecodeWorkspaceEdit(lspserver, reply.result)
   textedit.ApplyWorkspaceEdit(reply.result)
 enddef
 
+# Decode the range in "CodeAction"
+def DecodeCodeAction(lspserver: dict<any>, actionList: list<dict<any>>)
+  if !lspserver.needOffsetEncoding
+    return
+  endif
+  actionList->map((_, act) => {
+      if !act->has_key('disabled') && act->has_key('edit')
+       DecodeWorkspaceEdit(lspserver, act.edit)
+      endif
+      return act
+    })
+enddef
+
 # Request: "textDocument/codeAction"
 # Param: CodeActionParams
 def CodeAction(lspserver: dict<any>, fname_arg: string, line1: number,
@@ -1184,17 +1333,24 @@ def CodeAction(lspserver: dict<any>, fname_arg: string, line1: number,
   var r: dict<dict<number>> = {
     start: {
       line: line1 - 1,
-      character: line1 == line2 ? charcol('.') - 1 : 0
+      character: line1 == line2 ? util.GetCharIdxWithCompChar(getline('.'), charcol('.') - 1) : 0
     },
     end: {
       line: line2 - 1,
-      character: charcol([line2, '$']) - 1
+      character: util.GetCharIdxWithCompChar(getline(line2), charcol([line2, '$']) - 1)
     }
   }
+  lspserver.encodeRange(bnr, r)
   params->extend({textDocument: {uri: util.LspFileToUri(fname)}, range: r})
   var d: list<dict<any>> = []
   for lnum in range(line1, line2)
-    var diagsInfo: list<dict<any>> = diag.GetDiagsByLine(bnr, lnum, lspserver)
+    var diagsInfo: list<dict<any>> = diag.GetDiagsByLine(bnr, lnum, lspserver)->deepcopy()
+    if lspserver.needOffsetEncoding
+      diagsInfo->map((_, di) => {
+         lspserver.encodeRange(bnr, di.range)
+         return di
+       })
+    endif
     d->extend(diagsInfo)
   endfor
   params->extend({context: {diagnostics: d, triggerKind: 1}})
@@ -1208,6 +1364,8 @@ def CodeAction(lspserver: dict<any>, fname_arg: string, line1: number,
     return
   endif
 
+  DecodeCodeAction(lspserver, reply.result)
+
   codeaction.ApplyCodeAction(lspserver, reply.result, query)
 enddef
 
@@ -1227,20 +1385,44 @@ def CodeLens(lspserver: dict<any>, fname: string)
     return
   endif
 
-  codelens.ProcessCodeLens(lspserver, reply.result)
+  var bnr = fname->bufnr()
+
+  # Decode the position encoding in all the code lens items
+  if lspserver.needOffsetEncoding
+    reply.result->map((_, codeLensItem) => {
+      lspserver.decodeRange(bnr, codeLensItem.range)
+      return codeLensItem
+    })
+  endif
+
+  codelens.ProcessCodeLens(lspserver, bnr, reply.result)
 enddef
 
 # Request: "codeLens/resolve"
 # Param: CodeLens
-def ResolveCodeLens(lspserver: dict<any>, codeLens: dict<any>): dict<any>
+def ResolveCodeLens(lspserver: dict<any>, bnr: number,
+                   codeLens: dict<any>): dict<any>
   if !lspserver.isCodeLensResolveProvider
     return {}
   endif
+
+  if lspserver.needOffsetEncoding
+    lspserver.encodeRange(bnr, codeLens.range)
+  endif
+
   var reply = lspserver.rpc('codeLens/resolve', codeLens)
   if reply->empty()
     return {}
   endif
-  return reply.result
+
+  var codeLensItem: dict<any> = reply.result
+
+  # Decode the position encoding in the code lens item
+  if lspserver.needOffsetEncoding
+    lspserver.decodeRange(bnr, codeLensItem.range)
+  endif
+
+  return codeLensItem
 enddef
 
 # List project-wide symbols matching query string
@@ -1312,6 +1494,13 @@ def RemoveWorkspaceFolder(lspserver: dict<any>, dirName: string): void
   lspserver.workspaceFolders->remove(idx)
 enddef
 
+def DecodeSelectionRange(lspserver: dict<any>, bnr: number, selRange: dict<any>)
+  lspserver.decodeRange(bnr, selRange.range)
+  if selRange->has_key('parent')
+    DecodeSelectionRange(lspserver, bnr, selRange.parent)
+  endif
+enddef
+
 # select the text around the current cursor location
 # Request: "textDocument/selectionRange"
 # Param: SelectionRangeParams
@@ -1330,13 +1519,22 @@ def SelectionRange(lspserver: dict<any>, fname: string)
   var param = {}
   param.textDocument = {}
   param.textDocument.uri = util.LspFileToUri(fname)
-  param.positions = [GetLspPosition(false)]
+  param.positions = [lspserver.getPosition(false)]
   var reply = lspserver.rpc('textDocument/selectionRange', param)
 
   if reply->empty() || reply.result->empty()
     return
   endif
 
+  # Decode the position encoding in all the selection range items
+  if lspserver.needOffsetEncoding
+    var bnr = fname->bufnr()
+    reply.result->map((_, selItem) => {
+       DecodeSelectionRange(lspserver, bnr, selItem)
+       return selItem
+      })
+  endif
+
   selection.SelectionStart(lspserver, reply.result)
 enddef
 
@@ -1480,7 +1678,8 @@ def TagFunc(lspserver: dict<any>, pat: string, flags: string, info: dict<any>):
 
   # interface DefinitionParams
   #   interface TextDocumentPositionParams
-  var reply = lspserver.rpc('textDocument/definition', GetLspTextDocPosition(false))
+  var reply = lspserver.rpc('textDocument/definition',
+                           lspserver.getTextDocPosition(false))
   if reply->empty() || reply.result->empty()
     return null
   endif
@@ -1492,6 +1691,14 @@ def TagFunc(lspserver: dict<any>, pat: string, flags: string, info: dict<any>):
     taglocations = [reply.result]
   endif
 
+  if lspserver.needOffsetEncoding
+    # Decode the position encoding in all the reference locations
+    taglocations->map((_, loc) => {
+      lspserver.decodeLocation(loc)
+      return loc
+    })
+  endif
+
   return symbol.TagFunc(lspserver, taglocations, pat)
 enddef
 
@@ -1574,6 +1781,14 @@ export def NewLspServer(name_arg: string, path_arg: string, args: list<string>,
     processNotif: function(handlers.ProcessNotif, [lspserver]),
     processRequest: function(handlers.ProcessRequest, [lspserver]),
     processMessages: function(handlers.ProcessMessages, [lspserver]),
+    encodePosition: function(offset.EncodePosition, [lspserver]),
+    decodePosition: function(offset.DecodePosition, [lspserver]),
+    encodeRange: function(offset.EncodeRange, [lspserver]),
+    decodeRange: function(offset.DecodeRange, [lspserver]),
+    encodeLocation: function(offset.EncodeLocation, [lspserver]),
+    decodeLocation: function(offset.DecodeLocation, [lspserver]),
+    getPosition: function(GetPosition, [lspserver]),
+    getTextDocPosition: function(GetTextDocPosition, [lspserver]),
     textdocDidOpen: function(TextdocDidOpen, [lspserver]),
     textdocDidClose: function(TextdocDidClose, [lspserver]),
     textdocDidChange: function(TextdocDidChange, [lspserver]),
diff --git a/autoload/lsp/offset.vim b/autoload/lsp/offset.vim
new file mode 100644 (file)
index 0000000..b6bbe38
--- /dev/null
@@ -0,0 +1,168 @@
+vim9script
+
+import './util.vim'
+
+# Functions for encoding and decoding the LSP position offsets.  Language
+# servers support either UTF-8 or UTF-16 or UTF-32 position offsets.  The
+# character related Vim functions use the UTF-32 position offset.  The
+# encoding used is negotiated during the language server initialization.
+
+# Encode the UTF-32 character offset in the LSP position "pos" to the encoding
+# negotiated with the language server.
+#
+# Modifies in-place the UTF-32 offset in pos.character to a UTF-8 or UTF-16 or
+# UTF-32 offset.
+export def EncodePosition(lspserver: dict<any>, bnr: number, pos: dict<number>)
+  if has('patch-9.0.1629')
+    if lspserver.posEncoding == 32 || bnr <= 0
+      # LSP client plugin also uses utf-32 encoding
+      return
+    endif
+
+    bnr->bufload()
+    var text = bnr->getbufline(pos.line + 1)->get(0, '')
+    if text == ''
+      return
+    endif
+
+    if lspserver.posEncoding == 16
+      pos.character = text->utf16idx(pos.character, true, true)
+    else
+      pos.character = text->byteidxcomp(pos.character)
+    endif
+  endif
+enddef
+
+# Decode the character offset in the LSP position "pos" using the encoding
+# negotiated with the language server to a UTF-32 offset.
+#
+# Modifies in-place the UTF-8 or UTF-16 or UTF-32 offset in pos.character to a
+# UTF-32 offset.
+export def DecodePosition(lspserver: dict<any>, bnr: number, pos: dict<number>)
+  if has('patch-9.0.1629')
+    if lspserver.posEncoding == 32 || bnr <= 0
+      # LSP client plugin also uses utf-32 encoding
+      return
+    endif
+
+    bnr->bufload()
+    var text = bnr->getbufline(pos.line + 1)->get(0, '')
+    # If the line is empty then don't decode the character position.
+    if text == ''
+      return
+    endif
+
+    # If the character position is out-of-bounds, then don't decode the
+    # character position.
+    var textLen = 0
+    if lspserver.posEncoding == 16
+      textLen = text->strutf16len(true)
+    else
+      textLen = text->strlen()
+    endif
+
+    if pos.character > textLen
+      return
+    endif
+
+    if pos.character == textLen
+      pos.character = text->strchars()
+    else
+      if lspserver.posEncoding == 16
+       pos.character = text->charidx(pos.character, true, true)
+      else
+       pos.character = text->charidx(pos.character, true)
+      endif
+    endif
+  endif
+enddef
+
+# Encode the start and end UTF-32 character offsets in the LSP range "range"
+# to the encoding negotiated with the language server.
+#
+# Modifies in-place the UTF-32 offset in range.start.character and
+# range.end.character to a UTF-8 or UTF-16 or UTF-32 offset.
+export def EncodeRange(lspserver: dict<any>, bnr: number,
+                      range: dict<dict<number>>)
+  if has('patch-9.0.1629')
+    if lspserver.posEncoding == 32
+      return
+    endif
+
+    EncodePosition(lspserver, bnr, range.start)
+    EncodePosition(lspserver, bnr, range.end)
+  endif
+enddef
+
+# Decode the start and end character offsets in the LSP range "range" to
+# UTF-32 offsets.
+#
+# Modifies in-place the offset value in range.start.character and
+# range.end.character to a UTF-32 offset.
+export def DecodeRange(lspserver: dict<any>, bnr: number,
+                      range: dict<dict<number>>)
+  if has('patch-9.0.1629')
+    if lspserver.posEncoding == 32
+      return
+    endif
+
+    DecodePosition(lspserver, bnr, range.start)
+    DecodePosition(lspserver, bnr, range.end)
+  endif
+enddef
+
+# Encode the range in the LSP position "location" to the encoding negotiated
+# with the language server.
+#
+# Modifies in-place the UTF-32 offset in location.range to a UTF-8 or UTF-16
+# or UTF-32 offset.
+export def EncodeLocation(lspserver: dict<any>, location: dict<any>)
+  if has('patch-9.0.1629')
+    if lspserver.posEncoding == 32
+      return
+    endif
+
+    var bnr = 0
+    if location->has_key('targetUri')
+      # LocationLink
+      bnr = util.LspUriToBufnr(location.targetUri)
+      if bnr > 0
+       # We use only the "targetSelectionRange" item.  The
+       # "originSelectionRange" and the "targetRange" items are not used.
+       lspserver.encodeRange(bnr, location.targetSelectionRange)
+      endif
+    else
+      # Location
+      bnr = util.LspUriToBufnr(location.uri)
+      if bnr > 0
+       lspserver.encodeRange(bnr, location.range)
+      endif
+    endif
+  endif
+enddef
+
+# Decode the range in the LSP location "location" to UTF-32.
+#
+# Modifies in-place the offset value in location.range to a UTF-32 offset.
+export def DecodeLocation(lspserver: dict<any>, location: dict<any>)
+  if has('patch-9.0.1629')
+    if lspserver.posEncoding == 32
+      return
+    endif
+
+    var bnr = 0
+    if location->has_key('targetUri')
+      # LocationLink
+      bnr = util.LspUriToBufnr(location.targetUri)
+      # We use only the "targetSelectionRange" item.  The
+      # "originSelectionRange" and the "targetRange" items are not used.
+      lspserver.decodeRange(bnr, location.targetSelectionRange)
+    else
+      # Location
+      bnr = util.LspUriToBufnr(location.uri)
+      lspserver.decodeRange(bnr, location.range)
+    endif
+  endif
+enddef
+
+# vim: tabstop=8 shiftwidth=2 softtabstop=2
index 5914f92a419bb28112e7efd4be50a321792fd9e1..761ef188dee20db031427488852b8093e2593237 100644 (file)
@@ -196,6 +196,7 @@ export def WorkspaceSymbolPopup(lspserver: dict<any>, query: string,
     # interface SymbolInformation
     fileName = util.LspUriToFile(symbol.location.uri)
     r = symbol.location.range
+    lspserver.decodeRange(fileName->bufnr(), r)
 
     symName = symbol.name
     if symbol->has_key('containerName') && symbol.containerName != ''
@@ -513,9 +514,11 @@ export def TagFunc(lspserver: dict<any>,
 enddef
 
 # process SymbolInformation[]
-def ProcessSymbolInfoTable(symbolInfoTable: list<dict<any>>,
-                               symbolTypeTable: dict<list<dict<any>>>,
-                               symbolLineTable: list<dict<any>>)
+def ProcessSymbolInfoTable(lspserver: dict<any>,
+                          bnr: number,
+                          symbolInfoTable: list<dict<any>>,
+                          symbolTypeTable: dict<list<dict<any>>>,
+                          symbolLineTable: list<dict<any>>)
   var fname: string
   var symbolType: string
   var name: string
@@ -532,6 +535,7 @@ def ProcessSymbolInfoTable(symbolInfoTable: list<dict<any>>,
       endif
     endif
     r = syminfo.location.range
+    lspserver.decodeRange(bnr, r)
 
     if !symbolTypeTable->has_key(symbolType)
       symbolTypeTable[symbolType] = []
@@ -543,9 +547,11 @@ def ProcessSymbolInfoTable(symbolInfoTable: list<dict<any>>,
 enddef
 
 # process DocumentSymbol[]
-def ProcessDocSymbolTable(docSymbolTable: list<dict<any>>,
-                               symbolTypeTable: dict<list<dict<any>>>,
-                               symbolLineTable: list<dict<any>>)
+def ProcessDocSymbolTable(lspserver: dict<any>,
+                         bnr: number,
+                         docSymbolTable: list<dict<any>>,
+                         symbolTypeTable: dict<list<dict<any>>>,
+                         symbolLineTable: list<dict<any>>)
   var symbolType: string
   var name: string
   var r: dict<dict<number>>
@@ -556,7 +562,8 @@ def ProcessDocSymbolTable(docSymbolTable: list<dict<any>>,
   for syminfo in docSymbolTable
     name = syminfo.name
     symbolType = SymbolKindToName(syminfo.kind)
-    r = syminfo.range
+    r = syminfo.selectionRange
+    lspserver.decodeRange(bnr, r)
     if syminfo->has_key('detail')
       symbolDetail = syminfo.detail
     endif
@@ -565,7 +572,8 @@ def ProcessDocSymbolTable(docSymbolTable: list<dict<any>>,
     endif
     childSymbols = {}
     if syminfo->has_key('children')
-      ProcessDocSymbolTable(syminfo.children, childSymbols, symbolLineTable)
+      ProcessDocSymbolTable(lspserver, bnr, syminfo.children, childSymbols,
+                           symbolLineTable)
     endif
     symInfo = {name: name, range: r, detail: symbolDetail,
                                                children: childSymbols}
@@ -580,6 +588,7 @@ enddef
 export def DocSymbolReply(lspserver: dict<any>, docsymbol: any, fname: string)
   var symbolTypeTable: dict<list<dict<any>>> = {}
   var symbolLineTable: list<dict<any>> = []
+  var bnr = fname->bufnr()
 
   if docsymbol->empty()
     # No symbols defined for this file. Clear the outline window.
@@ -589,10 +598,12 @@ export def DocSymbolReply(lspserver: dict<any>, docsymbol: any, fname: string)
 
   if docsymbol[0]->has_key('location')
     # SymbolInformation[]
-    ProcessSymbolInfoTable(docsymbol, symbolTypeTable, symbolLineTable)
+    ProcessSymbolInfoTable(lspserver, bnr, docsymbol, symbolTypeTable,
+                          symbolLineTable)
   else
     # DocumentSymbol[]
-    ProcessDocSymbolTable(docsymbol, symbolTypeTable, symbolLineTable)
+    ProcessDocSymbolTable(lspserver, bnr, docsymbol, symbolTypeTable,
+                         symbolLineTable)
   endif
 
   # sort the symbols by line number
index 4c793fe19ce0c8f3281eb62c4c4bf91c33a21f75..14b4cd919e37ae617769c4c9feadeee1d269d821 100644 (file)
@@ -235,9 +235,9 @@ To add a language server, the following information is needed:
        path            complete path to the language server executable
                        (without any arguments).
                                                *lsp-cfg-args*
-       args            a list of command-line arguments passed to the
-                       language server. Each argument is a separate List
-                       item.
+       args            a |List| of command-line arguments passed to the
+                       language server.  Each space separated language server
+                       command-line argument is a separate List item.
                                                *lsp-cfg-filetype*
        filetype        One or more file types supported by the language
                        server.  This can be a |String| or a |List|. To
diff --git a/test/clangd_offsetencoding.vim b/test/clangd_offsetencoding.vim
new file mode 100644 (file)
index 0000000..c61b37c
--- /dev/null
@@ -0,0 +1,478 @@
+vim9script
+# Unit tests for language server protocol offset encoding using clangd
+
+source common.vim
+
+# Start the C language server.  Returns true on success and false on failure.
+def g:StartLangServer(): bool
+  if has('patch-9.0.1629')
+    return g:StartLangServerWithFile('Xtest.c')
+  endif
+  return false
+enddef
+
+if !has('patch-9.0.1629')
+  # Need patch 9.0.1629 to properly encode/decode the UTF-16 offsets
+  finish
+endif
+
+var lspOpts = {autoComplete: false, highlightDiagInline: true}
+g:LspOptionsSet(lspOpts)
+
+var lspServers = [{
+      filetype: ['c', 'cpp'],
+      path: (exepath('clangd-15') ?? exepath('clangd')),
+      args: ['--background-index',
+            '--clang-tidy',
+            $'--offset-encoding={$LSP_OFFSET_ENCODING}']
+  }]
+call LspAddServer(lspServers)
+
+# Test for :LspCodeAction with symbols containing multibyte and composing
+# characters
+def g:Test_LspCodeAction_multibyte()
+  silent! edit XLspCodeAction_mb.c
+  sleep 200m
+  var lines =<< trim END
+    #include <stdio.h>
+    void fn(int aVar)
+    {
+        printf("aVar = %d\n", aVar);
+        printf("馃槉馃槉馃槉馃槉 = %d\n", aVar):
+        printf("a虂b虂a虂b虂 = %d\n", aVar):
+        printf("a台虂a台虂a台虂a台虂 = %d\n", aVar):
+    }
+  END
+  setline(1, lines)
+  g:WaitForServerFileLoad(3)
+  :redraw!
+  cursor(5, 5)
+  redraw!
+  :LspCodeAction 1
+  assert_equal('    printf("馃槉馃槉馃槉馃槉 = %d\n", aVar);', getline(5))
+  cursor(6, 5)
+  redraw!
+  :LspCodeAction 1
+  assert_equal('    printf("a虂b虂a虂b虂 = %d\n", aVar);', getline(6))
+  cursor(7, 5)
+  redraw!
+  :LspCodeAction 1
+  assert_equal('    printf("a台虂a台虂a台虂a台虂 = %d\n", aVar);', getline(7))
+
+  :%bw!
+enddef
+
+# Test for :LspDiagShow when using multibyte and composing characters
+def g:Test_LspDiagShow_multibyte()
+  :silent! edit XLspDiagShow_mb.c
+  sleep 200m
+  var lines =<< trim END
+    #include <stdio.h>
+    void fn(int aVar)
+    {
+        printf("aVar = %d\n", aVar);
+        printf("馃槉馃槉馃槉馃槉 = %d\n". aVar);
+        printf("a虂b虂a虂b虂 = %d\n". aVar);
+        printf("a台虂a台虂a台虂a台虂 = %d\n". aVar);
+    }
+  END
+  setline(1, lines)
+  g:WaitForServerFileLoad(3)
+  :redraw!
+  :LspDiagShow
+  var qfl: list<dict<any>> = getloclist(0)
+  assert_equal([5, 37], [qfl[0].lnum, qfl[0].col])
+  assert_equal([6, 33], [qfl[1].lnum, qfl[1].col])
+  assert_equal([7, 41], [qfl[2].lnum, qfl[2].col])
+  :lclose
+  :%bw!
+enddef
+
+# Test for :LspFormat when using multibyte and composing characters
+def g:Test_LspFormat_multibyte()
+  :silent! edit XLspFormat_mb.c
+  sleep 200m
+  var lines =<< trim END
+    void fn(int aVar)
+    {
+       int 馃槉馃槉馃槉馃槉   =   aVar + 1;
+       int a虂b虂a虂b虂   =   aVar + 1;
+       int a台虂a台虂a台虂a台虂   =   aVar + 1;
+    }
+  END
+  setline(1, lines)
+  g:WaitForServerFileLoad(0)
+  :redraw!
+  :LspFormat
+  var expected =<< trim END
+    void fn(int aVar) {
+      int 馃槉馃槉馃槉馃槉 = aVar + 1;
+      int a虂b虂a虂b虂 = aVar + 1;
+      int a台虂a台虂a台虂a台虂 = aVar + 1;
+    }
+  END
+  assert_equal(expected, getline(1, '$'))
+  :%bw!
+enddef
+
+# Test for :LspGotoDefinition when using multibyte and composing characters
+def g:Test_LspGotoDefinition_multibyte()
+  :silent! edit XLspGotoDefinition_mb.c
+  sleep 200m
+  var lines: list<string> =<< trim END
+    #include <stdio.h>
+    void fn(int aVar)
+    {
+        printf("aVar = %d\n", aVar);
+        printf("馃槉馃槉馃槉馃槉 = %d\n", aVar);
+        printf("a虂b虂a虂b虂 = %d\n", aVar);
+        printf("a台虂a台虂a台虂a台虂 = %d\n", aVar);
+    }
+  END
+  setline(1, lines)
+  g:WaitForServerFileLoad(0)
+  redraw!
+
+  for [lnum, colnr] in [[4, 27], [5, 39], [6, 35], [7, 43]]
+    cursor(lnum, colnr)
+    :LspGotoDefinition
+    assert_equal([2, 13], [line('.'), col('.')])
+  endfor
+
+  :%bw!
+enddef
+
+# Test for :LspGotoDefinition when using multibyte and composing characters
+def g:Test_LspGotoDefinition_after_multibyte()
+  :silent! edit XLspGotoDef_after_mb.c
+  sleep 200m
+  var lines =<< trim END
+    void fn(int aVar)
+    {
+        /* 伪尾纬未, 馃槉馃槉馃槉馃槉, a虂b虂a虂b虂, a台虂a台虂a台虂a台虂 */ int 伪尾纬未, bVar;
+        /* 伪尾纬未, 馃槉馃槉馃槉馃槉, a虂b虂a虂b虂, a台虂a台虂a台虂a台虂 */ int 馃槉馃槉馃槉馃槉, cVar;
+        /* 伪尾纬未, 馃槉馃槉馃槉馃槉, a虂b虂a虂b虂, a台虂a台虂a台虂a台虂 */ int a虂b虂a虂b虂, dVar;
+        /* 伪尾纬未, 馃槉馃槉馃槉馃槉, a虂b虂a虂b虂, a台虂a台虂a台虂a台虂 */ int a台虂a台虂a台虂a台虂, eVar;
+        bVar = 1;
+        cVar = 2;
+        dVar = 3;
+        eVar = 4;
+       aVar = 伪尾纬未 + 馃槉馃槉馃槉馃槉 + a虂b虂a虂b虂 + a台虂a台虂a台虂a台虂 + bVar;
+    }
+  END
+  setline(1, lines)
+  g:WaitForServerFileLoad(0)
+  :redraw!
+  cursor(7, 5)
+  :LspGotoDefinition
+  assert_equal([3, 88], [line('.'), col('.')])
+  cursor(8, 5)
+  :LspGotoDefinition
+  assert_equal([4, 96], [line('.'), col('.')])
+  cursor(9, 5)
+  :LspGotoDefinition
+  assert_equal([5, 92], [line('.'), col('.')])
+  cursor(10, 5)
+  :LspGotoDefinition
+  assert_equal([6, 100], [line('.'), col('.')])
+  cursor(11, 12)
+  :LspGotoDefinition
+  assert_equal([3, 78], [line('.'), col('.')])
+  cursor(11, 23)
+  :LspGotoDefinition
+  assert_equal([4, 78], [line('.'), col('.')])
+  cursor(11, 42)
+  :LspGotoDefinition
+  assert_equal([5, 78], [line('.'), col('.')])
+  cursor(11, 57)
+  :LspGotoDefinition
+  assert_equal([6, 78], [line('.'), col('.')])
+
+  :%bw!
+enddef
+
+# Test for doing omni completion for symbols with multibyte and composing
+# characters
+def g:Test_OmniComplete_multibyte()
+  :silent! edit XOmniComplete_mb.c
+  sleep 200m
+  var lines: list<string> =<< trim END
+    void Func1(void)
+    {
+        int 馃槉馃槉馃槉馃槉, aVar;
+        int a虂b虂a虂b虂, bVar;
+        int a台虂a台虂a台虂a台虂, cVar;
+        
+        
+        
+    }
+  END
+  setline(1, lines)
+  g:WaitForServerFileLoad(0)
+  redraw!
+
+  cursor(6, 4)
+  feedkeys("aaV\<C-X>\<C-O> = 馃槉馃槉\<C-X>\<C-O>;", 'xt')
+  assert_equal('    aVar = 馃槉馃槉馃槉馃槉;', getline('.'))
+  cursor(7, 4)
+  feedkeys("abV\<C-X>\<C-O> = a虂b虂\<C-X>\<C-O>;", 'xt')
+  assert_equal('    bVar = a虂b虂a虂b虂;', getline('.'))
+  cursor(8, 4)
+  feedkeys("acV\<C-X>\<C-O> = a台虂a台虂\<C-X>\<C-O>;", 'xt')
+  assert_equal('    cVar = a台虂a台虂a台虂a台虂;', getline('.'))
+  feedkeys("oa虂b虂\<C-X>\<C-O> = a台虂a台虂\<C-X>\<C-O>;", 'xt')
+  assert_equal('    a虂b虂a虂b虂 = a台虂a台虂a台虂a台虂;', getline('.'))
+  feedkeys("oa台虂a台虂\<C-X>\<C-O> = a虂b虂\<C-X>\<C-O>;", 'xt')
+  assert_equal('    a台虂a台虂a台虂a台虂 = a虂b虂a虂b虂;', getline('.'))
+  :%bw!
+enddef
+
+# Test for :LspOutline with multibyte and composing characters
+def g:Test_Outline_multibyte()
+  silent! edit XLspOutline_mb.c
+  sleep 200m
+  var lines: list<string> =<< trim END
+    typedef void 馃槉馃槉馃槉馃槉;
+    typedef void a虂b虂a虂b虂;
+    typedef void a台虂a台虂a台虂a台虂;
+    
+    馃槉馃槉馃槉馃槉 Func1()
+    {
+    }
+    
+    a虂b虂a虂b虂 Func2()
+    {
+    }
+    
+    a台虂a台虂a台虂a台虂 Func3()
+    {
+    }
+  END
+  setline(1, lines)
+  g:WaitForServerFileLoad(0)
+  redraw!
+
+  cursor(1, 1)
+  :LspOutline
+  assert_equal(2, winnr('$'))
+
+  :wincmd w
+  cursor(5, 1)
+  feedkeys("\<CR>", 'xt')
+  assert_equal([2, 5, 18], [winnr(), line('.'), col('.')])
+
+  :wincmd w
+  cursor(6, 1)
+  feedkeys("\<CR>", 'xt')
+  assert_equal([2, 9, 14], [winnr(), line('.'), col('.')])
+
+  :wincmd w
+  cursor(7, 1)
+  feedkeys("\<CR>", 'xt')
+  assert_equal([2, 13, 22], [winnr(), line('.'), col('.')])
+
+  :wincmd w
+  cursor(10, 1)
+  feedkeys("\<CR>", 'xt')
+  assert_equal([2, 1, 14], [winnr(), line('.'), col('.')])
+
+  :wincmd w
+  cursor(11, 1)
+  feedkeys("\<CR>", 'xt')
+  assert_equal([2, 2, 14], [winnr(), line('.'), col('.')])
+
+  :wincmd w
+  cursor(12, 1)
+  feedkeys("\<CR>", 'xt')
+  assert_equal([2, 3, 14], [winnr(), line('.'), col('.')])
+
+  :%bw!
+enddef
+
+# Test for :LspRename with multibyte and composing characters
+def g:Test_LspRename_multibyte()
+  silent! edit XLspRename_mb.c
+  sleep 200m
+  var lines: list<string> =<< trim END
+    #include <stdio.h>
+    void fn(int aVar)
+    {
+        printf("aVar = %d\n", aVar);
+        printf("馃槉馃槉馃槉馃槉 = %d\n", aVar);
+        printf("a虂b虂a虂b虂 = %d\n", aVar);
+        printf("a台虂a台虂a台虂a台虂 = %d\n", aVar);
+    }
+  END
+  setline(1, lines)
+  g:WaitForServerFileLoad(0)
+  redraw!
+  cursor(2, 12)
+  :LspRename bVar
+  redraw!
+  var expected: list<string> =<< trim END
+    #include <stdio.h>
+    void fn(int bVar)
+    {
+        printf("aVar = %d\n", bVar);
+        printf("馃槉馃槉馃槉馃槉 = %d\n", bVar);
+        printf("a虂b虂a虂b虂 = %d\n", bVar);
+        printf("a台虂a台虂a台虂a台虂 = %d\n", bVar);
+    }
+  END
+  assert_equal(expected, getline(1, '$'))
+  :%bw!
+enddef
+
+# Test for :LspShowReferences when using multibyte and composing characters
+def g:Test_LspShowReferences_multibyte()
+  :silent! edit XLspShowReferences_mb.c
+  sleep 200m
+  var lines: list<string> =<< trim END
+    #include <stdio.h>
+    void fn(int aVar)
+    {
+        printf("aVar = %d\n", aVar);
+        printf("馃槉馃槉馃槉馃槉 = %d\n", aVar);
+        printf("a虂b虂a虂b虂 = %d\n", aVar);
+        printf("a台虂a台虂a台虂a台虂 = %d\n", aVar);
+    }
+  END
+  setline(1, lines)
+  g:WaitForServerFileLoad(0)
+  redraw!
+  cursor(4, 27)
+  :LspShowReferences
+  var qfl: list<dict<any>> = getloclist(0)
+  assert_equal([2, 13], [qfl[0].lnum, qfl[0].col])
+  assert_equal([4, 27], [qfl[1].lnum, qfl[1].col])
+  assert_equal([5, 39], [qfl[2].lnum, qfl[2].col])
+  assert_equal([6, 35], [qfl[3].lnum, qfl[3].col])
+  assert_equal([7, 43], [qfl[4].lnum, qfl[4].col])
+  :lclose
+
+  :%bw!
+enddef
+
+# Test for :LspSymbolSearch when using multibyte and composing characters
+def g:Test_LspSymbolSearch_multibyte()
+  silent! edit XLspSymbolSearch_mb.c
+  sleep 200m
+  var lines: list<string> =<< trim END
+    typedef void 馃槉馃槉馃槉馃槉;
+    typedef void a虂b虂a虂b虂;
+    typedef void a台虂a台虂a台虂a台虂;
+
+    馃槉馃槉馃槉馃槉 Func1()
+    {
+    }
+
+    a虂b虂a虂b虂 Func2()
+    {
+    }
+
+    a台虂a台虂a台虂a台虂 Func3()
+    {
+    }
+  END
+  setline(1, lines)
+  g:WaitForServerFileLoad(0)
+
+  cursor(1, 1)
+  feedkeys(":LspSymbolSearch Func1\<CR>\<CR>", "xt")
+  assert_equal([5, 18], [line('.'), col('.')])
+  cursor(1, 1)
+  feedkeys(":LspSymbolSearch Func2\<CR>\<CR>", "xt")
+  assert_equal([9, 14], [line('.'), col('.')])
+  cursor(1, 1)
+  feedkeys(":LspSymbolSearch Func3\<CR>\<CR>", "xt")
+  assert_equal([13, 22], [line('.'), col('.')])
+
+  :%bw!
+enddef
+
+# Test for setting the 'tagfunc' with multibyte and composing characters in
+# symbols
+def g:Test_LspTagFunc_multibyte()
+  var lines =<< trim END
+    void fn(int aVar)
+    {
+        int 馃槉馃槉馃槉馃槉, bVar;
+        int a虂b虂a虂b虂, cVar;
+        int a台虂a台虂a台虂a台虂, dVar;
+        bVar = 10;
+        cVar = 10;
+        dVar = 10;
+    }
+  END
+  writefile(lines, 'Xtagfunc_mb.c')
+  :silent! edit! Xtagfunc_mb.c
+  g:WaitForServerFileLoad(0)
+  :setlocal tagfunc=lsp#lsp#TagFunc
+  cursor(6, 5)
+  :exe "normal \<C-]>"
+  assert_equal([3, 27], [line('.'), col('.')])
+  cursor(7, 5)
+  :exe "normal \<C-]>"
+  assert_equal([4, 23], [line('.'), col('.')])
+  cursor(8, 5)
+  :exe "normal \<C-]>"
+  assert_equal([5, 31], [line('.'), col('.')])
+  :set tagfunc&
+
+  :%bw!
+  delete('Xtagfunc_mb.c')
+enddef
+
+# Test for the :LspSuperTypeHierarchy and :LspSubTypeHierarchy commands with
+# multibyte and composing characters
+def g:Test_LspTypeHier_multibyte()
+  silent! edit XLspTypeHier_mb.cpp
+  sleep 200m
+  var lines =<< trim END
+    /* 伪尾馃槉馃槉a虂a虂a台虂a台虂 */ class parent {
+    };
+
+    /* 伪尾馃槉馃槉a虂a虂a台虂a台虂 */ class child : public parent {
+    };
+
+    /* 伪尾馃槉馃槉a虂a虂a台虂a台虂 */ class grandchild : public child {
+    };
+  END
+  setline(1, lines)
+  g:WaitForServerFileLoad(0)
+  redraw!
+
+  cursor(1, 42)
+  :LspSubTypeHierarchy
+  call feedkeys("\<CR>", 'xt')
+  assert_equal([1, 36], [line('.'), col('.')])
+  cursor(1, 42)
+
+  :LspSubTypeHierarchy
+  call feedkeys("\<Down>\<CR>", 'xt')
+  assert_equal([4, 42], [line('.'), col('.')])
+
+  cursor(1, 42)
+  :LspSubTypeHierarchy
+  call feedkeys("\<Down>\<Down>\<CR>", 'xt')
+  assert_equal([7, 42], [line('.'), col('.')])
+
+  cursor(7, 42)
+  :LspSuperTypeHierarchy
+  call feedkeys("\<CR>", 'xt')
+  assert_equal([7, 36], [line('.'), col('.')])
+
+  cursor(7, 42)
+  :LspSuperTypeHierarchy
+  call feedkeys("\<Down>\<CR>", 'xt')
+  assert_equal([4, 36], [line('.'), col('.')])
+
+  cursor(7, 42)
+  :LspSuperTypeHierarchy
+  call feedkeys("\<Down>\<Down>\<CR>", 'xt')
+  assert_equal([1, 36], [line('.'), col('.')])
+
+  :%bw!
+enddef
+
+# vim: shiftwidth=2 softtabstop=2 noexpandtab
index 88298f63b98b05551f985605072763716858ae21..7eabcf3b314e377cb5a72504959123cabaf349f3 100644 (file)
@@ -131,33 +131,6 @@ def g:Test_LspFormat()
   :%bw!
 enddef
 
-# Test for :LspFormat when using composing characters
-def g:Test_LspFormat_ComposingChars()
-  :silent! edit XLspFormatComposing.c
-  sleep 200m
-  var lines =<< trim END
-    void fn(int aVar)
-    {
-       int 馃槉馃槉馃槉馃槉   =   aVar + 1;
-       int a虂b虂a虂b虂   =   aVar + 1;
-       int a台虂a台虂a台虂a台虂   =   aVar + 1;
-    }
-  END
-  setline(1, lines)
-  g:WaitForServerFileLoad(0)
-  :redraw!
-  :LspFormat
-  var expected =<< trim END
-    void fn(int aVar) {
-      int 馃槉馃槉馃槉馃槉 = aVar + 1;
-      int a虂b虂a虂b虂 = aVar + 1;
-      int a台虂a台虂a台虂a台虂 = aVar + 1;
-    }
-  END
-  assert_equal(expected, getline(1, '$'))
-  :%bw!
-enddef
-
 # Test for formatting a file using 'formatexpr'
 def g:Test_LspFormatExpr()
   :silent! edit XLspFormat.c
@@ -283,36 +256,6 @@ def g:Test_LspShowReferences()
   :%bw!
 enddef
 
-# Test for :LspShowReferences when using composing characters
-def g:Test_LspShowReferences_ComposingChars()
-  :silent! edit Xtest.c
-  sleep 200m
-  var lines: list<string> =<< trim END
-    #include <stdio.h>
-    void fn(int aVar)
-    {
-        printf("aVar = %d\n", aVar);
-        printf("馃槉馃槉馃槉馃槉 = %d\n", aVar);
-        printf("a虂b虂a虂b虂 = %d\n", aVar);
-        printf("a台虂a台虂a台虂a台虂 = %d\n", aVar);
-    }
-  END
-  setline(1, lines)
-  g:WaitForServerFileLoad(0)
-  redraw!
-  cursor(4, 27)
-  :LspShowReferences
-  var qfl: list<dict<any>> = getloclist(0)
-  assert_equal([2, 13], [qfl[0].lnum, qfl[0].col])
-  assert_equal([4, 27], [qfl[1].lnum, qfl[1].col])
-  assert_equal([5, 39], [qfl[2].lnum, qfl[2].col])
-  assert_equal([6, 35], [qfl[3].lnum, qfl[3].col])
-  assert_equal([7, 43], [qfl[4].lnum, qfl[4].col])
-  :lclose
-
-  :%bw!
-enddef
-
 # Test for LSP diagnostics
 def g:Test_LspDiag()
   :silent! edit XLspDiag.c
@@ -397,32 +340,6 @@ def g:Test_LspDiag()
   :%bw!
 enddef
 
-# Test for :LspDiagShow when using composing characters
-def g:Test_LspDiagShow_ComposingChars()
-  :silent! edit XDiagShowCompose.c
-  sleep 200m
-  var lines =<< trim END
-    #include <stdio.h>
-    void fn(int aVar)
-    {
-        printf("aVar = %d\n", aVar);
-        printf("馃槉馃槉馃槉馃槉 = %d\n". aVar);
-        printf("a虂b虂a虂b虂 = %d\n". aVar);
-        printf("a台虂a台虂a台虂a台虂 = %d\n". aVar);
-    }
-  END
-  setline(1, lines)
-  g:WaitForServerFileLoad(3)
-  :redraw!
-  :LspDiagShow
-  var qfl: list<dict<any>> = getloclist(0)
-  assert_equal([5, 37], [qfl[0].lnum, qfl[0].col])
-  assert_equal([6, 33], [qfl[1].lnum, qfl[1].col])
-  assert_equal([7, 41], [qfl[2].lnum, qfl[2].col])
-  :lclose
-  :%bw!
-enddef
-
 # Test for LSP diagnostics handler
 def g:Test_LspProcessDiagHandler()
   g:LSPTest_modifyDiags = true
@@ -758,39 +675,6 @@ def g:Test_LspCodeAction()
   :%bw!
 enddef
 
-# Test for :LspCodeAction with symbols containing composing characters
-def g:Test_LspCodeAction_ComposingChars()
-  silent! edit XLspCodeActionComposing.c
-  sleep 200m
-  var lines =<< trim END
-    #include <stdio.h>
-    void fn(int aVar)
-    {
-        printf("aVar = %d\n", aVar);
-        printf("馃槉馃槉馃槉馃槉 = %d\n", aVar):
-        printf("a虂b虂a虂b虂 = %d\n", aVar):
-        printf("a台虂a台虂a台虂a台虂 = %d\n", aVar):
-    }
-  END
-  setline(1, lines)
-  g:WaitForServerFileLoad(3)
-  :redraw!
-  cursor(5, 5)
-  redraw!
-  :LspCodeAction 1
-  assert_equal('    printf("馃槉馃槉馃槉馃槉 = %d\n", aVar);', getline(5))
-  cursor(6, 5)
-  redraw!
-  :LspCodeAction 1
-  assert_equal('    printf("a虂b虂a虂b虂 = %d\n", aVar);', getline(6))
-  cursor(7, 5)
-  redraw!
-  :LspCodeAction 1
-  assert_equal('    printf("a台虂a台虂a台虂a台虂 = %d\n", aVar);', getline(7))
-
-  :%bw!
-enddef
-
 # Test for :LspRename
 def g:Test_LspRename()
   silent! edit XLspRename.c
@@ -861,40 +745,6 @@ def g:Test_LspRename()
   :%bw!
 enddef
 
-# Test for :LspRename with composing characters
-def g:Test_LspRename_ComposingChars()
-  silent! edit XLspRenameComposing.c
-  sleep 200m
-  var lines: list<string> =<< trim END
-    #include <stdio.h>
-    void fn(int aVar)
-    {
-        printf("aVar = %d\n", aVar);
-        printf("馃槉馃槉馃槉馃槉 = %d\n", aVar);
-        printf("a虂b虂a虂b虂 = %d\n", aVar);
-        printf("a台虂a台虂a台虂a台虂 = %d\n", aVar);
-    }
-  END
-  setline(1, lines)
-  g:WaitForServerFileLoad(0)
-  redraw!
-  cursor(2, 12)
-  :LspRename bVar
-  redraw!
-  var expected: list<string> =<< trim END
-    #include <stdio.h>
-    void fn(int bVar)
-    {
-        printf("aVar = %d\n", bVar);
-        printf("馃槉馃槉馃槉馃槉 = %d\n", bVar);
-        printf("a虂b虂a虂b虂 = %d\n", bVar);
-        printf("a台虂a台虂a台虂a台虂 = %d\n", bVar);
-    }
-  END
-  assert_equal(expected, getline(1, '$'))
-  :%bw!
-enddef
-
 # Test for :LspSelectionExpand and :LspSelectionShrink
 def g:Test_LspSelection()
   silent! edit XLspSelection.c
@@ -1132,64 +982,6 @@ def g:Test_LspGotoSymbol()
   :%bw!
 enddef
 
-# Test for :LspGotoDefinition when using composing characters
-def g:Test_LspGotoDefinition_With_ComposingCharacters()
-  :silent! edit Xtest.c
-  sleep 200m
-  var lines: list<string> =<< trim END
-    #include <stdio.h>
-    void fn(int aVar)
-    {
-        printf("aVar = %d\n", aVar);
-        printf("馃槉馃槉馃槉馃槉 = %d\n", aVar);
-        printf("a虂b虂a虂b虂 = %d\n", aVar);
-        printf("a台虂a台虂a台虂a台虂 = %d\n", aVar);
-    }
-  END
-  setline(1, lines)
-  g:WaitForServerFileLoad(0)
-  redraw!
-
-  for [lnum, colnr] in [[4, 27], [5, 39], [6, 35], [7, 43]]
-    cursor(lnum, colnr)
-    :LspGotoDefinition
-    assert_equal([2, 13], [line('.'), col('.')])
-  endfor
-
-  :%bw!
-enddef
-
-# Test for :LspGotoDefinition when using composing characters
-def g:Test_LspGotoDefinition_After_ComposingCharacters()
-  :silent! edit Xtest.c
-  sleep 200m
-  var lines =<< trim END
-    void fn(int aVar)
-    {
-        int 馃槉馃槉馃槉馃槉, bVar;
-        int a虂b虂a虂b虂, cVar;
-        int a台虂a台虂a台虂a台虂, dVar;
-        bVar = 10;
-        cVar = 10;
-        dVar = 10;
-    }
-  END
-  setline(1, lines)
-  g:WaitForServerFileLoad(0)
-  :redraw!
-  cursor(6, 5)
-  :LspGotoDefinition
-  assert_equal([3, 27], [line('.'), col('.')])
-  cursor(7, 5)
-  :LspGotoDefinition
-  assert_equal([4, 23], [line('.'), col('.')])
-  cursor(8, 5)
-  :LspGotoDefinition
-  assert_equal([5, 31], [line('.'), col('.')])
-
-  :%bw!
-enddef
-
 # Test for :LspHighlight
 def g:Test_LspHighlight()
   silent! edit XLspHighlight.c
@@ -1361,43 +1153,6 @@ def g:Test_LspSymbolSearch()
   :%bw!
 enddef
 
-# Test for :LspSymbolSearch when using composing characters
-def g:Test_LspSymbolSearch_ComposingChars()
-  silent! edit XLspSymbolSearchCompose.c
-  sleep 200m
-  var lines: list<string> =<< trim END
-    typedef void 馃槉馃槉馃槉馃槉;
-    typedef void a虂b虂a虂b虂;
-    typedef void a台虂a台虂a台虂a台虂;
-
-    馃槉馃槉馃槉馃槉 Func1()
-    {
-    }
-
-    a虂b虂a虂b虂 Func2()
-    {
-    }
-
-    a台虂a台虂a台虂a台虂 Func3()
-    {
-    }
-  END
-  setline(1, lines)
-  g:WaitForServerFileLoad(0)
-
-  cursor(1, 1)
-  feedkeys(":LspSymbolSearch Func1\<CR>\<CR>", "xt")
-  assert_equal([5, 18], [line('.'), col('.')])
-  cursor(1, 1)
-  feedkeys(":LspSymbolSearch Func2\<CR>\<CR>", "xt")
-  assert_equal([9, 14], [line('.'), col('.')])
-  cursor(1, 1)
-  feedkeys(":LspSymbolSearch Func3\<CR>\<CR>", "xt")
-  assert_equal([13, 22], [line('.'), col('.')])
-
-  :%bw!
-enddef
-
 # Test for :LspIncomingCalls
 def g:Test_LspIncomingCalls()
   silent! edit XLspIncomingCalls.c
@@ -1535,38 +1290,6 @@ def g:Test_LspTagFunc()
   delete('Xtagfunc.c')
 enddef
 
-# Test for setting the 'tagfunc' with composing characters in symbols
-def g:Test_LspTagFunc_ComposingChars()
-  var lines =<< trim END
-    void fn(int aVar)
-    {
-        int 馃槉馃槉馃槉馃槉, bVar;
-        int a虂b虂a虂b虂, cVar;
-        int a台虂a台虂a台虂a台虂, dVar;
-        bVar = 10;
-        cVar = 10;
-        dVar = 10;
-    }
-  END
-  writefile(lines, 'XtagfuncCompose.c')
-  :silent! edit! XtagfuncCompose.c
-  g:WaitForServerFileLoad(0)
-  :setlocal tagfunc=lsp#lsp#TagFunc
-  cursor(6, 5)
-  :exe "normal \<C-]>"
-  assert_equal([3, 27], [line('.'), col('.')])
-  cursor(7, 5)
-  :exe "normal \<C-]>"
-  assert_equal([4, 23], [line('.'), col('.')])
-  cursor(8, 5)
-  :exe "normal \<C-]>"
-  assert_equal([5, 31], [line('.'), col('.')])
-  :set tagfunc&
-
-  :%bw!
-  delete('XtagfuncCompose.c')
-enddef
-
 # Test for the LspDiagsUpdated autocmd
 def g:Test_LspDiagsUpdated_Autocmd()
   g:LspAutoCmd = 0
@@ -1726,41 +1449,6 @@ def g:Test_OmniComplete_Struct()
   :%bw!
 enddef
 
-# Test for doing omni completion for symbols with composing characters
-def g:Test_OmniComplete_ComposingChars()
-  :silent! edit XOmniCompleteCompose.c
-  sleep 200m
-  var lines: list<string> =<< trim END
-    void Func1(void)
-    {
-        int 馃槉馃槉馃槉馃槉, aVar;
-        int a虂b虂a虂b虂, bVar;
-        int a台虂a台虂a台虂a台虂, cVar;
-        
-        
-        
-    }
-  END
-  setline(1, lines)
-  g:WaitForServerFileLoad(0)
-  redraw!
-
-  cursor(6, 4)
-  feedkeys("aaV\<C-X>\<C-O> = 馃槉馃槉\<C-X>\<C-O>;", 'xt')
-  assert_equal('    aVar = 馃槉馃槉馃槉馃槉;', getline('.'))
-  cursor(7, 4)
-  feedkeys("abV\<C-X>\<C-O> = a虂b虂\<C-X>\<C-O>;", 'xt')
-  assert_equal('    bVar = a虂b虂a虂b虂;', getline('.'))
-  cursor(8, 4)
-  feedkeys("acV\<C-X>\<C-O> = a台虂a台虂\<C-X>\<C-O>;", 'xt')
-  assert_equal('    cVar = a台虂a台虂a台虂a台虂;', getline('.'))
-  feedkeys("oa虂b虂\<C-X>\<C-O> = a台虂a台虂\<C-X>\<C-O>;", 'xt')
-  assert_equal('    a虂b虂a虂b虂 = a台虂a台虂a台虂a台虂;', getline('.'))
-  feedkeys("oa台虂a台虂\<C-X>\<C-O> = a虂b虂\<C-X>\<C-O>;", 'xt')
-  assert_equal('    a台虂a台虂a台虂a台虂 = a虂b虂a虂b虂;', getline('.'))
-  :%bw!
-enddef
-
 # Test for the :LspServer command.
 def g:Test_LspServer()
   new a.raku
index 151dcbd6d6e0aa3f90e6040565f30de238927f64..625ba6f863366a17e57434da2264c2c2105f1049 100755 (executable)
@@ -12,8 +12,8 @@ VIM_CMD="$VIMPRG -u NONE -U NONE -i NONE --noplugin -N --not-a-term"
 
 TESTS="clangd_tests.vim tsserver_tests.vim gopls_tests.vim not_lspserver_related_tests.vim"
 
-for testfile in $TESTS
-do
+RunTestsInFile() {
+    testfile=$1
     echo "Running tests in $testfile"
     $VIM_CMD -c "let g:TestName='$testfile'" -S runner.vim
 
@@ -31,6 +31,18 @@ do
 
     echo "SUCCESS: All the tests in $testfile passed."
     echo
+}
+
+for testfile in $TESTS
+do
+    RunTestsInFile $testfile
+done
+
+for encoding in "utf-8" "utf-16" "utf-32"
+do
+    export LSP_OFFSET_ENCODING=$encoding
+    echo "LSP offset encoding: $LSP_OFFSET_ENCODING"
+    RunTestsInFile clangd_offsetencoding.vim
 done
 
 echo "SUCCESS: All the tests passed."