neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

vimexpect.vim (4946B)


      1 " vimexpect.vim is a small object-oriented library that simplifies the task of
      2 " scripting communication with jobs or any interactive program. The name
      3 " `expect` comes from the famous tcl extension that has the same purpose.
      4 "
      5 " This library is built upon two simple concepts: Parsers and States.
      6 "
      7 " A State represents a program state and associates a set of regular
      8 " expressions(to parse program output) with method names(to deal with parsed
      9 " output). States are created with the vimexpect#State(patterns) function.
     10 "
     11 " A Parser manages data received from the program. It also manages State
     12 " objects by storing them into a stack, where the top of the stack is the
     13 " current State. Parsers are created with the vimexpect#Parser(initial_state,
     14 " target) function
     15 "
     16 " The State methods are defined by the user, and are always called with `self`
     17 " set as the Parser target. Advanced control flow is achieved by changing the
     18 " current state with the `push`/`pop`/`switch` parser methods.
     19 "
     20 " An example of this library in action can be found in Neovim source
     21 " code(contrib/neovim_gdb subdirectory)
     22 
     23 let s:State = {}
     24 
     25 
     26 " Create a new State instance with a list where each item is a [regexp, name]
     27 " pair. A method named `name` must be defined in the created instance.
     28 function s:State.create(patterns)
     29  let this = copy(self)
     30  let this._patterns = a:patterns
     31  return this
     32 endfunction
     33 
     34 
     35 let s:Parser = {}
     36 let s:Parser.LINE_BUFFER_MAX_LEN = 100
     37 
     38 
     39 " Create a new Parser instance with the initial state and a target. The target
     40 " is a dictionary that will be the `self` of every State method call associated
     41 " with the parser, and may contain options normally passed to
     42 " `jobstart`(on_stdout/on_stderr will be overridden). Returns the target so it
     43 " can be called directly as the second argument of `jobstart`:
     44 "
     45 " call jobstart(prog_argv, vimexpect#Parser(initial_state, {'pty': 1}))
     46 function s:Parser.create(initial_state, target)
     47  let parser = copy(self)
     48  let parser._line_buffer = []
     49  let parser._stack = [a:initial_state]
     50  let parser._target = a:target
     51  let parser._target.on_stdout = function('s:JobOutput')
     52  let parser._target.on_stderr = function('s:JobOutput')
     53  let parser._target._parser = parser
     54  return parser._target
     55 endfunction
     56 
     57 
     58 " Push a state to the state stack
     59 function s:Parser.push(state)
     60  call add(self._stack, a:state)
     61 endfunction
     62 
     63 
     64 " Pop a state from the state stack. Fails if there's only one state remaining.
     65 function s:Parser.pop()
     66  if len(self._stack) == 1
     67    throw 'vimexpect:emptystack:State stack cannot be empty'
     68  endif
     69  return remove(self._stack, -1)
     70 endfunction
     71 
     72 
     73 " Replace the state currently in the top of the stack.
     74 function s:Parser.switch(state)
     75  let old_state = self._stack[-1]
     76  let self._stack[-1] = a:state
     77  return old_state
     78 endfunction
     79 
     80 
     81 " Append a list of lines to the parser line buffer and try to match it the
     82 " current state. This will shift old lines if the buffer crosses its
     83 " limit(defined by the LINE_BUFFER_MAX_LEN field). During normal operation,
     84 " this function is called by the job handler provided by this module, but it
     85 " may be called directly by the user for other purposes(testing for example)
     86 function s:Parser.feed(lines)
     87  if empty(a:lines)
     88    return
     89  endif
     90  let lines = a:lines
     91  let linebuf = self._line_buffer
     92  if lines[0] != "\n" && !empty(linebuf)
     93    " continue the previous line
     94    let linebuf[-1] .= lines[0]
     95    call remove(lines, 0)
     96  endif
     97  " append the newly received lines to the line buffer
     98  let linebuf += lines
     99  " keep trying to match handlers while the line isnt empty
    100  while !empty(linebuf)
    101    let match_idx = self.parse(linebuf)
    102    if match_idx == -1
    103      break
    104    endif
    105    let linebuf = linebuf[match_idx + 1 : ]
    106  endwhile
    107  " shift excess lines from the buffer
    108  while len(linebuf) > self.LINE_BUFFER_MAX_LEN
    109    call remove(linebuf, 0)
    110  endwhile
    111  let self._line_buffer = linebuf
    112 endfunction
    113 
    114 
    115 " Try to match a list of lines with the current state and call the handler if
    116 " the match succeeds. Return the index in `lines` of the first match.
    117 function s:Parser.parse(lines)
    118  let lines = a:lines
    119  if empty(lines)
    120    return -1
    121  endif
    122  let state = self.state()
    123  " search for a match using the list of patterns
    124  for [pattern, handler] in state._patterns
    125    let matches = matchlist(lines, pattern)
    126    if empty(matches)
    127      continue
    128    endif
    129    let match_idx = match(lines, pattern)
    130    call call(state[handler], matches[1:], self._target)
    131    return match_idx
    132  endfor
    133 endfunction
    134 
    135 
    136 " Return the current state
    137 function s:Parser.state()
    138  return self._stack[-1]
    139 endfunction
    140 
    141 
    142 " Job handler that simply forwards lines to the parser.
    143 function! s:JobOutput(_id, lines, _event) dict
    144  call self._parser.feed(a:lines)
    145 endfunction
    146 
    147 function vimexpect#Parser(initial_state, target)
    148  return s:Parser.create(a:initial_state, a:target)
    149 endfunction
    150 
    151 
    152 function vimexpect#State(patterns)
    153  return s:State.create(a:patterns)
    154 endfunction