bb8dd13ce8a8b717dbebf191c9a872ff11e27e85
[nit.git] / misc / vim / plugin / nit.vim
1 " This file is part of NIT ( http://www.nitlanguage.org ).
2 "
3 " Licensed under the Apache License, Version 2.0 (the "License");
4 " you may not use this file except in compliance with the License.
5 " You may obtain a copy of the License at
6 "
7 "                http://www.apache.org/licenses/LICENSE-2.0
8 "
9 " Unless required by applicable law or agreed to in writing, software
10 " distributed under the License is distributed on an "AS IS" BASIS,
11 " WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 " See the License for the specific language governing permissions and
13 " limitations under the License.
14
15 " Nit plugin for Vim, provides some advanced features
16
17 if v:version < 700
18         finish
19 endif
20
21 if exists("loaded_nit_plugin")
22         finish
23 endif
24 let loaded_nit_plugin = 1
25
26 " Scan all relevant Nit modules for the current directory to autocomplete
27 "
28 " The guard `g:nit_complete_done` ensures that its body is executed only
29 " once. The call to `nitls -M` analyzses the current directory. However,
30 " updating the module list can be forced using ForceNitComplete.
31 "
32 " To activate, add the following line to ~/.vimrc
33 "
34 " autocmd Filetype nit call NitComplete()
35 function NitComplete()
36         if !exists("g:nit_complete_done")
37                 let g:nit_complete_done = 1
38
39                 " Reset or backup the original complete
40                 if !exists("g:nit_complete_backup")
41                         let g:nit_complete_backup = &complete
42                 else
43                         silent let &complete = g:nit_complete_backup
44                         silent set complete?
45                 endif
46
47                 " This gives us better results for Nit
48                 set noignorecase
49                 set completeopt=longest,menuone,preview
50
51                 " Do not predict small 3 letters keywords (or their prefix), they slow down
52                 " prediction and some also require double-enter on end of line.
53                 let g:acp_behaviorKeywordIgnores = ['new', 'var', 'in', 'do', 'els', 'end', 'ret', 'for', 'fun']
54
55                 " Use nitls to compute all interesting files from the current directory and the standard library
56                 for file in split(system('nitls -M standard .', '\n'))
57                         silent let &complete = &complete . ',s' . file
58                         silent set complete?
59                 endfor
60
61                 " Compatibility with AutoComplPop
62                 let g:acp_completeOption = &complete
63                 let g:acp_ignorecaseOption = &ignorecase
64
65                 " Redraw in case the user pressed some keys while waiting
66                 redraw!
67         endif
68 endfunction
69
70 " Force updating the Nit modules used for autocomplete
71 "
72 " It is recommended to manually call this function as needed. It can be mapped
73 " to a key with:
74 "
75 " map <F2> :call ForceNitComplete()
76 "
77 " For small projects (or fast computers) you might want to call it on each
78 " file save or load.
79 function ForceNitComplete()
80         unlet! g:nit_complete_done
81         call NitComplete()
82 endfunction
83
84 " Get path to the best metadata file named `name`
85 "
86 " Returns an empty string if not found.
87 fun s:NitMetadataFile(name)
88         " Where are the generated metadata files?
89         if empty($NIT_VIM_DIR)
90                 let metadata_dir = $HOME . '/.vim/nit'
91         else
92                 let metadata_dir = $NIT_VIM_DIR
93         end
94
95         let path = metadata_dir . '/' . a:name
96
97         " Is there generated custom metadata files?
98         if ! filereadable(path)
99                 let path = s:script_dir . '/' . a:name
100
101                 " Is there standard metadata files?
102                 if ! filereadable(path)
103                         return ''
104                 endif
105         endif
106
107         return path
108 endfun
109
110 " Internal function to search for lines in `path` corresponding to the partial
111 " word `base`. Adds found and formated match to `matches`.
112 "
113 " Will order the results in 5 levels:
114 " 1. Exact matches
115 " 2. Common prefix matches
116 " 3. Substring matches
117 " 4. Synopsis matches
118 " 5. Doc matches
119 fun NitOmnifuncAddFromFile(base, matches, path)
120         let prefix_matches = []
121         let substring_matches = []
122         let synopsis_matches = []
123         let doc_matches = []
124
125         let path = s:NitMetadataFile(a:path)
126         if empty(path)
127                 return
128         endif
129
130         for line in readfile(path)
131                 let words = split(line, '#====#', 1)
132                 let name = get(words, 0, '')
133
134                 " Add?
135                 if name == a:base
136                         " Exact match
137                         call s:NitOmnifuncAddAMatch(a:matches, words, name)
138                 elseif name =~? '^'.a:base
139                         " Common-prefix match
140                         call s:NitOmnifuncAddAMatch(prefix_matches, words, name)
141                 elseif name =~? a:base
142                         " Substring match
143                         call s:NitOmnifuncAddAMatch(substring_matches, words, name)
144                 elseif get(words, 2, '') =~? a:base
145                         " Match in the synopsis
146                         call s:NitOmnifuncAddAMatch(synopsis_matches, words, name)
147                 elseif get(words, 3, '') =~? a:base
148                         " Match in the longer doc
149                         call s:NitOmnifuncAddAMatch(doc_matches, words, name)
150                 endif
151         endfor
152
153         " Assemble the final match list
154         call extend(a:matches, sort(prefix_matches))
155         call extend(a:matches, sort(substring_matches))
156         call extend(a:matches, sort(synopsis_matches))
157         call extend(a:matches, sort(doc_matches))
158 endfun
159
160 " Internal function to search parse the information from a metadata line
161 fun s:NitOmnifuncAddAMatch(matches, words, name)
162         let pretty = get(a:words, 1, '')
163         let synopsis = get(a:words, 2, '')
164         let desc = get(a:words, 3, '')
165         let desc = join(split(desc, '#nnnn#', 1), "\n")
166         if strlen(pretty) > 40
167                 let pretty = pretty[:39] . '…'
168         endif
169         call add(a:matches, {'word': a:name, 'abbr': pretty, 'menu': synopsis, 'info': desc, 'dup': 1})
170 endfun
171
172 " Omnifunc using metadata files generated by nitpick to offer better
173 " contextual autocomplete for Nit source code.
174 fun NitOmnifunc(findstart, base)
175         if a:findstart
176                 " locate the start of the word
177                 let line = getline('.')
178                 let start = col('.') - 1
179                 while start > 0 && line[start - 1] =~ '\w'
180                         let start -= 1
181                 endwhile
182                 return start
183         else
184                 " find keyword matching with "a:base"
185                 let matches = []
186
187                 " advanced suggestions
188                 let cursor_line = getline('.')
189
190                 " content of the line before the partial word
191                 let line_prev_cursor = cursor_line[:col('.')-1]
192
193                 let prev_char_at = strlen(line_prev_cursor) - 1
194                 while prev_char_at > 0 && line_prev_cursor[prev_char_at] =~ '\s'
195                         let prev_char_at -= 1
196                 endwhile
197
198                 " Non whitespace char just before the partial word
199                 let prev_char = line_prev_cursor[prev_char_at]
200
201                 " Nity words on the current line before the partial word
202                 let prev_words = split(line_prev_cursor, '\W\+')
203
204                 " The word right before the partial word
205                 let prev_word = get(prev_words, -1, '')
206
207                 " Have we found a promising heuristic yet?
208                 let handled = 0
209
210                 " Modules
211                 if prev_word == 'import' && ! (line_prev_cursor =~ 'fun')
212                         let handled = 1
213                         call NitOmnifuncAddFromFile(a:base, matches, 'modules.txt')
214                 endif
215
216                 " Classes (instanciable only)
217                 if prev_word == 'new'
218                         let handled = 1
219                         call NitOmnifuncAddFromFile(a:base, matches, 'constructors.txt')
220                 endif
221
222                 " Other class references
223                 if count(['class', 'super'], prev_word) > 0
224                         let handled = 1
225                         call NitOmnifuncAddFromFile(a:base, matches, 'classes.txt')
226                 endif
227
228                 " Types
229                 if count(['class', 'super', 'nullable', 'isa', 'as'], prev_word) > 0 ||
230                  \ line_prev_cursor =~ '[' || prev_char == ':' ||
231                  \ (line_prev_cursor =~ 'fun' && line_prev_cursor =~ 'import' && (prev_word == 'import' || prev_char == ','))
232                         let handled = 1
233                         call NitOmnifuncAddFromFile(a:base, matches, 'types.txt')
234                 endif
235
236                 " Properties
237                 if prev_char == '.' || line_prev_cursor =~ '['
238                         let handled = 1
239                         call NitOmnifuncAddFromFile(a:base, matches, 'properties.txt')
240                 endif
241
242                 " If is nothing else...
243                 if !handled
244                         " It may be a keyword
245                         if strlen(a:base) > 0
246                                 for keyword in ['module', 'import', 'class', 'abstract', 'interface',
247                                         \'universal', 'enum', 'end', 'fun', 'type', 'init', 'redef', 'is',
248                                         \'do', 'var', 'extern', 'public', 'protected', 'private', 'intrude',
249                                         \'if', 'then', 'else', 'while', 'loop', 'for', 'in', 'and', 'or',
250                                         \'not', 'implies', 'return', 'continue', 'break', 'abort', 'assert',
251                                         \'new', 'isa', 'once', 'super', 'self', 'true', 'false', 'null',
252                                         \'as', 'nullable', 'isset', 'label']
253
254                                         if keyword =~ '^' . a:base
255                                                 call add(matches, keyword)
256                                         endif
257                                 endfor
258                         endif
259
260                         " it may still be a method call or property access
261                         call NitOmnifuncAddFromFile(a:base, matches, 'properties.txt')
262                 endif
263
264                 return {'words': matches, 'refresh': 'always'}
265         endif
266 endfun
267
268 " Show doc for the entity under the cursor in the preview window
269 fun Nitdoc(...)
270         if a:0 == 0 || empty(a:1)
271                 " Word under cursor
272                 let word = expand("<cword>")
273         else
274                 let word = join(a:000, ' ')
275         endif
276
277         " All possible docs (there may be more than one entity with the same name)
278         let docs = []
279         let prefix_matches = []
280         let substring_matches = []
281         let synopsis_matches = []
282         let doc_matches = []
283
284         " Search in all metadata files
285         for file in ['modules', 'classes', 'properties']
286                 let path = s:NitMetadataFile(file.'.txt')
287                 if empty(path)
288                         continue
289                 endif
290
291                 for line in readfile(path)
292                         let words = split(line, '#====#', 1)
293                         let name = get(words, 0, '')
294                         if name =~ '^'.word.'\>'
295                                 " Exact match
296                                 call s:NitdocAdd(docs, words)
297                         elseif name =~? '^'.word
298                                 " Common-prefix match
299                                 call s:NitdocAdd(prefix_matches, words)
300                         elseif name =~? word
301                                 " Substring match
302                                 call s:NitdocAdd(substring_matches, words)
303                         elseif get(words, 2, '') =~? word
304                                 " Match in the synopsis
305                                 call s:NitdocAdd(synopsis_matches, words)
306                         elseif get(words, 3, '') =~? word
307                                 " Match in the longer doc
308                                 call s:NitdocAdd(doc_matches, words)
309                         endif
310                 endfor
311         endfor
312
313         " Unite all results in prefered order
314         call extend(docs, sort(prefix_matches))
315         call extend(docs, sort(substring_matches))
316         call extend(docs, sort(synopsis_matches))
317         call extend(docs, sort(doc_matches))
318
319         " Found no doc, give up
320         if empty(docs) || !(join(docs, '') =~ '\w')
321                 echo 'Nitdoc found nothing for "' . word . '"'
322                 return
323         endif
324
325         " Open the preview window on a temp file
326         execute "silent pedit " . tempname()
327
328         " Change to preview window
329         wincmd P
330
331         " Show all found doc one after another
332         for doc in docs
333                 if doc =~ '\w'
334                         silent put = doc
335                         silent put = ''
336                 endif
337         endfor
338         execute 0
339         delete " the first empty line
340
341         " Set options
342         setlocal buftype=nofile
343         setlocal noswapfile
344         setlocal syntax=none
345         setlocal bufhidden=delete
346
347         " Change back to the source buffer
348         wincmd p
349         redraw!
350 endfun
351
352 " Internal function to search parse the information from a metadata line
353 fun s:NitdocAdd(matches, words)
354         let desc = get(a:words, 3, '')
355         let desc = join(split(desc, '#nnnn#', 1), "\n")
356         call add(a:matches, desc)
357 endfun
358
359 " Call `git grep` on the word under the cursor
360 "
361 " Shows declarations first, then all matches, in the preview window.
362 fun NitGitGrep()
363         let word = expand("<cword>")
364         let out = tempname()
365         execute 'silent !(git grep "\\(module\\|class\\|universal\\|interface\\|var\\|fun\\) '.word.'";'.
366                 \'echo; git grep '.word.') > '.out
367
368         " Open the preview window on a temp file
369         execute "silent pedit " . out
370
371         " Change to preview window
372         wincmd P
373
374         " Set options
375         setlocal buftype=nofile
376         setlocal noswapfile
377         setlocal syntax=none
378         setlocal bufhidden=delete
379
380         " Change back to the source buffer
381         wincmd p
382         redraw!
383 endfun
384
385 if !exists("g:nit_disable_omnifunc") || !g:nit_disable_omnifunc
386         " Activate the omnifunc on Nit files
387         autocmd FileType nit set omnifunc=NitOmnifunc
388 endif
389
390 " Define the user command Nitdoc for ease of use
391 command -nargs=* Nitdoc call Nitdoc("<args>")
392
393 let s:script_dir = fnamemodify(resolve(expand('<sfile>:p')), ':h')