From 11dabbb9fef88760c3b160e905db1ae6b0bef1c6 Mon Sep 17 00:00:00 2001 From: Yegappan Lakshmanan Date: Fri, 16 Jun 2023 18:55:26 -0700 Subject: [PATCH] Add support for UTF-8 and UTF-16 offset encoding --- autoload/lsp/capabilities.vim | 28 ++ autoload/lsp/codelens.vim | 4 +- autoload/lsp/diag.vim | 9 + autoload/lsp/inlayhints.vim | 1 + autoload/lsp/lspserver.vim | 297 +++++++++++++++++--- autoload/lsp/offset.vim | 168 ++++++++++++ autoload/lsp/symbol.vim | 31 ++- doc/lsp.txt | 6 +- test/clangd_offsetencoding.vim | 478 +++++++++++++++++++++++++++++++++ test/clangd_tests.vim | 312 --------------------- test/run_tests.sh | 16 +- 11 files changed, 980 insertions(+), 370 deletions(-) create mode 100644 autoload/lsp/offset.vim create mode 100644 test/clangd_offsetencoding.vim diff --git a/autoload/lsp/capabilities.vim b/autoload/lsp/capabilities.vim index 28a1215..9df58de 100644 --- a/autoload/lsp/capabilities.vim +++ b/autoload/lsp/capabilities.vim @@ -7,6 +7,28 @@ import './options.vim' as opt # Process the server capabilities # interface ServerCapabilities export def ProcessServerCaps(lspserver: dict, caps: dict) + 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 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 diff --git a/autoload/lsp/codelens.vim b/autoload/lsp/codelens.vim index 2571665..082edf7 100644 --- a/autoload/lsp/codelens.vim +++ b/autoload/lsp/codelens.vim @@ -4,13 +4,13 @@ import './codeaction.vim' # Functions related to handling LSP code lens -export def ProcessCodeLens(lspserver: dict, codeLensItems: list>) +export def ProcessCodeLens(lspserver: dict, bnr: number, codeLensItems: list>) var text: list = [] 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 diff --git a/autoload/lsp/diag.vim b/autoload/lsp/diag.vim index 45cf820..9eaa30c 100644 --- a/autoload/lsp/diag.vim +++ b/autoload/lsp/diag.vim @@ -309,6 +309,15 @@ export def DiagNotification(lspserver: dict, uri: string, diags_arg: list> = 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 diff --git a/autoload/lsp/inlayhints.vim b/autoload/lsp/inlayhints.vim index 07c0a1b..87cd8c6 100644 --- a/autoload/lsp/inlayhints.vim +++ b/autoload/lsp/inlayhints.vim @@ -45,6 +45,7 @@ export def InlayHintsReply(lspserver: dict, 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, diff --git a/autoload/lsp/lspserver.vim b/autoload/lsp/lspserver.vim index c2d0763..78d8141 100644 --- a/autoload/lsp/lspserver.vim +++ b/autoload/lsp/lspserver.vim @@ -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, 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) + # 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, method: string, params: any, handleError: bool = true): dict @@ -382,12 +413,7 @@ def Rpc(lspserver: dict, 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, 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 +def GetPosition(lspserver: dict, find_ident: bool): dict var lnum: number = line('.') - 1 var col: number = charcol('.') - 1 var line = getline('.') @@ -599,16 +620,19 @@ def GetLspPosition(find_ident: bool): dict # 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> +def GetTextDocPosition(lspserver: dict, find_ident: bool): dict> # 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, 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, 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, msg: string, peekSymbol: bool, return endif + var result = reply.result var location: dict - 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, 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): 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, 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, peek: bool): void # interface ReferenceParams # interface TextDocumentPositionParams var param: dict - 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, 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, 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, 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, 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 = getcurpos() @@ -1016,7 +1067,7 @@ def PrepareCallHierarchy(lspserver: dict): dict # interface CallHierarchyPrepareParams # interface TextDocumentPositionParams var param: dict - 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, item: dict): 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, item: dict): 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) } } + lspserver.encodeRange(bufnr(), param.range) + var msg: string if lspserver.isClangdInlayHintsProvider # clangd-style inlay hints @@ -1113,6 +1186,28 @@ def InlayHintsShow(lspserver: dict) var reply = lspserver.rpc_a(msg, param, inlayhints.InlayHintsReply) enddef +def DecodeTypeHierarchy(lspserver: dict, isSuper: bool, typeHier: dict) + if !lspserver.needOffsetEncoding + return + endif + var bnr = util.LspUriToBufnr(typeHier.uri) + lspserver.decodeRange(bnr, typeHier.range) + lspserver.decodeRange(bnr, typeHier.selectionRange) + var subType: list> + 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, direction: number) # interface TypeHierarchy # interface TextDocumentPositionParams var param: dict - 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, 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, workspaceEdit: dict) + 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, newName: string) # interface RenameParams # interface TextDocumentPositionParams var param: dict = {} - 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, newName: string) endif # result: WorkspaceEdit + DecodeWorkspaceEdit(lspserver, reply.result) textedit.ApplyWorkspaceEdit(reply.result) enddef +# Decode the range in "CodeAction" +def DecodeCodeAction(lspserver: dict, actionList: list>) + 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, fname_arg: string, line1: number, @@ -1184,17 +1333,24 @@ def CodeAction(lspserver: dict, fname_arg: string, line1: number, var r: dict> = { 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> = [] for lnum in range(line1, line2) - var diagsInfo: list> = diag.GetDiagsByLine(bnr, lnum, lspserver) + var diagsInfo: list> = 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, 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, 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, codeLens: dict): dict +def ResolveCodeLens(lspserver: dict, bnr: number, + codeLens: dict): dict 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 = 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, dirName: string): void lspserver.workspaceFolders->remove(idx) enddef +def DecodeSelectionRange(lspserver: dict, bnr: number, selRange: dict) + 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, 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, pat: string, flags: string, info: dict): # 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, pat: string, flags: string, info: dict): 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, 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 index 0000000..b6bbe38 --- /dev/null +++ b/autoload/lsp/offset.vim @@ -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, bnr: number, pos: dict) + 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, bnr: number, pos: dict) + 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, bnr: number, + range: dict>) + 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, bnr: number, + range: dict>) + 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, location: dict) + 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, location: dict) + 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 diff --git a/autoload/lsp/symbol.vim b/autoload/lsp/symbol.vim index 5914f92..761ef18 100644 --- a/autoload/lsp/symbol.vim +++ b/autoload/lsp/symbol.vim @@ -196,6 +196,7 @@ export def WorkspaceSymbolPopup(lspserver: dict, 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, enddef # process SymbolInformation[] -def ProcessSymbolInfoTable(symbolInfoTable: list>, - symbolTypeTable: dict>>, - symbolLineTable: list>) +def ProcessSymbolInfoTable(lspserver: dict, + bnr: number, + symbolInfoTable: list>, + symbolTypeTable: dict>>, + symbolLineTable: list>) var fname: string var symbolType: string var name: string @@ -532,6 +535,7 @@ def ProcessSymbolInfoTable(symbolInfoTable: list>, 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>, enddef # process DocumentSymbol[] -def ProcessDocSymbolTable(docSymbolTable: list>, - symbolTypeTable: dict>>, - symbolLineTable: list>) +def ProcessDocSymbolTable(lspserver: dict, + bnr: number, + docSymbolTable: list>, + symbolTypeTable: dict>>, + symbolLineTable: list>) var symbolType: string var name: string var r: dict> @@ -556,7 +562,8 @@ def ProcessDocSymbolTable(docSymbolTable: list>, 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>, 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, docsymbol: any, fname: string) var symbolTypeTable: dict>> = {} var symbolLineTable: list> = [] + 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, 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 diff --git a/doc/lsp.txt b/doc/lsp.txt index 4c793fe..14b4cd9 100644 --- a/doc/lsp.txt +++ b/doc/lsp.txt @@ -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 index 0000000..c61b37c --- /dev/null +++ b/test/clangd_offsetencoding.vim @@ -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 + void fn(int aVar) + { + printf("aVar = %d\n", aVar); + printf("😊😊😊😊 = %d\n", aVar): + printf("áb́áb́ = %d\n", aVar): + printf("ą́ą́ą́ą́ = %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("áb́áb́ = %d\n", aVar);', getline(6)) + cursor(7, 5) + redraw! + :LspCodeAction 1 + assert_equal(' printf("ą́ą́ą́ą́ = %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 + void fn(int aVar) + { + printf("aVar = %d\n", aVar); + printf("😊😊😊😊 = %d\n". aVar); + printf("áb́áb́ = %d\n". aVar); + printf("ą́ą́ą́ą́ = %d\n". aVar); + } + END + setline(1, lines) + g:WaitForServerFileLoad(3) + :redraw! + :LspDiagShow + var qfl: list> = 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 áb́áb́ = aVar + 1; + int ą́ą́ą́ą́ = aVar + 1; + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + :redraw! + :LspFormat + var expected =<< trim END + void fn(int aVar) { + int 😊😊😊😊 = aVar + 1; + int áb́áb́ = aVar + 1; + int ą́ą́ą́ą́ = 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 =<< trim END + #include + void fn(int aVar) + { + printf("aVar = %d\n", aVar); + printf("😊😊😊😊 = %d\n", aVar); + printf("áb́áb́ = %d\n", aVar); + printf("ą́ą́ą́ą́ = %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) + { + /* αβγδ, 😊😊😊😊, áb́áb́, ą́ą́ą́ą́ */ int αβγδ, bVar; + /* αβγδ, 😊😊😊😊, áb́áb́, ą́ą́ą́ą́ */ int 😊😊😊😊, cVar; + /* αβγδ, 😊😊😊😊, áb́áb́, ą́ą́ą́ą́ */ int áb́áb́, dVar; + /* αβγδ, 😊😊😊😊, áb́áb́, ą́ą́ą́ą́ */ int ą́ą́ą́ą́, eVar; + bVar = 1; + cVar = 2; + dVar = 3; + eVar = 4; + aVar = αβγδ + 😊😊😊😊 + áb́áb́ + ą́ą́ą́ą́ + 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 =<< trim END + void Func1(void) + { + int 😊😊😊😊, aVar; + int áb́áb́, bVar; + int ą́ą́ą́ą́, cVar; + + + + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + + cursor(6, 4) + feedkeys("aaV\\ = 😊😊\\;", 'xt') + assert_equal(' aVar = 😊😊😊😊;', getline('.')) + cursor(7, 4) + feedkeys("abV\\ = áb́\\;", 'xt') + assert_equal(' bVar = áb́áb́;', getline('.')) + cursor(8, 4) + feedkeys("acV\\ = ą́ą́\\;", 'xt') + assert_equal(' cVar = ą́ą́ą́ą́;', getline('.')) + feedkeys("oáb́\\ = ą́ą́\\;", 'xt') + assert_equal(' áb́áb́ = ą́ą́ą́ą́;', getline('.')) + feedkeys("oą́ą́\\ = áb́\\;", 'xt') + assert_equal(' ą́ą́ą́ą́ = áb́á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 =<< trim END + typedef void 😊😊😊😊; + typedef void áb́áb́; + typedef void ą́ą́ą́ą́; + + 😊😊😊😊 Func1() + { + } + + áb́áb́ Func2() + { + } + + ą́ą́ą́ą́ Func3() + { + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + + cursor(1, 1) + :LspOutline + assert_equal(2, winnr('$')) + + :wincmd w + cursor(5, 1) + feedkeys("\", 'xt') + assert_equal([2, 5, 18], [winnr(), line('.'), col('.')]) + + :wincmd w + cursor(6, 1) + feedkeys("\", 'xt') + assert_equal([2, 9, 14], [winnr(), line('.'), col('.')]) + + :wincmd w + cursor(7, 1) + feedkeys("\", 'xt') + assert_equal([2, 13, 22], [winnr(), line('.'), col('.')]) + + :wincmd w + cursor(10, 1) + feedkeys("\", 'xt') + assert_equal([2, 1, 14], [winnr(), line('.'), col('.')]) + + :wincmd w + cursor(11, 1) + feedkeys("\", 'xt') + assert_equal([2, 2, 14], [winnr(), line('.'), col('.')]) + + :wincmd w + cursor(12, 1) + feedkeys("\", '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 =<< trim END + #include + void fn(int aVar) + { + printf("aVar = %d\n", aVar); + printf("😊😊😊😊 = %d\n", aVar); + printf("áb́áb́ = %d\n", aVar); + printf("ą́ą́ą́ą́ = %d\n", aVar); + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + cursor(2, 12) + :LspRename bVar + redraw! + var expected: list =<< trim END + #include + void fn(int bVar) + { + printf("aVar = %d\n", bVar); + printf("😊😊😊😊 = %d\n", bVar); + printf("áb́áb́ = %d\n", bVar); + printf("ą́ą́ą́ą́ = %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 =<< trim END + #include + void fn(int aVar) + { + printf("aVar = %d\n", aVar); + printf("😊😊😊😊 = %d\n", aVar); + printf("áb́áb́ = %d\n", aVar); + printf("ą́ą́ą́ą́ = %d\n", aVar); + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + cursor(4, 27) + :LspShowReferences + var qfl: list> = 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 =<< trim END + typedef void 😊😊😊😊; + typedef void áb́áb́; + typedef void ą́ą́ą́ą́; + + 😊😊😊😊 Func1() + { + } + + áb́áb́ Func2() + { + } + + ą́ą́ą́ą́ Func3() + { + } + END + setline(1, lines) + g:WaitForServerFileLoad(0) + + cursor(1, 1) + feedkeys(":LspSymbolSearch Func1\\", "xt") + assert_equal([5, 18], [line('.'), col('.')]) + cursor(1, 1) + feedkeys(":LspSymbolSearch Func2\\", "xt") + assert_equal([9, 14], [line('.'), col('.')]) + cursor(1, 1) + feedkeys(":LspSymbolSearch Func3\\", "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 áb́áb́, cVar; + int ą́ą́ą́ą́, 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 \" + assert_equal([3, 27], [line('.'), col('.')]) + cursor(7, 5) + :exe "normal \" + assert_equal([4, 23], [line('.'), col('.')]) + cursor(8, 5) + :exe "normal \" + 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 + /* αβ😊😊ááą́ą́ */ class parent { + }; + + /* αβ😊😊ááą́ą́ */ class child : public parent { + }; + + /* αβ😊😊ááą́ą́ */ class grandchild : public child { + }; + END + setline(1, lines) + g:WaitForServerFileLoad(0) + redraw! + + cursor(1, 42) + :LspSubTypeHierarchy + call feedkeys("\", 'xt') + assert_equal([1, 36], [line('.'), col('.')]) + cursor(1, 42) + + :LspSubTypeHierarchy + call feedkeys("\\", 'xt') + assert_equal([4, 42], [line('.'), col('.')]) + + cursor(1, 42) + :LspSubTypeHierarchy + call feedkeys("\\\", 'xt') + assert_equal([7, 42], [line('.'), col('.')]) + + cursor(7, 42) + :LspSuperTypeHierarchy + call feedkeys("\", 'xt') + assert_equal([7, 36], [line('.'), col('.')]) + + cursor(7, 42) + :LspSuperTypeHierarchy + call feedkeys("\\", 'xt') + assert_equal([4, 36], [line('.'), col('.')]) + + cursor(7, 42) + :LspSuperTypeHierarchy + call feedkeys("\\\", 'xt') + assert_equal([1, 36], [line('.'), col('.')]) + + :%bw! +enddef + +# vim: shiftwidth=2 softtabstop=2 noexpandtab diff --git a/test/clangd_tests.vim b/test/clangd_tests.vim index 88298f6..7eabcf3 100644 --- a/test/clangd_tests.vim +++ b/test/clangd_tests.vim @@ -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 áb́áb́ = aVar + 1; - int ą́ą́ą́ą́ = aVar + 1; - } - END - setline(1, lines) - g:WaitForServerFileLoad(0) - :redraw! - :LspFormat - var expected =<< trim END - void fn(int aVar) { - int 😊😊😊😊 = aVar + 1; - int áb́áb́ = aVar + 1; - int ą́ą́ą́ą́ = 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 =<< trim END - #include - void fn(int aVar) - { - printf("aVar = %d\n", aVar); - printf("😊😊😊😊 = %d\n", aVar); - printf("áb́áb́ = %d\n", aVar); - printf("ą́ą́ą́ą́ = %d\n", aVar); - } - END - setline(1, lines) - g:WaitForServerFileLoad(0) - redraw! - cursor(4, 27) - :LspShowReferences - var qfl: list> = 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 - void fn(int aVar) - { - printf("aVar = %d\n", aVar); - printf("😊😊😊😊 = %d\n". aVar); - printf("áb́áb́ = %d\n". aVar); - printf("ą́ą́ą́ą́ = %d\n". aVar); - } - END - setline(1, lines) - g:WaitForServerFileLoad(3) - :redraw! - :LspDiagShow - var qfl: list> = 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 - void fn(int aVar) - { - printf("aVar = %d\n", aVar); - printf("😊😊😊😊 = %d\n", aVar): - printf("áb́áb́ = %d\n", aVar): - printf("ą́ą́ą́ą́ = %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("áb́áb́ = %d\n", aVar);', getline(6)) - cursor(7, 5) - redraw! - :LspCodeAction 1 - assert_equal(' printf("ą́ą́ą́ą́ = %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 =<< trim END - #include - void fn(int aVar) - { - printf("aVar = %d\n", aVar); - printf("😊😊😊😊 = %d\n", aVar); - printf("áb́áb́ = %d\n", aVar); - printf("ą́ą́ą́ą́ = %d\n", aVar); - } - END - setline(1, lines) - g:WaitForServerFileLoad(0) - redraw! - cursor(2, 12) - :LspRename bVar - redraw! - var expected: list =<< trim END - #include - void fn(int bVar) - { - printf("aVar = %d\n", bVar); - printf("😊😊😊😊 = %d\n", bVar); - printf("áb́áb́ = %d\n", bVar); - printf("ą́ą́ą́ą́ = %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 =<< trim END - #include - void fn(int aVar) - { - printf("aVar = %d\n", aVar); - printf("😊😊😊😊 = %d\n", aVar); - printf("áb́áb́ = %d\n", aVar); - printf("ą́ą́ą́ą́ = %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 áb́áb́, cVar; - int ą́ą́ą́ą́, 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 =<< trim END - typedef void 😊😊😊😊; - typedef void áb́áb́; - typedef void ą́ą́ą́ą́; - - 😊😊😊😊 Func1() - { - } - - áb́áb́ Func2() - { - } - - ą́ą́ą́ą́ Func3() - { - } - END - setline(1, lines) - g:WaitForServerFileLoad(0) - - cursor(1, 1) - feedkeys(":LspSymbolSearch Func1\\", "xt") - assert_equal([5, 18], [line('.'), col('.')]) - cursor(1, 1) - feedkeys(":LspSymbolSearch Func2\\", "xt") - assert_equal([9, 14], [line('.'), col('.')]) - cursor(1, 1) - feedkeys(":LspSymbolSearch Func3\\", "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 áb́áb́, cVar; - int ą́ą́ą́ą́, 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 \" - assert_equal([3, 27], [line('.'), col('.')]) - cursor(7, 5) - :exe "normal \" - assert_equal([4, 23], [line('.'), col('.')]) - cursor(8, 5) - :exe "normal \" - 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 =<< trim END - void Func1(void) - { - int 😊😊😊😊, aVar; - int áb́áb́, bVar; - int ą́ą́ą́ą́, cVar; - - - - } - END - setline(1, lines) - g:WaitForServerFileLoad(0) - redraw! - - cursor(6, 4) - feedkeys("aaV\\ = 😊😊\\;", 'xt') - assert_equal(' aVar = 😊😊😊😊;', getline('.')) - cursor(7, 4) - feedkeys("abV\\ = áb́\\;", 'xt') - assert_equal(' bVar = áb́áb́;', getline('.')) - cursor(8, 4) - feedkeys("acV\\ = ą́ą́\\;", 'xt') - assert_equal(' cVar = ą́ą́ą́ą́;', getline('.')) - feedkeys("oáb́\\ = ą́ą́\\;", 'xt') - assert_equal(' áb́áb́ = ą́ą́ą́ą́;', getline('.')) - feedkeys("oą́ą́\\ = áb́\\;", 'xt') - assert_equal(' ą́ą́ą́ą́ = áb́áb́;', getline('.')) - :%bw! -enddef - # Test for the :LspServer command. def g:Test_LspServer() new a.raku diff --git a/test/run_tests.sh b/test/run_tests.sh index 151dcbd..625ba6f 100755 --- a/test/run_tests.sh +++ b/test/run_tests.sh @@ -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." -- 2.44.0