046ab9f93793914574b6593f6ef9238fe85487a8
[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 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 = 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 NitOmnifuncAddAMatch(a:matches, words, name)
138                 elseif name =~? '^'.a:base
139                         " Common-prefix match
140                         call NitOmnifuncAddAMatch(prefix_matches, words, name)
141                 elseif name =~? a:base
142                         " Substring match
143                         call NitOmnifuncAddAMatch(substring_matches, words, name)
144                 elseif get(words, 2, '') =~? a:base
145                         " Match in the synopsis
146                         call NitOmnifuncAddAMatch(synopsis_matches, words, name)
147                 elseif get(words, 3, '') =~? a:base
148                         " Match in the longer doc
149                         call NitOmnifuncAddAMatch(synopsis_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 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         " Word under cursor
271         let word = expand("<cword>")
272
273         " All possible docs (there may be more than one entity with the same name)
274         let docs = []
275
276         " Search in all metadata files
277         for file in ['modules', 'classes', 'properties']
278                 let path = NitMetadataFile(file.'.txt')
279                 if empty(path)
280                         continue
281                 endif
282
283                 for line in readfile(path)
284                         let words = split(line, '#====#', 1)
285                         let name = get(words, 0, '')
286                         if name =~ '^' . word
287                                 " It fits our word, get long doc
288                                 let desc = get(words,3,'')
289                                 let desc = join(split(desc, '#nnnn#', 1), "\n")
290                                 call add(docs, desc)
291                         endif
292                 endfor
293         endfor
294
295         " Found no doc, give up
296         if empty(docs) || !(join(docs, '') =~ '\w')
297                 return
298         endif
299
300         " Open the preview window on a temp file
301         execute "silent pedit " . tempname()
302
303         " Change to preview window
304         wincmd P
305
306         " Show all found doc one after another
307         for doc in docs
308                 if doc =~ '\w'
309                         silent put = doc
310                         silent put = ''
311                 endif
312         endfor
313
314         " Set options
315         setlocal buftype=nofile
316         setlocal noswapfile
317         setlocal syntax=none
318         setlocal bufhidden=delete
319
320         " Change back to the source buffer
321         wincmd p
322         redraw!
323 endfun
324
325 " Call `git grep` on the word under the cursor
326 "
327 " Shows declarations first, then all matches, in the preview window.
328 fun NitGitGrep()
329         let word = expand("<cword>")
330         let out = tempname()
331         execute 'silent !(git grep "\\(module\\|class\\|universal\\|interface\\|var\\|fun\\) '.word.'";'.
332                 \'echo; git grep '.word.') > '.out
333
334         " Open the preview window on a temp file
335         execute "silent pedit " . out
336
337         " Change to preview window
338         wincmd P
339
340         " Set options
341         setlocal buftype=nofile
342         setlocal noswapfile
343         setlocal syntax=none
344         setlocal bufhidden=delete
345
346         " Change back to the source buffer
347         wincmd p
348         redraw!
349 endfun
350
351 " Activate the omnifunc on Nit files
352 autocmd FileType nit set omnifunc=NitOmnifunc
353
354 let s:script_dir = fnamemodify(resolve(expand('<sfile>:p')), ':h')