misc/vim: use the metadata files for a better autocompletion
[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 " Internal function to search for lines in `path` corresponding to the partial
85 " word `base`. Adds found and formated match to `matches`.
86 "
87 " Will order the results in 3 levels:
88 " 1. Exact matches
89 " 2. Common prefix matches
90 " 3. Substring matches
91 fun NitOmnifuncAddFromFile(base, matches, path)
92         let prefix_matches = []
93         let substring_matches = []
94
95         " Where are the generated metadata files?
96         if empty($NIT_VIM_DIR)
97                 let metadata_dir = $HOME . '/.vim/nit'
98         else
99                 let metadata_dir = $NIT_VIM_DIR
100         end
101
102         let path = metadata_dir . '/' . a:path
103         " Is there generated custom metadata files?
104         if ! filereadable(path)
105                 let path = s:script_dir . '/' . a:path
106
107                 " Is there standard metadata files?
108                 if ! filereadable(path)
109                         return
110                 endif
111         endif
112
113         for line in readfile(path)
114                 let words = split(line, '#====#', 1)
115                 let name = get(words, 0, '')
116
117                 " Add?
118                 if name == a:base
119                         " Exact match
120                         call NitOmnifuncAddAMatch(a:matches, words, name)
121                 elseif name =~ '^'.a:base
122                         " Common-prefix match
123                         call NitOmnifuncAddAMatch(prefix_matches, words, name)
124                 elseif name =~ a:base
125                         " Substring match
126                         call NitOmnifuncAddAMatch(substring_matches, words, name)
127                 endif
128         endfor
129
130         " Assemble the final match list
131         call extend(a:matches, sort(prefix_matches))
132         call extend(a:matches, sort(substring_matches))
133 endfun
134
135 " Internal function to search parse the information from a metadata line
136 fun NitOmnifuncAddAMatch(matches, words, name)
137         let pretty = get(a:words, 1, '')
138         let synopsis = get(a:words, 2, '')
139         let desc = get(a:words, 3, '')
140         let desc = join(split(desc, '#nnnn#', 1), "\n")
141         if strlen(pretty) > 40
142                 let pretty = pretty[:37] . '...'
143         endif
144         call add(a:matches, {'word': a:name, 'abbr': pretty, 'menu': synopsis, 'info': desc, 'dup': 1})
145 endfun
146
147 " Omnifunc using metadata files generated by nitpick to offer better
148 " contextual autocomplete for Nit source code.
149 fun NitOmnifunc(findstart, base)
150         if a:findstart
151                 " locate the start of the word
152                 let line = getline('.')
153                 let start = col('.') - 1
154                 while start > 0 && line[start - 1] =~ '\w'
155                         let start -= 1
156                 endwhile
157                 return start
158         else
159                 " find keyword matching with "a:base"
160                 let matches = []
161
162                 " Advanced suggestions
163                 let cursor_line = getline('.')
164
165                 " Content of the line before the partial word
166                 let line_prev_cursor = cursor_line[:col('.')-1]
167
168                 let prev_char_at = strlen(line_prev_cursor) - 1
169                 while prev_char_at > 0 && line_prev_cursor[prev_char_at] =~ '\s'
170                         let prev_char_at -= 1
171                 endwhile
172
173                 " Non whitespace char just before the partial word
174                 let prev_char = line_prev_cursor[prev_char_at]
175
176                 " Nity words on the current line before the partial word
177                 let prev_words = split(line_prev_cursor, '\W\+')
178
179                 " The word right before the partial word
180                 let prev_word = get(prev_words, -1, '')
181
182                 " Have we found a promising heuristic yet?
183                 let handled = 0
184
185                 " Modules
186                 if prev_word == 'import' && ! (line_prev_cursor =~ 'fun')
187                         let handled = 1
188                         call NitOmnifuncAddFromFile(a:base, matches, 'modules.txt')
189                 endif
190
191                 " Classes
192                 if count(['new', 'super', 'class', 'nullable'], prev_word) > 0 ||
193                  \ line_prev_cursor =~ '[' || prev_char == ':' ||
194                  \ (line_prev_cursor =~ 'fun' && line_prev_cursor =~ 'import' && (prev_word == 'import' || prev_char == ','))
195                         let handled = 1
196                         call NitOmnifuncAddFromFile(a:base, matches, 'classes.txt')
197                 endif
198
199                 " Properties
200                 if prev_char == '.' || line_prev_cursor =~ '['
201                         let handled = 1
202                         call NitOmnifuncAddFromFile(a:base, matches, 'properties.txt')
203                 endif
204
205                 " If is nothing else...
206                 if !handled
207                         " It may be a keyword
208                         if strlen(a:base) > 0
209                                 for keyword in ['module', 'import', 'class', 'abstract', 'interface',
210                                         \'universal', 'enum', 'end', 'fun', 'type', 'init', 'redef', 'is',
211                                         \'do', 'var', 'extern', 'public', 'protected', 'private', 'intrude',
212                                         \'if', 'then', 'else', 'while', 'loop', 'for', 'in', 'and', 'or',
213                                         \'not', 'implies', 'return', 'continue', 'break', 'abort', 'assert',
214                                         \'new', 'isa', 'once', 'super', 'self', 'true', 'false', 'null',
215                                         \'as', 'nullable', 'isset', 'label']
216
217                                         if keyword =~ '^' . a:base
218                                                 call add(matches, keyword)
219                                         endif
220                                 endfor
221                         endif
222
223                         " it may still be a method call or property access
224                         call NitOmnifuncAddFromFile(a:base, matches, 'properties.txt')
225                 endif
226
227                 return {'words': matches, 'refresh': 'always'}
228         endif
229 endfun
230
231 " Activate the omnifunc on Nit files
232 autocmd FileType nit set omnifunc=NitOmnifunc
233
234 let s:script_dir = fnamemodify(resolve(expand('<sfile>:p')), ':h')