b1b115b06d871bc46ee2c912941ef83a36322236
[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 3 levels:
114 " 1. Exact matches
115 " 2. Common prefix matches
116 " 3. Substring matches
117 fun NitOmnifuncAddFromFile(base, matches, path)
118         let prefix_matches = []
119         let substring_matches = []
120
121         let path = NitMetadataFile(a:path)
122         if empty(path)
123                 return
124         endif
125
126         for line in readfile(path)
127                 let words = split(line, '#====#', 1)
128                 let name = get(words, 0, '')
129
130                 " Add?
131                 if name == a:base
132                         " Exact match
133                         call NitOmnifuncAddAMatch(a:matches, words, name)
134                 elseif name =~ '^'.a:base
135                         " Common-prefix match
136                         call NitOmnifuncAddAMatch(prefix_matches, words, name)
137                 elseif name =~ a:base
138                         " Substring match
139                         call NitOmnifuncAddAMatch(substring_matches, words, name)
140                 endif
141         endfor
142
143         " Assemble the final match list
144         call extend(a:matches, sort(prefix_matches))
145         call extend(a:matches, sort(substring_matches))
146 endfun
147
148 " Internal function to search parse the information from a metadata line
149 fun NitOmnifuncAddAMatch(matches, words, name)
150         let pretty = get(a:words, 1, '')
151         let synopsis = get(a:words, 2, '')
152         let desc = get(a:words, 3, '')
153         let desc = join(split(desc, '#nnnn#', 1), "\n")
154         if strlen(pretty) > 40
155                 let pretty = pretty[:39] . '…'
156         endif
157         call add(a:matches, {'word': a:name, 'abbr': pretty, 'menu': synopsis, 'info': desc, 'dup': 1})
158 endfun
159
160 " Omnifunc using metadata files generated by nitpick to offer better
161 " contextual autocomplete for Nit source code.
162 fun NitOmnifunc(findstart, base)
163         if a:findstart
164                 " locate the start of the word
165                 let line = getline('.')
166                 let start = col('.') - 1
167                 while start > 0 && line[start - 1] =~ '\w'
168                         let start -= 1
169                 endwhile
170                 return start
171         else
172                 " find keyword matching with "a:base"
173                 let matches = []
174
175                 " advanced suggestions
176                 let cursor_line = getline('.')
177
178                 " content of the line before the partial word
179                 let line_prev_cursor = cursor_line[:col('.')-1]
180
181                 let prev_char_at = strlen(line_prev_cursor) - 1
182                 while prev_char_at > 0 && line_prev_cursor[prev_char_at] =~ '\s'
183                         let prev_char_at -= 1
184                 endwhile
185
186                 " Non whitespace char just before the partial word
187                 let prev_char = line_prev_cursor[prev_char_at]
188
189                 " Nity words on the current line before the partial word
190                 let prev_words = split(line_prev_cursor, '\W\+')
191
192                 " The word right before the partial word
193                 let prev_word = get(prev_words, -1, '')
194
195                 " Have we found a promising heuristic yet?
196                 let handled = 0
197
198                 " Modules
199                 if prev_word == 'import' && ! (line_prev_cursor =~ 'fun')
200                         let handled = 1
201                         call NitOmnifuncAddFromFile(a:base, matches, 'modules.txt')
202                 endif
203
204                 " Classes (instanciable only)
205                 if prev_word == 'new'
206                         let handled = 1
207                         call NitOmnifuncAddFromFile(a:base, matches, 'constructors.txt')
208                 endif
209
210                 " Other class references
211                 if count(['class', 'super'], prev_word) > 0
212                         let handled = 1
213                         call NitOmnifuncAddFromFile(a:base, matches, 'classes.txt')
214                 endif
215
216                 " Types
217                 if count(['class', 'super', 'nullable', 'isa', 'as'], prev_word) > 0 ||
218                  \ line_prev_cursor =~ '[' || prev_char == ':' ||
219                  \ (line_prev_cursor =~ 'fun' && line_prev_cursor =~ 'import' && (prev_word == 'import' || prev_char == ','))
220                         let handled = 1
221                         call NitOmnifuncAddFromFile(a:base, matches, 'types.txt')
222                 endif
223
224                 " Properties
225                 if prev_char == '.' || line_prev_cursor =~ '['
226                         let handled = 1
227                         call NitOmnifuncAddFromFile(a:base, matches, 'properties.txt')
228                 endif
229
230                 " If is nothing else...
231                 if !handled
232                         " It may be a keyword
233                         if strlen(a:base) > 0
234                                 for keyword in ['module', 'import', 'class', 'abstract', 'interface',
235                                         \'universal', 'enum', 'end', 'fun', 'type', 'init', 'redef', 'is',
236                                         \'do', 'var', 'extern', 'public', 'protected', 'private', 'intrude',
237                                         \'if', 'then', 'else', 'while', 'loop', 'for', 'in', 'and', 'or',
238                                         \'not', 'implies', 'return', 'continue', 'break', 'abort', 'assert',
239                                         \'new', 'isa', 'once', 'super', 'self', 'true', 'false', 'null',
240                                         \'as', 'nullable', 'isset', 'label']
241
242                                         if keyword =~ '^' . a:base
243                                                 call add(matches, keyword)
244                                         endif
245                                 endfor
246                         endif
247
248                         " it may still be a method call or property access
249                         call NitOmnifuncAddFromFile(a:base, matches, 'properties.txt')
250                 endif
251
252                 return {'words': matches, 'refresh': 'always'}
253         endif
254 endfun
255
256 " Show doc for the entity under the cursor in the preview window
257 fun Nitdoc()
258         " Word under cursor
259         let word = expand("<cword>")
260
261         " All possible docs (there may be more than one entity with the same name)
262         let docs = []
263
264         " Search in all metadata files
265         for file in ['modules', 'classes', 'properties']
266                 let path = NitMetadataFile(file.'.txt')
267                 if empty(path)
268                         continue
269                 endif
270
271                 for line in readfile(path)
272                         let words = split(line, '#====#', 1)
273                         let name = get(words, 0, '')
274                         if name =~ '^' . word
275                                 " It fits our word, get long doc
276                                 let desc = get(words,3,'')
277                                 let desc = join(split(desc, '#nnnn#', 1), "\n")
278                                 call add(docs, desc)
279                         endif
280                 endfor
281         endfor
282
283         " Found no doc, give up
284         if empty(docs) || !(join(docs, '') =~ '\w')
285                 return
286         endif
287
288         " Open the preview window on a temp file
289         execute "silent pedit " . tempname()
290
291         " Change to preview window
292         wincmd P
293
294         " Show all found doc one after another
295         for doc in docs
296                 if doc =~ '\w'
297                         silent put = doc
298                         silent put = ''
299                 endif
300         endfor
301
302         " Set options
303         setlocal buftype=nofile
304         setlocal noswapfile
305         setlocal syntax=none
306         setlocal bufhidden=delete
307
308         " Change back to the source buffer
309         wincmd p
310         redraw!
311 endfun
312
313 " Call `git grep` on the word under the cursor
314 "
315 " Shows declarations first, then all matches, in the preview window.
316 fun NitGitGrep()
317         let word = expand("<cword>")
318         let out = tempname()
319         execute 'silent !(git grep "\\(module\\|class\\|universal\\|interface\\|var\\|fun\\) '.word.'";'.
320                 \'echo; git grep '.word.') > '.out
321
322         " Open the preview window on a temp file
323         execute "silent pedit " . out
324
325         " Change to preview window
326         wincmd P
327
328         " Set options
329         setlocal buftype=nofile
330         setlocal noswapfile
331         setlocal syntax=none
332         setlocal bufhidden=delete
333
334         " Change back to the source buffer
335         wincmd p
336         redraw!
337 endfun
338
339 " Activate the omnifunc on Nit files
340 autocmd FileType nit set omnifunc=NitOmnifunc
341
342 let s:script_dir = fnamemodify(resolve(expand('<sfile>:p')), ':h')