9968b5e04e578dd724fea7f30fa067ef97866b2b
[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
280         " Search in all metadata files
281         for file in ['modules', 'classes', 'properties']
282                 let path = s:NitMetadataFile(file.'.txt')
283                 if empty(path)
284                         continue
285                 endif
286
287                 for line in readfile(path)
288                         let words = split(line, '#====#', 1)
289                         let name = get(words, 0, '')
290                         if name =~ '^' . word . '\>'
291                                 " It fits our word, get long doc
292                                 let desc = get(words,3,'')
293                                 let desc = join(split(desc, '#nnnn#', 1), "\n")
294                                 call add(docs, desc)
295                         endif
296                 endfor
297         endfor
298
299         " Found no doc, give up
300         if empty(docs) || !(join(docs, '') =~ '\w')
301                 return
302         endif
303
304         " Open the preview window on a temp file
305         execute "silent pedit " . tempname()
306
307         " Change to preview window
308         wincmd P
309
310         " Show all found doc one after another
311         for doc in docs
312                 if doc =~ '\w'
313                         silent put = doc
314                         silent put = ''
315                 endif
316         endfor
317
318         " Set options
319         setlocal buftype=nofile
320         setlocal noswapfile
321         setlocal syntax=none
322         setlocal bufhidden=delete
323
324         " Change back to the source buffer
325         wincmd p
326         redraw!
327 endfun
328
329 " Call `git grep` on the word under the cursor
330 "
331 " Shows declarations first, then all matches, in the preview window.
332 fun NitGitGrep()
333         let word = expand("<cword>")
334         let out = tempname()
335         execute 'silent !(git grep "\\(module\\|class\\|universal\\|interface\\|var\\|fun\\) '.word.'";'.
336                 \'echo; git grep '.word.') > '.out
337
338         " Open the preview window on a temp file
339         execute "silent pedit " . out
340
341         " Change to preview window
342         wincmd P
343
344         " Set options
345         setlocal buftype=nofile
346         setlocal noswapfile
347         setlocal syntax=none
348         setlocal bufhidden=delete
349
350         " Change back to the source buffer
351         wincmd p
352         redraw!
353 endfun
354
355 " Activate the omnifunc on Nit files
356 autocmd FileType nit set omnifunc=NitOmnifunc
357
358 " Define the user command Nitdoc for ease of use
359 command -nargs=* Nitdoc call Nitdoc("<args>")
360
361 let s:script_dir = fnamemodify(resolve(expand('<sfile>:p')), ':h')