]> Sergey Matveev's repositories - vim-lsp.git/blob - autoload/lsp/util.vim
Update diags location list when the diags for the buffer changes. Update comments
[vim-lsp.git] / autoload / lsp / util.vim
1 vim9script
2
3 # Display an info message
4 export def InfoMsg(msg: string)
5   :echohl Question
6   :echomsg $'Info: {msg}'
7   :echohl None
8 enddef
9
10 # Display a warning message
11 export def WarnMsg(msg: string)
12   :echohl WarningMsg
13   :echomsg $'Warn: {msg}'
14   :echohl None
15 enddef
16
17 # Display an error message
18 export def ErrMsg(msg: string)
19   :echohl Error
20   :echomsg $'Error: {msg}'
21   :echohl None
22 enddef
23
24 # Lsp server trace log directory
25 var lsp_log_dir: string
26 if has('unix')
27   lsp_log_dir = '/tmp/'
28 else
29   lsp_log_dir = $TEMP .. '\\'
30 endif
31
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)
35   if stderr
36     writefile(msg->split("\n"), $'{lsp_log_dir}{fname}', 'a')
37   else
38     writefile([msg], $'{lsp_log_dir}{fname}', 'a')
39   endif
40 enddef
41
42 # Empty out the LSP server trace logs
43 export def ClearTraceLogs(fname: string)
44   writefile([], $'{lsp_log_dir}{fname}')
45 enddef
46
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')
52     return
53   endif
54   var wid = fullname->bufwinid()
55   if wid == -1
56     exe $'split {fullname}'
57   else
58     win_gotoid(wid)
59   endif
60   setlocal autoread
61   setlocal bufhidden=wipe
62   setlocal nomodified
63   setlocal nomodifiable
64 enddef
65
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')
70     # LocationLink
71     return [lsploc.targetUri, lsploc.targetSelectionRange]
72   else
73     # Location
74     return [lsploc.uri, lsploc.range]
75   endif
76 enddef
77
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')
83
84   # File URIs on MS-Windows start with file:///[a-zA-Z]:'
85   if uri_decoded =~? '^file:///\a:'
86     # MS-Windows URI
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 : ]
92   endif
93
94   return uri_decoded
95 enddef
96
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.
99 # Returns 0 on error.
100 export def LspUriToBufnr(uri: string): number
101   return LspUriToFile(uri)->bufadd()
102 enddef
103
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+.-]*://'
108 enddef
109
110 var resolvedUris = {}
111
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')
115
116   if resolvedUris->has_key(fname_full)
117     return resolvedUris[fname_full]
118   endif
119
120   var uri: string = fname_full
121
122   if has("win32unix")
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
125     # line.
126     uri = system($'cygpath -m {uri}')->substitute('^\(\p*\).*$', '\=submatch(1)', "")
127   endif
128
129   var on_windows: bool = false
130   if uri =~? '^\a:'
131     on_windows = true
132   endif
133
134   if on_windows
135     # MS-Windows
136     uri = uri->substitute('\\', '/', 'g')
137   endif
138
139   uri = uri->substitute('\([^A-Za-z0-9-._~:/]\)',
140                         '\=printf("%%%02x", char2nr(submatch(1)))', 'g')
141
142   if on_windows
143     uri = $'file:///{uri}'
144   else
145     uri = $'file://{uri}'
146   endif
147
148   resolvedUris[fname_full] = uri
149   return uri
150 enddef
151
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())
155 enddef
156
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
164   # character
165   if col <= 0
166     return col
167   endif
168
169   # Need a loaded buffer to read the line and compute the offset
170   bnr->bufload()
171
172   var ltext: string = bnr->getbufline(pos.line + 1)->get(0, '')
173   if ltext->empty()
174     return col
175   endif
176
177   var byteIdx = ltext->byteidxcomp(col)
178   if byteIdx != -1
179     return byteIdx
180   endif
181
182   return col
183 enddef
184
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.
192   if col <= 0
193     return col
194   endif
195
196   # Need a loaded buffer to read the line and compute the offset
197   bnr->bufload()
198
199   var ltext: string = bnr->getbufline(pos.line + 1)->get(0, '')
200   if ltext->empty()
201     return col
202   endif
203
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)
208   if byteIdx != -1
209     if byteIdx == ltext->strlen()
210       # Byte index points to the byte after the last byte.
211       return ltext->strcharlen()
212     else
213       return ltext->charidx(byteIdx, v:false)
214     endif
215   endif
216
217   return col
218 enddef
219
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()
227     return charIdx
228   endif
229
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)
234   if byteIdx != -1
235     if byteIdx == ltext->strlen()
236       return ltext->strchars()
237     else
238       return ltext->charidx(byteIdx, v:true)
239     endif
240   endif
241
242   return charIdx
243 enddef
244
245 # push the current location on to the tag stack
246 export def PushCursorToTagStack()
247   settagstack(winnr(), {items: [
248                          {
249                            bufnr: bufnr(),
250                            from: getpos('.'),
251                            matchnr: 1,
252                            tagname: expand('<cword>')
253                          }]}, 't')
254 enddef
255
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)
262
263   # jump to the file and line containing the symbol
264   if cmdmods->empty()
265     var bnr: number = fname->bufnr()
266     if bnr == 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.
269       :normal m'
270     else
271       var wid = fname->bufwinid()
272       if wid != -1
273         wid->win_gotoid()
274       else
275         if bnr != -1
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}'
281           else
282             exe $'buf {bnr}'
283           endif
284         else
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
288             # in a new window
289             exe $'belowright split {fname}'
290           else
291             exe $'edit {fname}'
292           endif
293         endif
294       endif
295     endif
296   else
297     exe $'{cmdmods} split {fname}'
298   endif
299   setcursorcharpos(range.start.line + 1,
300                    GetCharIdxWithoutCompChar(bufnr(), range.start) + 1)
301 enddef
302
303 # indexof() function is not present in older Vim 9 versions.  So use this
304 # function.
305 export def Indexof(list: list<any>, CallbackFn: func(number, any): bool): number
306   var ix = 0
307   for val in list
308     if CallbackFn(ix, val)
309       return ix
310     endif
311     ix += 1
312   endfor
313   return -1
314 enddef
315
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
321 # it is a file name.
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> = {}
326
327   for file in files
328     if file->type() != v:t_string || file->empty()
329       continue
330     endif
331     var isDir = file[-1 : ] == '/' || file[-1 : ] == '\'
332     var relPath: string
333     if isDir
334       relPath = finddir(file, $'{startDir};')
335     else
336       relPath = findfile(file, $'{startDir};')
337     endif
338     if relPath->empty()
339       continue
340     endif
341     var rootDir = relPath->fnamemodify(isDir ? ':p:h:h' : ':p:h')
342     foundDirs[rootDir] = true
343   endfor
344   if foundDirs->empty()
345     return ''
346   endif
347
348   # Sort the directory names by length
349   var sortedList: list<string> = foundDirs->keys()->sort((a, b) => {
350     return b->len() - a->len()
351   })
352
353   # choose the longest matching path (the nearest directory from "startDir")
354   return sortedList[0]
355 enddef
356
357 # vim: tabstop=8 shiftwidth=2 softtabstop=2