├── README.md ├── autoload ├── pseudocl.vim └── pseudocl │ ├── complete.vim │ └── render.vim └── test ├── _clear.vader ├── _restore.vader ├── basic.vader ├── callbacks.vader ├── cmap.vader ├── completion.vader └── history.vader /README.md: -------------------------------------------------------------------------------- 1 | vim-pseudocl: Pseudo-command-line interface 2 | =========================================== 3 | 4 | vim-pseudocl implements a command-line interface that mimics the native Vim 5 | command-line. It enables writing advanced command-line features with its 6 | callback functions. 7 | 8 | Usage 9 | ----- 10 | 11 | `pseudocl#start` function will start the pseudo-command-line. It returns 12 | user-typed string or throws 'exit' on escape key or on interrupt. 13 | 14 | ```vim 15 | try 16 | let got = pseudocl#start(options) 17 | catch 'exit' 18 | call pseudocl#render#clear() 19 | echon 'No input' 20 | endtry 21 | ``` 22 | 23 | The following table summarizes the dictionary parameter to `pseudocl#start` 24 | function. 25 | 26 | | Option name | Type | Default | Description | 27 | | -------------- | ------ | ---------------------- | ----------------------------------------------------------- | 28 | | prompt | String | `':'` | Command-line prompt in string | 29 | | prompt | List | `['None', ':']` | Sequence of highlight group and string pairs | 30 | | highlight | String | `None` | Highlight group for command-line | 31 | | input | String | `''` | Initial input string | 32 | | on_change | Funcref | `pseudocl#nop` | Callback function invoked on change (new, old, cursor) | 33 | | on_unknown_key | Funcref | `pseudocl#nop` | Callback function invoked on unknown key (key, str, cursor) | 34 | | renderer | Funcref | `pseudocl#render#echo` | Command-line renderer (prompt, line, cursor) | 35 | | remap | Dict | `{}` | Remap keys | 36 | | map | Boolean | `1` | To use command-line maps or not | 37 | | history | List | `[]` | Command-line history | 38 | | words | List | `[]` | Words for tab completion | 39 | 40 | API 41 | --- 42 | 43 | ### Main 44 | 45 | - `pseudocl#start()` 46 | 47 | ### Render 48 | 49 | - `pseudocl#render#echo()` 50 | - `pseudocl#render#echo_prompt()` 51 | - `pseudocl#render#echo_line()` 52 | - `pseudocl#render#clear()` 53 | 54 | ### Complete 55 | 56 | - `pseudocl#complete#extract_words()` 57 | 58 | License 59 | ------- 60 | 61 | MIT 62 | 63 | -------------------------------------------------------------------------------- /autoload/pseudocl.vim: -------------------------------------------------------------------------------- 1 | " Copyright (c) 2014 Junegunn Choi 2 | " 3 | " MIT License 4 | " 5 | " Permission is hereby granted, free of charge, to any person obtaining 6 | " a copy of this software and associated documentation files (the 7 | " "Software"), to deal in the Software without restriction, including 8 | " without limitation the rights to use, copy, modify, merge, publish, 9 | " distribute, sublicense, and/or sell copies of the Software, and to 10 | " permit persons to whom the Software is furnished to do so, subject to 11 | " the following conditions: 12 | " 13 | " The above copyright notice and this permission notice shall be 14 | " included in all copies or substantial portions of the Software. 15 | " 16 | " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | " EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | " MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | " NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | " LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | let s:cpo_save = &cpo 25 | set cpo&vim 26 | 27 | let g:pseudocl#MAP = -1 28 | let g:pseudocl#RETURN = -2 29 | let g:pseudocl#CONTINUE = -3 30 | let g:pseudocl#UNKNOWN = -4 31 | let g:pseudocl#EXIT = -5 32 | let g:pseudocl#CTRL_F = -6 33 | 34 | function! pseudocl#nop(...) 35 | return extend([g:pseudocl#CONTINUE], a:000[1:]) 36 | endfunction 37 | 38 | let s:default_opts = { 39 | \ 'prompt': ':', 40 | \ 'history': [], 41 | \ 'words': [], 42 | \ 'input': '', 43 | \ 'highlight': 'None', 44 | \ 'remap': {}, 45 | \ 'map': 1, 46 | \ 'renderer': function('pseudocl#render#echo'), 47 | \ 'on_change': function('pseudocl#nop'), 48 | \ 'on_event': function('pseudocl#nop'), 49 | \ 'on_unknown_key': function('pseudocl#nop') 50 | \ } 51 | 52 | function! pseudocl#start(opts) range 53 | let s:current = extend(copy(s:default_opts), a:opts) 54 | let s:current.cursor = len(s:current.input) 55 | return pseudocl#render#loop(s:current) 56 | endfunction 57 | 58 | function! pseudocl#get_prompt() 59 | return s:current.prompt 60 | endfunction 61 | 62 | function! pseudocl#set_prompt(new_prompt) 63 | let s:current.prompt = a:new_prompt 64 | endfunction 65 | 66 | if !s:default_opts.renderer 67 | function! pseudocl#default_renderer(...) 68 | return call('pseudocl#render#echo', a:000) 69 | endfunction 70 | 71 | let s:default_opts.renderer = function('pseudocl#default_renderer') 72 | endif 73 | 74 | let &cpo = s:cpo_save 75 | unlet s:cpo_save 76 | -------------------------------------------------------------------------------- /autoload/pseudocl/complete.vim: -------------------------------------------------------------------------------- 1 | " Copyright (c) 2014 Junegunn Choi 2 | " 3 | " MIT License 4 | " 5 | " Permission is hereby granted, free of charge, to any person obtaining 6 | " a copy of this software and associated documentation files (the 7 | " "Software"), to deal in the Software without restriction, including 8 | " without limitation the rights to use, copy, modify, merge, publish, 9 | " distribute, sublicense, and/or sell copies of the Software, and to 10 | " permit persons to whom the Software is furnished to do so, subject to 11 | " the following conditions: 12 | " 13 | " The above copyright notice and this permission notice shall be 14 | " included in all copies or substantial portions of the Software. 15 | " 16 | " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | " EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | " MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | " NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | " LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | let s:cpo_save = &cpo 25 | set cpo&vim 26 | 27 | function! pseudocl#complete#extract_words(content) 28 | let dict = {} 29 | for word in split(a:content, '\W\+') 30 | let dict[word] = 1 31 | endfor 32 | return sort(keys(dict)) 33 | endfunction 34 | 35 | function! pseudocl#complete#match(prefix, words) 36 | let started = 0 37 | let matches = [a:prefix] 38 | let tokens = matchlist(a:prefix, '^\(.\{-}\)\(\w*\)$') 39 | let [prefix, suffix] = tokens[1 : 2] 40 | for word in a:words 41 | if stridx(word, suffix) == 0 && len(word) > len(suffix) 42 | call add(matches, prefix . word) 43 | let started = 1 44 | elseif started 45 | break 46 | end 47 | endfor 48 | return matches 49 | endfunction 50 | 51 | let &cpo = s:cpo_save 52 | unlet s:cpo_save 53 | -------------------------------------------------------------------------------- /autoload/pseudocl/render.vim: -------------------------------------------------------------------------------- 1 | " Copyright (c) 2014 Junegunn Choi 2 | " 3 | " MIT License 4 | " 5 | " Permission is hereby granted, free of charge, to any person obtaining 6 | " a copy of this software and associated documentation files (the 7 | " "Software"), to deal in the Software without restriction, including 8 | " without limitation the rights to use, copy, modify, merge, publish, 9 | " distribute, sublicense, and/or sell copies of the Software, and to 10 | " permit persons to whom the Software is furnished to do so, subject to 11 | " the following conditions: 12 | " 13 | " The above copyright notice and this permission notice shall be 14 | " included in all copies or substantial portions of the Software. 15 | " 16 | " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | " EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | " MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | " NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | " LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | " OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | " WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | let s:cpo_save = &cpo 25 | set cpo&vim 26 | 27 | if exists("*strwidth") 28 | function! s:strwidth(str) 29 | return strwidth(a:str) 30 | endfunction 31 | else 32 | function! s:strwidth(str) 33 | return len(split(a:str, '\zs')) 34 | endfunction 35 | endif 36 | 37 | function! pseudocl#render#loop(opts) 38 | let s:highlight = a:opts.highlight 39 | let s:yanked = '' 40 | let s:noremap = 0 41 | let xyz = [&ruler, &showcmd, &mouse] 42 | try 43 | set noruler noshowcmd mouse= 44 | call s:hide_cursor() 45 | let shortmess = &shortmess 46 | set shortmess+=T 47 | 48 | let old = a:opts.input 49 | let history = copy(a:opts.history) 50 | let s:matches = [] 51 | let s:history_idx = len(history) 52 | let s:keystrokes = [] 53 | let s:prev_time = 0 54 | let s:remap = a:opts.remap 55 | let s:use_maps = a:opts.map 56 | call add(history, old) 57 | 58 | let valid_codes = [g:pseudocl#CONTINUE, g:pseudocl#RETURN, g:pseudocl#EXIT, g:pseudocl#MAP] 59 | let valid_events = [g:pseudocl#CTRL_F] 60 | while 1 61 | call a:opts.renderer(pseudocl#get_prompt(), old, a:opts.cursor) 62 | let [code, new, a:opts.cursor] = 63 | \ s:process_char(old, a:opts.cursor, a:opts.words, history) 64 | 65 | " Event 66 | if index(valid_events, code) >= 0 67 | let [code, new, a:opts.cursor] = a:opts.on_event(code, new, a:opts.cursor) 68 | endif 69 | 70 | while index(valid_codes, code) < 0 71 | let [code, new, a:opts.cursor] = a:opts.on_unknown_key(code, new, a:opts.cursor) 72 | endwhile 73 | 74 | if code == g:pseudocl#MAP 75 | continue 76 | elseif code == g:pseudocl#CONTINUE 77 | if new != old 78 | call a:opts.on_change(new, old, a:opts.cursor) 79 | endif 80 | let old = new 81 | continue 82 | elseif code == g:pseudocl#RETURN 83 | call a:opts.renderer(pseudocl#get_prompt(), new, -1) 84 | return new 85 | elseif code == g:pseudocl#EXIT 86 | call a:opts.renderer(pseudocl#get_prompt(), new, -1) 87 | throw 'exit' 88 | endif 89 | endwhile 90 | finally 91 | call s:show_cursor() 92 | let &shortmess = shortmess 93 | let [&ruler, &showcmd, &mouse] = xyz 94 | endtry 95 | endfunction 96 | 97 | function! pseudocl#render#echo(prompt, line, cursor) 98 | if exists('s:force_echo') || getchar(1) == 0 99 | call pseudocl#render#clear() 100 | let plen = pseudocl#render#echo_prompt(a:prompt) 101 | call pseudocl#render#echo_line(a:line, a:cursor, plen) 102 | unlet! s:force_echo 103 | endif 104 | endfunction 105 | 106 | function! pseudocl#render#clear() 107 | echon "\r\r" 108 | echon '' 109 | endfunction 110 | 111 | function! s:strtrans(str) 112 | return substitute(a:str, "\n", '^M', 'g') 113 | endfunction 114 | 115 | function! s:trim(str, margin, left) 116 | let str = a:str 117 | let mod = 0 118 | let ww = winwidth(winnr()) - a:margin - 2 119 | let sw = s:strwidth(str) 120 | let pat = a:left ? '^.' : '.$' 121 | while sw >= ww 122 | let sw -= s:strwidth(matchstr(str, pat)) 123 | let str = substitute(str, pat, '', '') 124 | let mod = 1 125 | endwhile 126 | if mod 127 | let str = substitute(str, a:left ? '^..' : '..$', '', '') 128 | endif 129 | return [str, mod ? '..' : '', sw] 130 | endfunction 131 | 132 | function! s:trim_left(str, margin) 133 | return s:trim(a:str, a:margin, 1) 134 | endfunction 135 | 136 | function! s:trim_right(str, margin) 137 | return s:trim(a:str, a:margin, 0) 138 | endfunction 139 | 140 | function! pseudocl#render#echo_line(str, cursor, prompt_width) 141 | hi PseudoCLCursor term=inverse cterm=inverse gui=inverse 142 | try 143 | if a:cursor < 0 144 | let [str, ellipsis, _] = s:trim_left(s:strtrans(a:str), a:prompt_width + 2) 145 | if !empty(ellipsis) 146 | echohl NonText 147 | echon ellipsis 148 | endif 149 | 150 | execute 'echohl '.s:highlight 151 | echon str 152 | elseif a:cursor == len(a:str) 153 | let [str, ellipsis, _] = s:trim_left(s:strtrans(a:str), a:prompt_width + 2) 154 | if !empty(ellipsis) 155 | echohl NonText 156 | echon ellipsis 157 | endif 158 | 159 | execute 'echohl '.s:highlight 160 | echon str 161 | 162 | echohl PseudoCLCursor 163 | echon ' ' 164 | else 165 | let prefix = s:strtrans(strpart(a:str, 0, a:cursor)) 166 | let m = matchlist(strpart(a:str, a:cursor), '^\(.\)\(.*\)') 167 | let cursor = s:strtrans(m[1]) 168 | let suffix = s:strtrans(m[2]) 169 | 170 | let [prefix, ellipsis, pwidth] = s:trim_left(prefix, a:prompt_width + 1 + 2) 171 | 172 | if !empty(ellipsis) 173 | echohl NonText 174 | echon ellipsis 175 | endif 176 | 177 | execute 'echohl '.s:highlight 178 | echon prefix 179 | 180 | echohl PseudoCLCursor 181 | echon cursor 182 | 183 | let [suffix, ellipsis, _] = s:trim_right(suffix, a:prompt_width + pwidth - 1 + 2) 184 | execute 'echohl '.s:highlight 185 | echon suffix 186 | if !empty(ellipsis) 187 | echohl NonText 188 | echon ellipsis 189 | endif 190 | endif 191 | finally 192 | echohl None 193 | endtry 194 | endfunction 195 | 196 | function! s:hide_cursor() 197 | if !exists('s:t_ve') 198 | let s:t_ve = &t_ve 199 | set t_ve= 200 | endif 201 | 202 | if hlID('Cursor') != 0 203 | redir => hi_cursor 204 | silent 0verb hi Cursor 205 | redir END 206 | let link = matchstr(hi_cursor, 'links to \zs.*') 207 | let s:hi_cursor = empty(link) ? 208 | \ ('highlight ' . substitute(hi_cursor, 'xxx\|\n', '', 'g')) 209 | \ : ('highlight link Cursor '.link) 210 | hi Cursor guibg=bg 211 | endif 212 | endfunction 213 | 214 | function! s:show_cursor() 215 | if exists('s:t_ve') 216 | let &t_ve = s:t_ve 217 | unlet s:t_ve 218 | endif 219 | 220 | if exists('s:hi_cursor') 221 | hi clear Cursor 222 | if s:hi_cursor !~ 'cleared' 223 | execute s:hi_cursor 224 | endif 225 | endif 226 | endfunction 227 | 228 | function! pseudocl#render#echo_prompt(prompt) 229 | let type = type(a:prompt) 230 | if type == 1 231 | let len = s:prompt_in_str(a:prompt) 232 | elseif type == 3 233 | let len = s:prompt_in_list(a:prompt) 234 | else 235 | echoerr "Invalid type" 236 | endif 237 | return len 238 | endfunction 239 | 240 | function! s:prompt_in_str(str) 241 | execute 'echohl '.s:highlight 242 | echon a:str 243 | echohl None 244 | return s:strwidth(a:str) 245 | endfunction 246 | 247 | function! s:prompt_in_list(list) 248 | let list = copy(a:list) 249 | let len = 0 250 | while !empty(list) 251 | let hl = remove(list, 0) 252 | let str = remove(list, 0) 253 | execute 'echohl ' . hl 254 | echon str 255 | let len += s:strwidth(str) 256 | endwhile 257 | echohl None 258 | return len 259 | endfunction 260 | 261 | function! s:input(prompt, default) 262 | try 263 | call s:show_cursor() 264 | redraw 265 | return input(a:prompt, a:default) 266 | finally 267 | call s:hide_cursor() 268 | redraw 269 | endtry 270 | endfunction 271 | 272 | function! s:evaluate_keyseq(seq) 273 | return substitute(a:seq, '<[^> ]\+>', '\=eval("\"\\".submatch(0)."\"")', 'g') 274 | endfunction 275 | 276 | function! s:evaluate_keymap(arg) 277 | if a:arg.expr 278 | return eval(s:evaluate_keyseq(substitute(a:arg.rhs, '\c', ''.a:arg.sid.'_', ''))) 279 | else 280 | return s:evaluate_keyseq(a:arg.rhs) 281 | endif 282 | endfunction 283 | 284 | function! s:process_char(str, cursor, words, history) 285 | try 286 | let c = s:getchar() 287 | catch /^Vim:Interrupt$/ 288 | let c = "\" 289 | endtry 290 | if c == g:pseudocl#MAP 291 | return [g:pseudocl#MAP, a:str, a:cursor] 292 | else 293 | return s:decode_char(c, a:str, a:cursor, a:words, a:history) 294 | endif 295 | endfunction 296 | 297 | function! s:timed_getchar(time) 298 | if a:time == 0 299 | let c = getchar() 300 | let ch = nr2char(c) 301 | return empty(ch) ? c : ch 302 | else 303 | for _ in range(0, a:time, 10) 304 | let c = getchar(0) 305 | if !empty(c) 306 | let ch = nr2char(c) 307 | return empty(ch) ? c : ch 308 | endif 309 | sleep 10m 310 | endfor 311 | return '' 312 | endif 313 | endfunction 314 | 315 | function! s:gettime() 316 | let [s, us] = reltime() 317 | return s * 1000 + us / 1000 318 | endfunction 319 | 320 | function! s:cancel_map() 321 | let first = remove(s:keystrokes, 0) 322 | call feedkeys(substitute(join(s:keystrokes, ''), "\n", "\r", 'g')) 323 | let s:prev_time = 0 324 | let s:keystrokes = [] 325 | let s:force_echo = 1 326 | return first 327 | endfunction 328 | 329 | function! s:for_args(func, first, arglists) 330 | for arglist in a:arglists 331 | let ret = call(a:func, insert(arglist, a:first, 0)) 332 | if !empty(ret) 333 | break 334 | endif 335 | endfor 336 | return ret 337 | endfunction 338 | 339 | if v:version > 703 || v:version == 703 && has('patch32') 340 | function! s:cmaparg(combo, opts) 341 | for opt in a:opts 342 | call add(opt, 1) 343 | endfor 344 | return s:for_args('maparg', a:combo, a:opts) 345 | endfunction 346 | else 347 | " FIXME: Unable to check if it's mapping 348 | function! s:cmaparg(combo, opts) 349 | let arg = s:for_args('maparg', a:combo, a:opts) 350 | let dict = { 'rhs': arg, 'expr': 0, 'noremap': 1 } 351 | return empty(arg) ? {} : dict 352 | endfunction 353 | endif 354 | 355 | function! s:mapcheck(str) 356 | return s:for_args('mapcheck', a:str, [['c', 0], ['l', 0]]) 357 | endfunction 358 | 359 | function! s:getchar() 360 | let timeout = 0 361 | while 1 362 | let c = s:timed_getchar(timeout) 363 | if !s:use_maps || s:noremap > 0 364 | let s:noremap = max([0, s:noremap - len(c)]) 365 | return c 366 | endif 367 | 368 | call add(s:keystrokes, c) 369 | let maparg = s:cmaparg(join(s:keystrokes, ''), [['c', 0], ['l', 0]]) 370 | 371 | " FIXME: For now, let's just assume that we don't have multiple mappings 372 | " with the same prefix 373 | " e.g. 374 | " cnoremap x X 375 | " cnoremap xy XY 376 | " 377 | " In this case, xy should evaluate to XY, but this is not correctly 378 | " implemented since it doesn't seem possible to check at the time we 379 | " have 'x' if there is any mapping that starts with 'x'. 380 | " 381 | " We could look through the output of 'cmap' command, but it's just 382 | " too hacky. 383 | if !empty(maparg) 384 | let s:keystrokes = [] 385 | let s:prev_time = 0 386 | 387 | let keys = s:evaluate_keymap(maparg) 388 | if maparg.noremap || stridx(keys, s:evaluate_keyseq(maparg.lhs)) == 0 389 | let s:noremap = len(keys) 390 | endif 391 | call feedkeys(keys) 392 | return g:pseudocl#MAP 393 | elseif !empty(s:mapcheck(join(s:keystrokes, ''))) 394 | if s:prev_time == 0 395 | let s:prev_time = s:gettime() 396 | elseif s:gettime() - s:prev_time > timeout 397 | return s:cancel_map() 398 | endif 399 | let timeout = &timeoutlen 400 | else 401 | return s:cancel_map() 402 | endif 403 | endwhile 404 | endfunction 405 | 406 | function! s:k(ok) 407 | return get(s:remap, a:ok, a:ok) 408 | endfunction 409 | 410 | " Return: 411 | " - code 412 | " - cursor 413 | " - str 414 | function! s:decode_char(c, str, cursor, words, history) 415 | let c = a:c 416 | let str = a:str 417 | let cursor = a:cursor 418 | let matches = [] 419 | 420 | try 421 | if c == s:k("\") 422 | let prefix = substitute(strpart(str, 0, cursor), '\s*$', '', '') 423 | let pos = match(prefix, '\S*$') 424 | if pos >= 0 425 | return [g:pseudocl#CONTINUE, str, pos] 426 | endif 427 | elseif c == s:k("\") 428 | let begins = len(matchstr(strpart(str, cursor), '^\s*')) 429 | let pos = match(str, '\s', cursor + begins + 1) 430 | return [g:pseudocl#CONTINUE, str, pos == -1 ? len(str) : pos] 431 | elseif c == s:k("\") || c == s:k("\") 432 | return [g:pseudocl#EXIT, str, cursor] 433 | elseif c == s:k("\") || c == s:k("\") 434 | let cursor = 0 435 | elseif c == s:k("\") || c == s:k("\") 436 | let cursor = len(str) 437 | elseif c == s:k("\") 438 | return [g:pseudocl#RETURN, str, cursor] 439 | elseif c == s:k("\") 440 | let s:yanked = strpart(str, 0, cursor) 441 | let str = strpart(str, cursor) 442 | let cursor = 0 443 | elseif c == s:k("\") 444 | let ostr = strpart(str, 0, cursor) 445 | let prefix = substitute(substitute(strpart(str, 0, cursor), '\s*$', '', ''), '\S*$', '', '') 446 | let s:yanked = strpart(ostr, len(prefix)) 447 | let str = prefix . strpart(str, cursor) 448 | let cursor = len(prefix) 449 | elseif c == s:k("\") || c == s:k("\") 450 | let prefix = strpart(str, 0, cursor) 451 | let suffix = substitute(strpart(str, cursor), '^.', '', '') 452 | let str = prefix . suffix 453 | elseif c == s:k("\") 454 | let s:yanked = strpart(str, cursor) 455 | let str = strpart(str, 0, cursor) 456 | elseif c == s:k("\") 457 | let str = strpart(str, 0, cursor) . s:yanked . strpart(str, cursor) 458 | let cursor += len(s:yanked) 459 | elseif c == s:k("\") || c == s:k("\") 460 | if cursor == 0 && empty(str) 461 | return [g:pseudocl#EXIT, str, cursor] 462 | endif 463 | let prefix = substitute(strpart(str, 0, cursor), '.$', '', '') 464 | let str = prefix . strpart(str, cursor) 465 | let cursor = len(prefix) 466 | elseif c == s:k("\") || c == s:k("\") 467 | let cursor = len(substitute(strpart(str, 0, cursor), '.$', '', '')) 468 | elseif c == s:k("\") || c == s:k("\") 469 | if cursor == len(str) 470 | return [g:pseudocl#CTRL_F, str, cursor] 471 | endif 472 | let cursor += len(matchstr(strpart(str, cursor), '^.')) 473 | elseif len(a:history) > 1 && ( 474 | \ c == s:k("\") || c == s:k("\") || 475 | \ c == s:k("\") || c == s:k("\") || 476 | \ c == s:k("\") || c == s:k("\") || 477 | \ c == s:k("\") || c == s:k("\")) 478 | let s:history_idx = (c == s:k("\") || c == s:k("\") || 479 | \ c == s:k("\") || c == s:k("\")) ? 480 | \ min([s:history_idx + 1, len(a:history) - 1]) : 481 | \ max([s:history_idx - 1, 0]) 482 | if s:history_idx < len(a:history) 483 | let line = a:history[s:history_idx] 484 | return [g:pseudocl#CONTINUE, line, len(line)] 485 | end 486 | elseif !empty(a:words) && (c == s:k("\") || c == s:k("\")) 487 | let before = strpart(str, 0, cursor) 488 | let matches = get(s:, 'matches', pseudocl#complete#match(before, a:words)) 489 | 490 | if !empty(matches) 491 | if c == s:k("\") 492 | let matches = extend(copy(matches[1:-1]), matches[0:0]) 493 | else 494 | let matches = extend(copy(matches[-1:-1]), matches[0:-2]) 495 | endif 496 | let item = matches[0] 497 | let str = item . strpart(str, cursor) 498 | let cursor = len(item) 499 | endif 500 | elseif c == s:k("\") 501 | let reg = nr2char(getchar()) 502 | 503 | let text = '' 504 | if reg == s:k("\") 505 | let text = expand('') 506 | elseif reg == s:k("\") 507 | let text = expand('') 508 | elseif reg == "=" 509 | let text = eval(s:input('=', '')) 510 | elseif reg =~ '[a-zA-Z0-9"/%#*+:.-]' 511 | let text = getreg(reg) 512 | end 513 | if !empty(text) 514 | let str = strpart(str, 0, cursor) . text . strpart(str, cursor) 515 | let cursor += len(text) 516 | endif 517 | elseif c == s:k("\") || c =~ '[[:print:]]' && c[0] !~ nr2char(128) 518 | if c == s:k("\") 519 | let c = nr2char(getchar()) 520 | endif 521 | let str = strpart(str, 0, cursor) . c . strpart(str, cursor) 522 | let cursor += len(c) 523 | else 524 | return [c, str, cursor] 525 | endif 526 | 527 | call remove(a:history, -1) 528 | call add(a:history, str) 529 | let s:history_idx = len(a:history) - 1 530 | return [g:pseudocl#CONTINUE, str, cursor] 531 | finally 532 | if empty(matches) 533 | unlet! s:matches 534 | else 535 | let s:matches = matches 536 | endif 537 | endtry 538 | endfunction 539 | 540 | let &cpo = s:cpo_save 541 | unlet s:cpo_save 542 | 543 | -------------------------------------------------------------------------------- /test/_clear.vader: -------------------------------------------------------------------------------- 1 | Execute (Clear cmaps): 2 | " Make sure this doesn't break the test 3 | if !exists('g:pseudocl') 4 | let g:pseudocl = { 5 | \ "\": maparg("\", 'c', 0, 1), 6 | \ "\": maparg("\", 'c', 0, 1), 7 | \ "\": maparg("\", 'c', 0, 1), 8 | \ "\": maparg("\", 'c', 0, 1), 9 | \ 'jk': maparg("jk", 'c', 0, 1) 10 | \ } 11 | silent! cunmap 12 | silent! cunmap 13 | silent! cunmap 14 | silent! cunmap 15 | silent! cunmap jk 16 | endif 17 | silent! nunmap " 18 | -------------------------------------------------------------------------------- /test/_restore.vader: -------------------------------------------------------------------------------- 1 | Execute (Restore cmaps): 2 | if exists('g:pseudocl') 3 | for [key, arg] in items(g:pseudocl) 4 | if arg.expr 5 | execute "cnoremap ".key." ".arg.rhs 6 | else 7 | execute "cnoremap ".key." ".arg.rhs 8 | endif 9 | endfor 10 | unlet g:pseudocl 11 | endif 12 | 13 | -------------------------------------------------------------------------------- /test/basic.vader: -------------------------------------------------------------------------------- 1 | Include: _clear.vader 2 | 3 | Do (Basic): 4 | :let g:got = pseudocl#start({})\ 5 | abcde\ 6 | Execute: 7 | AssertEqual 'abcde', g:got 8 | 9 | Do (CTRL-A CTRL-D): 10 | :let g:got = pseudocl#start({})\ 11 | abcde 12 | \\ 13 | \ 14 | Execute: 15 | AssertEqual 'bcde', g:got 16 | 17 | Do (CTRL-A CTRL-D CTRL-F Right CTRL-H): 18 | :let g:got = pseudocl#start({})\ 19 | abcde 20 | \\ 21 | \\\ 22 | \ 23 | Execute: 24 | AssertEqual 'bde', g:got 25 | 26 | Do (CTRL-A CTRL-D CTRL-F Right CTRL-H Left CTRL-E): 27 | :let g:got = pseudocl#start({})\ 28 | abcde 29 | \\ 30 | \\\ 31 | \가나다 32 | \ 다나가 33 | \ 34 | Execute: 35 | AssertEqual '가나다 bde 다나가', g:got 36 | 37 | Do (CTRL-A CTRL-D CTRL-F Right CTRL-H Left CTRL-E CTRL-B CTRL-K): 38 | :let g:got = pseudocl#start({})\ 39 | abcde 40 | \\ 41 | \\\ 42 | \가나다 43 | \ 다나가 44 | \\\ 45 | \ 46 | Execute: 47 | AssertEqual '가나다 bde 다', g:got 48 | 49 | Do (Shift-left/right Del): 50 | :let g:got = pseudocl#start({})\ 51 | 가나다 abc 다나가 52 | \\ 53 | \\\\\ 54 | \\\ 55 | \ 56 | Execute: 57 | AssertEqual '나다 ab 나가', g:got 58 | 59 | Do (Home / End): 60 | :let g:got = pseudocl#start({})\ 61 | 가나다 abc 다나가 62 | \\ 63 | \\ 64 | \ 65 | Execute: 66 | AssertEqual '나다 abc 다나', g:got 67 | 68 | Do (CTRL-R-(W/A/=/"/%)): 69 | iapple apple-juice 70 | \ 71 | ^w 72 | "ayiW 73 | yiw 74 | :let g:got = pseudocl#start({})\ 75 | \\ 76 | \\ 77 | \" 78 | \=1234\ 79 | \a 80 | \% 81 | \ 82 | Execute: 83 | AssertEqual 'apple apple-juice apple 1234 apple-juice [Vader-workbench]', g:got 84 | 85 | Do (CTRL-V - char): 86 | :let g:got = pseudocl#start({})\ 87 | \\ 88 | \a 89 | \ 90 | Execute: 91 | AssertEqual "\a", g:got 92 | 93 | Do (CTRL-U CTRL-Y CTRL-Y): 94 | :let g:got = pseudocl#start({})\ 95 | 가나다 96 | \ 97 | \ 98 | \ 99 | \ 100 | Execute: 101 | AssertEqual "가나다가나다", g:got 102 | 103 | Do (CTRL-K CTRL-Y CTRL-Y): 104 | :let g:got = pseudocl#start({})\ 105 | 가나다 106 | \\ 107 | \ 108 | \ 109 | \ 110 | \ 111 | Execute: 112 | AssertEqual "가나다나다", g:got 113 | 114 | Do (CTRL-W CTRL-Y CTRL-Y): 115 | :let g:got = pseudocl#start({})\ 116 | 가나다 라마바 117 | \ 118 | \ 119 | \ 120 | \ 121 | Execute: 122 | AssertEqual "가나다 라마바 라마바 ", g:got 123 | 124 | Execute (try-catch block): 125 | function! TryPseudoCL(...) 126 | try 127 | let g:got = pseudocl#start(a:0 == 0 ? {} : a:1) 128 | catch 'exit' 129 | let g:got = 'exited' 130 | endtry 131 | endfunction 132 | 133 | Do (CTRL-C): 134 | :call TryPseudoCL()\ 135 | abcde\ 136 | \ 137 | Execute: 138 | AssertEqual 'exited', g:got 139 | 140 | Execute (Prepare options dictionary for remap): 141 | let g:opt = { 'remap': { "\": "\", "\": "\" } } 142 | 143 | Do (CTRL-H / CTRL-D swapped): 144 | :call TryPseudoCL(g:opt)\ 145 | abcde\fg\\\ 146 | \ 147 | Execute: 148 | AssertEqual 'acdfg', g:got 149 | unlet g:opt 150 | 151 | Do (ESC): 152 | :call TryPseudoCL()\ 153 | abcde\ 154 | \ 155 | Execute: 156 | AssertEqual 'exited', g:got 157 | 158 | Do (Empty string): 159 | :call TryPseudoCL()\ 160 | a\ 161 | \ 162 | Execute: 163 | AssertEqual '', g:got 164 | 165 | Do (Backspace to exit): 166 | :call TryPseudoCL()\ 167 | a\\ 168 | \ 169 | Execute: 170 | AssertEqual 'exited', g:got 171 | 172 | Do (Highlight for Cursor defined): 173 | :hi Cursor ctermfg=red ctermbg=blue\ 174 | :call TryPseudoCL()\ 175 | abcde\ 176 | Execute: 177 | AssertEqual 'abcde', g:got 178 | redir => out 179 | silent hi Cursor 180 | redir END 181 | Log out 182 | 183 | Do (Cursor linked to other highlight group): 184 | :hi clear Cursor\ 185 | :hi link Cursor TabLineFill\ 186 | :call TryPseudoCL()\ 187 | abcde\ 188 | Execute: 189 | AssertEqual 'abcde', g:got 190 | redir => out 191 | silent hi Cursor 192 | redir END 193 | Log out 194 | AssertEqual 'TabLineFill', matchstr(out, 'links to \zs.*') 195 | 196 | Execute (Cleanup): 197 | unlet out 198 | delfunction TryPseudoCL 199 | 200 | Include: _restore.vader 201 | -------------------------------------------------------------------------------- /test/callbacks.vader: -------------------------------------------------------------------------------- 1 | Include: _clear.vader 2 | 3 | Execute (Setup): 4 | function! OnChange(...) 5 | call add(g:changes, a:000) 6 | endfunction 7 | 8 | function! OnUnknownKey(...) 9 | call add(g:unknowns, a:000) 10 | return extend([g:pseudocl#CONTINUE], a:000[1:]) 11 | endfunction 12 | 13 | let g:opts = { 14 | \ 'on_change': function('OnChange'), 15 | \ 'on_unknown_key': function('OnUnknownKey') 16 | \ } 17 | 18 | Before: 19 | let g:changes = [] 20 | let g:unknowns = [] 21 | After: 22 | AssertEqual 5, len(g:changes) 23 | AssertEqual ['a', '', 1], g:changes[0] 24 | AssertEqual ['ab', 'a', 2], g:changes[1] 25 | AssertEqual ['abc', 'ab', 3], g:changes[2] 26 | Log g:unknowns 27 | AssertEqual 3, len(g:unknowns) 28 | AssertEqual ["\", 'ab', 2], g:unknowns[0] 29 | AssertEqual ["\", 'abcd', 4], g:unknowns[1] 30 | AssertEqual ["\", 'abcde', 5], g:unknowns[2] 31 | 32 | Do (Basic): 33 | :let g:got = pseudocl#start(g:opts)\ 34 | ab 35 | \ 36 | cd 37 | \ 38 | e 39 | \ 40 | \ 41 | 42 | Before: 43 | After: 44 | Execute (Cleanup): 45 | unlet g:changes g:unknowns 46 | delfunction OnChange 47 | delfunction OnUnknownKey 48 | 49 | Include: _restore.vader 50 | -------------------------------------------------------------------------------- /test/cmap.vader: -------------------------------------------------------------------------------- 1 | Include: _clear.vader 2 | 3 | Execute (Setup): 4 | cnoremap abc ABC 5 | cnoremap xyz XYZ 6 | cnoremap xyy XYY 7 | cnoremap xxx 8 | cnoremap 123 1 + 2 + 3 9 | cnoremap jk 10 | nnoremap :: :let g:got = pseudocl#start({}) 11 | nnoremap ::: :let g:got = pseudocl#start({'map': 0}) 12 | 13 | Execute (TODO Feedkeys doesn't work correctly in macros): 14 | normal ::abcxxx 15 | AssertEqual 'ABC', g:got 16 | normal ::xyzxxx 17 | AssertEqual 'XYZ', g:got 18 | normal ::abcxyzxxx 19 | AssertEqual 'ABCXYZ', g:got 20 | normal ::xyyxxx 21 | AssertEqual 'XYY', g:got 22 | normal ::xyxxxx 23 | AssertEqual 'xyx', g:got 24 | normal ::123xxx 25 | AssertEqual '6', g:got 26 | try 27 | normal ::123jk 28 | Assert 0, 'Should not reach here' 29 | catch 'exit' 30 | endtry 31 | 32 | execute "normal :::abcxyz\" 33 | AssertEqual 'abcxyz', g:got 34 | 35 | Execute (Cleanup): 36 | cunmap abc 37 | cunmap xyz 38 | cunmap xyy 39 | cunmap xxx 40 | cunmap 123 41 | cunmap jk 42 | nunmap :: 43 | nunmap ::: 44 | 45 | Include: _restore.vader 46 | -------------------------------------------------------------------------------- /test/completion.vader: -------------------------------------------------------------------------------- 1 | Include: _clear.vader 2 | 3 | Execute (Setup): 4 | let g:opts = {'words': ['apple', 'applejuice', 'appleinc']} 5 | 6 | Do (Tab): 7 | :let g:got = pseudocl#start(g:opts)\ 8 | ap\\ 9 | Execute: 10 | AssertEqual 'apple', g:got 11 | 12 | Do (S-tab): 13 | :let g:got = pseudocl#start(g:opts)\ 14 | ap\\ 15 | Execute: 16 | AssertEqual 'appleinc', g:got 17 | 18 | Do (Tab Tab): 19 | :let g:got = pseudocl#start(g:opts)\ 20 | ap\\\ 21 | Execute: 22 | AssertEqual 'applejuice', g:got 23 | 24 | Do (Tab Tab S-Tab Tab): 25 | :let g:got = pseudocl#start(g:opts)\ 26 | ap\\\\\ 27 | Execute: 28 | AssertEqual 'applejuice', g:got 29 | 30 | Do (Tab Tab Tab Tab): 31 | :let g:got = pseudocl#start(g:opts)\ 32 | ap\\\\\ 33 | Execute: 34 | AssertEqual 'ap', g:got 35 | 36 | Do (Tab Tab Change S-Tab): 37 | :let g:got = pseudocl#start(g:opts)\ 38 | ap\\ 39 | \\\\\ 40 | lej\ 41 | \ 42 | Execute: 43 | AssertEqual 'applejuice', g:got 44 | 45 | Execute (Cleanup): 46 | unlet g:opts 47 | 48 | Include: _restore.vader 49 | -------------------------------------------------------------------------------- /test/history.vader: -------------------------------------------------------------------------------- 1 | Include: _clear.vader 2 | 3 | Execute: 4 | let g:unknown_key_cnt = 0 5 | function! UnknownKey(...) 6 | let g:unknown_key_cnt += 1 7 | return extend([g:pseudocl#CONTINUE], a:000[1:]) 8 | endfunction 9 | 10 | Do (no history - C-P): 11 | :let g:got = pseudocl#start({'on_unknown_key': function('UnknownKey')})\ 12 | \\ 13 | Execute: 14 | AssertEqual '', g:got 15 | AssertEqual 1, g:unknown_key_cnt 16 | unlet g:unknown_key_cnt 17 | delfunction UnknownKey 18 | 19 | Execute (Setup): 20 | let g:opts = {'history': ['apple', 'applejuice', 'appleinc']} 21 | 22 | Do (C-N): 23 | :let g:got = pseudocl#start(g:opts)\ 24 | \\ 25 | Execute: 26 | AssertEqual '', g:got 27 | 28 | Do (C-P): 29 | :let g:got = pseudocl#start(g:opts)\ 30 | \\ 31 | Execute: 32 | AssertEqual 'appleinc', g:got 33 | 34 | Do (C-P Up): 35 | :let g:got = pseudocl#start(g:opts)\ 36 | abc\\\ 37 | Execute: 38 | AssertEqual 'applejuice', g:got 39 | 40 | Do (C-P S-Up Up PageUp): 41 | :let g:got = pseudocl#start(g:opts)\ 42 | abc\\\\\ 43 | Execute: 44 | AssertEqual 'apple', g:got 45 | 46 | Do (C-P C-P C-P C-P C-N): 47 | :let g:got = pseudocl#start(g:opts)\ 48 | abc\\\\\\ 49 | Execute: 50 | AssertEqual 'applejuice', g:got 51 | 52 | Do (C-P C-P C-P C-P S-Down Change C-P): 53 | :let g:got = pseudocl#start(g:opts)\ 54 | abc\\\\\ 55 | !!! 56 | \ 57 | \ 58 | Execute: 59 | AssertEqual 'appleinc', g:got 60 | 61 | Do (C-P C-P C-P C-P PageDown Change C-P Down): 62 | :let g:got = pseudocl#start(g:opts)\ 63 | abc\\\\\ 64 | !!! 65 | \ 66 | \ 67 | \ 68 | Execute: 69 | AssertEqual 'applejuice!!!', g:got 70 | 71 | Execute (Cleanup): 72 | unlet g:opts 73 | 74 | Include: _restore.vader 75 | --------------------------------------------------------------------------------