d1e92ef0853c7a221bd63f85197e0877f5ff7785
[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 " Activate the omnifunc on Nit files
257 autocmd FileType nit set omnifunc=NitOmnifunc
258
259 let s:script_dir = fnamemodify(resolve(expand('<sfile>:p')), ':h')