-
Notifications
You must be signed in to change notification settings - Fork 0
/
NiceMenu.vim
612 lines (495 loc) · 17.8 KB
/
NiceMenu.vim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
let g:loaded_nice_menu = 1
if exists('g:loaded_nice_menu')
finish
elseif v:version < 700
echoerr 'NiceMenu does not support this version of vim (' . v:version . ').'
finish
endif
let g:loaded_nice_menu = 1
" The delay from when typing stops to when
" a completions is should. Defaults to the vim
" timeout variable.
if ! exists( 'g:NiceMenuDelay' )
let g:NiceMenuDelay = &timeout
endif
" The minimum number of characters in word
" needed before a completions will be presented.
if ! exists( 'g:NiceMenuMin' )
let g:NiceMenuMin = 3
endif
if ! exists( 'g:NiceMenuDefaultCompl' )
let g:NiceMenuDefaultCompl = "\<C-N>"
endif
if ! exists( 'g:NiceMenuEnableOmni' )
let g:NiceMenuEnableOmni = 1
endif
if ! exists( 'g:NiceMenuEnableOmni' )
let g:NiceMenuIncludeDir = substitute(globpath(&rtp, 'nicemenu/'), "\n", ',', 'g')
endif
" only pop completion if one of these chars is to the
" left of the cursor.
" We should also have different classes of mappings and be able to chain them
" per file type. So a ftype can have classes A,B,C chained in that order. And
" perform a completion type performed by first matching class type and then
" fail back on a default(<C-N). We should have a global catch all chain to handle
" things like file path completions.
if ! exists( 'g:NiceMenuDefaultContextRegex' )
let g:NiceMenuDefaultContextRegex = '[a-zA-Z0-9_<>:\-\.\$\/]'
endif
au BufNewFile,BufReadPre * if !exists('b:NiceMenuContextRegex') | let b:NiceMenuContextRegex = g:NiceMenuDefaultContextRegex | endif
au BufNewFile,BufReadPre * if !exists('b:NiceMenuEnableOmni') | let b:NiceMenuEnableOmni = g:NiceMenuEnableOmni | endif
au BufNewFile,BufReadPre * let b:complPos = [0,0,0,0]
python << PEOF
import sys, os
sys.path.append( os.environ['HOME']+"/.vim" )
import nicemenu.NiceMenu as NiceMenu
PEOF
" specify the minimum 'word' length the must be present
" before we complete
"let s:minContextLen = 3
"
" Private Helper:
" getSynName:
" get the syntax type under the cursor
"{{{1
function! s:getSynName()
return synIDattr(synID(line("."), col("."), 0), "name" )
endfunction
"1}}}
" Private Helper:
" inString
"{{{1
function! s:inString()
if s:getSynName() =~? 'String'
return 1
endif
return 0
endfunction
"1}}}
"
" Private Helper:
" s:filePathIsValid
"{{{1
function! s:filePathIsValid( fpath )
return filereadable( a:fpath ) || isdirectory( a:fpath )
endfunction
" Private Helper:
" s:findFilePath
"{{{1
" THIS DOEN'T WORK
"function! s:findFilePath(cur_text)
function! FindFilePath()
let l:cline = getline( '.' )
" Look for a leading '/', './' or '~' characters.
let l:fchar = stridx( l:cline, '~' )
if -1 == l:fchar
let l:cpos = getpos( '.' )[2] - 1
let l:fpath = strpart( l:cline, l:fchar, l:cpos )
let l:fpath = substitute( l:fpath, '\~', expand("$HOME"), "" )
" Now we have the full path string, which may be partial. So find
" a valid substring if necessary.
if s:filePathIsValid( l:fpath )
"echo l:fpath
return 1
endif
endif
let l:fchar = stridx( l:cline, './' )
if -1 == l:fchar
let l:cpos = getpos( '.' )[2] - 1
let l:fpath = strpart( l:cline, l:fchar, l:cpos )
if s:filePathIsValid( l:fpath )
"echo l:fpath
return 1
endif
endif
let l:fchar = stridx( l:cline, '/' )
if -1 == l:fchar
let l:cpos = getpos( '.' )[2] - 1
let l:fpath = strpart( l:cline, l:fchar, l:cpos )
if s:filePathIsValid( l:fpath )
"echo l:fpath
return 1
endif
endif
return 0
endfunction
"let b:completionList = []
"let b:completionPos = -1
function! NiceMenuCompletefunc( startpos, base )
"echo "NiceMenuCompletefunc"
"sleep 1
if empty(b:completionList) || -1 == b:completionPos
return -1
endif
if 1 == a:startpos
return b:completionPos
endif
if 0 == a:startpos
"call complete( a:startpos, b:completionList )
return b:completionList
endif
return -1
endfunction
function! s:canComplete()
if (! exists('&omnifunc')) || (&omnifunc == '') || s:inString()
"echo "canComplete no omni"
"sleep 1
return ""
endif
if exists('&omnifunc') && &omnifunc != '' && (! s:inString())
"echo "canComplete checking omni"
"sleep 1
"echo "NiceMenu_is_file_path() ".NiceMenu_is_file_path(l:cword)
"return ""
"if NiceMenu_is_file_path(l:cword)
if FindFilePath()
return "\<C-X>\<C-F>"
else
"let l:cword = s:getCurrentWord()
"if l:cword =~ '\k$' || l:cword =~ '\k->$' || l:cword =~ '\k\.$'
" Test the complete function before setting it.
let b:completionPos = -1
let l:compl_res = call( &omnifunc, [1,''] )
if -1 != l:compl_res
comp
let l:cpos = getpos( '.' )
let l:compl_list = call( &omnifunc, [0,s:getOmniWord(l:compl_res)] )
" The second call to omnifunc can change our pos even though
" it doesn't have any work to do, so set it back.
call setpos( '.', l:cpos )
if ! empty(l:compl_list)
set completefunc=NiceMenuCompletefunc
let b:completionList = l:compl_list
let b:completionPos = l:compl_res
"echo "canComplete found " len( b:completionList ) " omni items \<C-X>\<C-U>"
"sleep 1
return "\<C-X>\<C-U>"
endif
endif
endif
endif
"echo "canComplete found nothing"
"sleep 1
"let b:completionList = []
return ""
endfunction
function! s:getWordMin()
if exists( 'b:NiceMenuMin' )
return b:NiceMenuMin
endif
return g:NiceMenuMin
endfunction
function! s:getDefaultCompl()
if exists( 'b:NiceMenuDefaultCompl' )
return b:NiceMenuDefaultCompl
endif
return g:NiceMenuDefaultCompl
endfunction
function! NiceMenuGetDelay()
if exists( 'b:NiceMenuDelay' )
return b:NiceMenuDelay
endif
return g:NiceMenuDelay
endfunction
function s:getCurrentChar()
return strpart( getline('.'), col('.')-2, 1)
endfunction
function s:getNextChar()
return strpart( getline('.'), col('.')-1, 1)
endfunction
function s:getOmniWord( spoint )
"return strpart( getline('.'), a:spoint, col('.')-1)
return strpart( getline('.'), a:spoint, col('.'))
endfunction
function s:getCurrentWord()
"return matchstr(s:getCurrentText(), '\k*$')
let l:wlist = split( strpart(getline('.'), 0, col('.')), '\s' )
if empty( l:wlist )
return ""
endif
return l:wlist[ -1 ]
endfunction
function s:getCurrentText()
return strpart(getline('.'), 0, col('.') - 1)
endfunction
" Private Helper:
" getSynName:
" get the syntax type under the cursor
"{{{1
"function! s:getSynName()
"return synIDattr(synID(line("."), col("."), 0), "name" )
"endfunction
"1}}}
" Private Helper:
" inString
" Param: thechar the quote character that's been double tapped.
" See if the cursor is inside a string according the current syntax definition
"{{{1
function! s:inString()
" This will often contain whether we are in a single or double quote
" string. How that is represented seems syntax specific, not standard.
" We still leverage that knowledge if we can.
return synIDattr(synID(line("."), col("."), 0), "name" ) =~? 'string'
endfunction
"1}}}
"word the text that will be inserted, mandatory
"abbr abbreviation of "word"; when not empty it is used in
"the menu instead of "word"
"menu extra text for the popup menu, displayed after "word"
"or "abbr"
"info more information about the item, can be displayed in a
"preview window
"kind single letter indicating the type of completion
"icase when non-zero case is to be ignored when comparing
"items to be equal; when omitted zero is used, thus
"items that only differ in case are added
"dup when non-zero this match will be added even when an
"item with the same word is already present.
"TODO: make the number of spell returns configurable.
function! s:checkSpell( word )
let l:spug = spellsuggest( a:word, 50 )
let l:clist = []
if ! empty( l:spug )
for item in l:spug
let l:clist += [{'word':item, 'menu':' | sp', 'dup':0}]
endfor
endif
return l:clist
endfunction
function! NiceMenuCheckContext()
if pumvisible() || &paste || ('i' != mode())
"echo "NiceMenuCheckContext bad mode"
return 0
endif
" Make sure the pos is the same as when this
" was started.
let l:npos = getpos(".")
if l:npos[1] != b:complPos[1] || l:npos[2] != b:complPos[2] || l:npos[3] != b:complPos[3]
"echo "NiceMenuCheckContext bad pos " l:npos ":" b:complPos
return 0
endif
" Sure the next character is whitespace.
" This is to help prevent doing a completion
" in the middle of a word.
" TODO: Needs to be a config param.
let l:nextChar = s:getNextChar()
if (strlen(l:nextChar) > 0) && l:nextChar !~ '\s' && l:nextChar !~ '\W'
"echo "NiceMenuCheckContext bad next char: '" s:getNextChar() "'"
return 0
endif
let curChar = s:getCurrentChar()
"echo "checking: " curChar
if curChar !~ b:NiceMenuContextRegex
return 0
endif
"TODO: make optional
"If we're inside a string, don't try to complete
if s:inString()
return 0
endif
"echo "NiceMenuCheckContext inContext 0"
return 1
endfunction
function! NiceMenuComplCleanup()
if 'i' == mode() && pumvisible()
return "\<C-P>"
endif
return ""
endfunction
python << PEOF
def NiceMenuAsyncCPL():
if not NiceMenuCheckContext():
#nmq_msgs.put_nowait( {'level':'info', 'msg':"NiceMenuAsyncCpl() bad context" )
return False
compl = ""
cancompl = ""
if vim.eval('b:NiceMenuEnableOmni'):
cancompl = canComplete()
endif
if len( cancompl ):
compl = cancompl
#nmq_msgs.put_nowait( {'level':'info', 'msg':"got omni string %s" % compl)
else:
compl = getDefaultCompl()
# Select first(original typed text) option without inserting it's text
# TODO: This should be a configurable option.
compl += "\<C-P>"
#let b:NiceMenu_has_shown = 1
# We need to track this by buffer outside of the vim buffer context.
# We'll need to track our own buffer vars
#vim.eval( 'b:NiceMenu_has_shown = 1' )
NiceMenu_has_shown = 1
#echo "Completion string " l:compl
#sleep 1
return compl
#NiceMenuAsyncCPL()
PEOF
function! NiceMenuAsyncCpl()
"echo "NiceMenuAsyncCpl()"
if 0 == NiceMenuCheckContext()
"echo "NiceMenuAsyncCpl() bad context"
return ""
endif
let l:compl = ""
let l:cancompl = ""
if b:NiceMenuEnableOmni
let l:cancompl = s:canComplete()
endif
if len( l:cancompl )
let l:compl = l:cancompl
"echo "got omni string " l:compl
"sleep 1
else
let l:compl = s:getDefaultCompl()
endif
" Select first(original typed text) option without inserting it's text
" TODO: This should be a configurable option.
let l:compl .= "\<C-P>"
let b:NiceMenu_has_shown = 1
"echo "Completion string " l:compl
"sleep 1
return l:compl
endfunction
python << PEOF
# Set up globals and define some def
import vim,threading,subprocess
#ptimer = None
#def NiceMenuShowMenu():
# return
#
# #print 'NiceMenuShowMenu'
#
#
# if 'i' != vim.eval('mode()'):
# #print 'NiceMenuShowMenu bad context'
# return
#
# sname = vim.eval( 'v:servername' )
# if not sname or sname == "":
# print 'NiceMenuShowMenu bad sname'
# return
#
# try:
# #subprocess.call( ["gvim", "--servername", "%s"%sname, "--remote-send", '<C-R>=NiceMenuAsyncCpl()<CR>'] )
# #nmq_key_trigger.put_nowait( "key trigger: " )
#
# except:
# print 'NiceMenuShowMenu except'
# pass
#
PEOF
function! NiceMenuCancel()
"echo "NiceMenuCancel"
python << PEOF
NiceMenu.sendCmd( NiceMenu.NMCMD_LEFT_INSERT )
#global ptimer
#if ptimer:
#ptimer.cancel()
PEOF
endfunction
"NiceMenuCompl: {{{1
"fun! s:NiceMenuCompl()
fun! s:NiceMenuCompl( need_i )
"echo "s:NiceMenuCompl " s:getCurrentChar()
"XXX We need to load this plugin after v:servername has been set!!
if v:servername == ''
echoerr 'No servername found, cannot load NiceMenu'
au! nicemenuau
"autocmd! CursorMovedI * call s:NiceMenuCompl(1)
"autocmd! InsertEnter * call s:NiceMenuCompl(0)
"autocmd! InsertLeave * call NiceMenuCancel()
"autocmd! VimLeave * call NiceMenuShutdown()
return
endif
" If the pop up menu is already showing or
" paste is on or
" we're not in insert mode when we should be
" The last funny business with need_i alows us to use the
" menu when we enter insert mode as a trigger. But when we
" enter insert mode, mode() says we're not in insert mode. So
" we use need_i as an indicator to tell we need be in insert mode
" or we think we're going into insert mode.
if pumvisible() || &paste || (('i' != mode()) && a:need_i )
return ""
endif
if exists( 'b:NiceMenu_has_shown' ) && 1 == b:NiceMenu_has_shown
let b:NiceMenu_has_shown = 0
return ""
endif
" Send the key event to the cmd q
python << PEOF
NiceMenu.sendCmd( NiceMenu.NMCMD_KEY_PRESS, {
'line':vim.eval("getline('.')"),
'pos':vim.eval( "getpos('.')" )
})
PEOF
return ""
" If we're in the same spot as the last trigger, don't show the menu
" again.
" XXX this should be handled in the python thread
"let l:npos = getpos(".")
"if l:npos[1] == b:complPos[1] && l:npos[2] == b:complPos[2] && l:npos[3] == b:complPos[3]
""echo "NiceMenuCheckContext bad pos " l:npos ":" b:complPos
"return ""
"endif
" XXX this should be handled in the python thread
" Only if current word/text is of a min length
"let l:cline = s:getCurrentText()
"let l:cword = s:getCurrentWord()
"if strlen(l:cword) < s:getWordMin()
""echo "s:NiceMenuCompl word to short"
"return ""
"endif
" XXX this should be handled in the python thread
" If it's just a number, don't
" TODO: make optional
"if l:cword =~ '^\d\+$'
"return ""
"endif
" XXX this should be handled in the python thread
"TODO: make optional
"If we're inside a string, don't try to complete
"if s:inString()
"return 0
"endif
" XXX this should be handled in the python thread
"let b:complPos = l:npos
"call NiceMenuCancel()
python << PEOF
#global ptimer
#delay = vim.eval("NiceMenuGetDelay()")
#if not delay:
# delay = '.8'
#ptimer = threading.Timer( float(delay), NiceMenuShowMenu )
#ptimer.start()
PEOF
return ""
endfun
function NiceMenu_enable()
"call s:mapForMappingDriven()
augroup nicemenuau
au!
autocmd CursorMovedI * call s:NiceMenuCompl(1)
autocmd InsertEnter * call s:NiceMenuCompl(0)
autocmd InsertLeave * call NiceMenuCancel()
autocmd VimLeave * call NiceMenuShutdown()
augroup END
endfunction
function NiceMenuWrapper()
return "\<C-X>\<C-U>"
endfunction
function NiceMenuShutdown()
python << PEOF
NiceMenu.NiceMenuShutdown()
PEOF
endfunction
" This mapping is a work around for a race condition in
" NiceMenu. If you hit <esc> while a completion is building
" its menu list when the list is done being built the completion
" menu will try to show itself and drop the user back into Insert mode.
" But, if there is another <ESC> loaded into the typeahead buffer it will
" cancel out menu put the user back into Normal mode like
" they want to be. This all happens very quickly and to the user it looks
" typing ESC to get into normal mode works just like it should.
imap <silent> <ESC> <ESC><ESC>
call NiceMenu_enable()