3 # Display an info message
4 export def InfoMsg(msg: string)
6 :echomsg $'Info: {msg}'
10 # Display a warning message
11 export def WarnMsg(msg: string)
13 :echomsg $'Warn: {msg}'
17 # Display an error message
18 export def ErrMsg(msg: string)
20 :echomsg $'Error: {msg}'
24 # Lsp server trace log directory
25 var lsp_log_dir: string
29 lsp_log_dir = $TEMP .. '\\'
32 # Log a message from the LSP server. stderr is true for logging messages
33 # from the standard error and false for stdout.
34 export def TraceLog(fname: string, stderr: bool, msg: string)
36 writefile(msg->split("\n"), $'{lsp_log_dir}{fname}', 'a')
38 writefile([msg], $'{lsp_log_dir}{fname}', 'a')
42 # Empty out the LSP server trace logs
43 export def ClearTraceLogs(fname: string)
44 writefile([], $'{lsp_log_dir}{fname}')
47 # Open the LSP server debug messages file.
48 export def ServerMessagesShow(fname: string)
49 var fullname = $'{lsp_log_dir}{fname}'
50 if !filereadable(fullname)
51 WarnMsg($'File {fullname} is not found')
54 var wid = fullname->bufwinid()
56 exe $'split {fullname}'
61 setlocal bufhidden=wipe
66 # Parse a LSP Location or LocationLink type and return a List with two items.
67 # The first item is the DocumentURI and the second item is the Range.
68 export def LspLocationParse(lsploc: dict<any>): list<any>
69 if lsploc->has_key('targetUri')
71 return [lsploc.targetUri, lsploc.targetSelectionRange]
74 return [lsploc.uri, lsploc.range]
78 # Convert a LSP file URI (file://<absolute_path>) to a Vim file name
79 export def LspUriToFile(uri: string): string
80 # Replace all the %xx numbers (e.g. %20 for space) in the URI to character
81 var uri_decoded: string = substitute(uri, '%\(\x\x\)',
82 '\=nr2char(str2nr(submatch(1), 16))', 'g')
84 # File URIs on MS-Windows start with file:///[a-zA-Z]:'
85 if uri_decoded =~? '^file:///\a:'
87 uri_decoded = uri_decoded[8 : ]
88 uri_decoded = uri_decoded->substitute('/', '\\', 'g')
89 # On GNU/Linux (pattern not end with `:`)
90 elseif uri_decoded =~? '^file:///\a'
91 uri_decoded = uri_decoded[7 : ]
97 # Convert a LSP file URI (file://<absolute_path>) to a Vim buffer number.
98 # If the file is not in a Vim buffer, then adds the buffer.
100 export def LspUriToBufnr(uri: string): number
101 return LspUriToFile(uri)->bufadd()
104 # Returns if the URI refers to a remote file (e.g. ssh://)
105 # Credit: vim-lsp plugin
106 export def LspUriRemote(uri: string): bool
107 return uri =~ '^\w\+::' || uri =~ '^[a-z][a-z0-9+.-]*://'
110 var resolvedUris = {}
112 # Convert a Vim filename to an LSP URI (file://<absolute_path>)
113 export def LspFileToUri(fname: string): string
114 var fname_full: string = fname->fnamemodify(':p')
116 if resolvedUris->has_key(fname_full)
117 return resolvedUris[fname_full]
120 var uri: string = fname_full
123 # We're in Cygwin, convert POSIX style paths to Windows style.
124 # The substitution is to remove the '^@' escape character from the end of
126 uri = system($'cygpath -m {uri}')->substitute('^\(\p*\).*$', '\=submatch(1)', "")
129 var on_windows: bool = false
136 uri = uri->substitute('\\', '/', 'g')
139 uri = uri->substitute('\([^A-Za-z0-9-._~:/]\)',
140 '\=printf("%%%02x", char2nr(submatch(1)))', 'g')
143 uri = $'file:///{uri}'
145 uri = $'file://{uri}'
148 resolvedUris[fname_full] = uri
152 # Convert a Vim buffer number to an LSP URI (file://<absolute_path>)
153 export def LspBufnrToUri(bnr: number): string
154 return LspFileToUri(bnr->bufname())
157 # Returns the byte number of the specified LSP position in buffer "bnr".
158 # LSP's line and characters are 0-indexed.
159 # Vim's line and columns are 1-indexed.
160 # Returns a zero-indexed column.
161 export def GetLineByteFromPos(bnr: number, pos: dict<number>): number
162 var col: number = pos.character
163 # When on the first character, we can ignore the difference between byte and
169 # Need a loaded buffer to read the line and compute the offset
172 var ltext: string = bnr->getbufline(pos.line + 1)->get(0, '')
177 var byteIdx = ltext->byteidxcomp(col)
185 # Get the index of the character at [pos.line, pos.character] in buffer "bnr"
186 # without counting the composing characters. The LSP server counts composing
187 # characters as separate characters whereas Vim string indexing ignores the
188 # composing characters.
189 export def GetCharIdxWithoutCompChar(bnr: number, pos: dict<number>): number
190 var col: number = pos.character
191 # When on the first character, nothing to do.
196 # Need a loaded buffer to read the line and compute the offset
199 var ltext: string = bnr->getbufline(pos.line + 1)->get(0, '')
204 # Convert the character index that includes composing characters as separate
205 # characters to a byte index and then back to a character index ignoring the
206 # composing characters.
207 var byteIdx = ltext->byteidxcomp(col)
209 if byteIdx == ltext->strlen()
210 # Byte index points to the byte after the last byte.
211 return ltext->strcharlen()
213 return ltext->charidx(byteIdx, v:false)
220 # Get the index of the character at [pos.line, pos.character] in buffer "bnr"
221 # counting the composing characters as separate characters. The LSP server
222 # counts composing characters as separate characters whereas Vim string
223 # indexing ignores the composing characters.
224 export def GetCharIdxWithCompChar(ltext: string, charIdx: number): number
225 # When on the first character, nothing to do.
226 if charIdx <= 0 || ltext->empty()
230 # Convert the character index that doesn't include composing characters as
231 # separate characters to a byte index and then back to a character index
232 # that includes the composing characters as separate characters
233 var byteIdx = ltext->byteidx(charIdx)
235 if byteIdx == ltext->strlen()
236 return ltext->strchars()
238 return ltext->charidx(byteIdx, v:true)
245 # push the current location on to the tag stack
246 export def PushCursorToTagStack()
247 settagstack(winnr(), {items: [
252 tagname: expand('<cword>')
256 # Jump to the LSP "location". The "location" contains the file name, line
257 # number and character number. The user specified window command modifiers
258 # (e.g. topleft) are in "cmdmods".
259 export def JumpToLspLocation(location: dict<any>, cmdmods: string)
260 var [uri, range] = LspLocationParse(location)
261 var fname = LspUriToFile(uri)
263 # jump to the file and line containing the symbol
265 var bnr: number = fname->bufnr()
267 # Set the previous cursor location mark. Instead of using setpos(), m' is
268 # used so that the current location is added to the jump list.
271 var wid = fname->bufwinid()
276 # Reuse an existing buffer. If the current buffer has unsaved changes
277 # and 'hidden' is not set or if the current buffer is a special
278 # buffer, then open the buffer in a new window.
279 if (&modified && !&hidden) || &buftype != ''
280 exe $'belowright sbuffer {bnr}'
285 if (&modified && !&hidden) || &buftype != ''
286 # if the current buffer has unsaved changes and 'hidden' is not set,
287 # or if the current buffer is a special buffer, then open the file
289 exe $'belowright split {fname}'
297 exe $'{cmdmods} split {fname}'
299 setcursorcharpos(range.start.line + 1,
300 GetCharIdxWithoutCompChar(bufnr(), range.start) + 1)
303 # indexof() function is not present in older Vim 9 versions. So use this
305 export def Indexof(list: list<any>, CallbackFn: func(number, any): bool): number
308 if CallbackFn(ix, val)
316 # Find the nearest root directory containing a file or directory name from the
317 # list of names in "files" starting with the directory "startDir".
318 # Based on a similar implementation in the vim-lsp plugin.
319 # Searches upwards starting with the directory "startDir".
320 # If a file name ends with '/' or '\', then it is a directory name, otherwise
322 # Returns '' if none of the file and directory names in "files" can be found
323 # in one of the parent directories.
324 export def FindNearestRootDir(startDir: string, files: list<any>): string
325 var foundDirs: dict<bool> = {}
328 if file->type() != v:t_string || file->empty()
331 var isDir = file[-1 : ] == '/' || file[-1 : ] == '\'
334 relPath = finddir(file, $'{startDir};')
336 relPath = findfile(file, $'{startDir};')
341 var rootDir = relPath->fnamemodify(isDir ? ':p:h:h' : ':p:h')
342 foundDirs[rootDir] = true
344 if foundDirs->empty()
348 # Sort the directory names by length
349 var sortedList: list<string> = foundDirs->keys()->sort((a, b) => {
350 return b->len() - a->len()
353 # choose the longest matching path (the nearest directory from "startDir")
357 # vim: tabstop=8 shiftwidth=2 softtabstop=2