123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 |
- " vimexpect.vim is a small object-oriented library that simplifies the task of
- " scripting communication with jobs or any interactive program. The name
- " `expect` comes from the famous tcl extension that has the same purpose.
- "
- " This library is built upon two simple concepts: Parsers and States.
- "
- " A State represents a program state and associates a set of regular
- " expressions(to parse program output) with method names(to deal with parsed
- " output). States are created with the vimexpect#State(patterns) function.
- "
- " A Parser manages data received from the program. It also manages State
- " objects by storing them into a stack, where the top of the stack is the
- " current State. Parsers are created with the vimexpect#Parser(initial_state,
- " target) function
- "
- " The State methods are defined by the user, and are always called with `self`
- " set as the Parser target. Advanced control flow is achieved by changing the
- " current state with the `push`/`pop`/`switch` parser methods.
- "
- " An example of this library in action can be found in Neovim source
- " code(contrib/neovim_gdb subdirectory)
- let s:State = {}
- " Create a new State instance with a list where each item is a [regexp, name]
- " pair. A method named `name` must be defined in the created instance.
- function s:State.create(patterns)
- let this = copy(self)
- let this._patterns = a:patterns
- return this
- endfunction
- let s:Parser = {}
- let s:Parser.LINE_BUFFER_MAX_LEN = 100
- " Create a new Parser instance with the initial state and a target. The target
- " is a dictionary that will be the `self` of every State method call associated
- " with the parser, and may contain options normally passed to
- " `jobstart`(on_stdout/on_stderr will be overridden). Returns the target so it
- " can be called directly as the second argument of `jobstart`:
- "
- " call jobstart(prog_argv, vimexpect#Parser(initial_state, {'pty': 1}))
- function s:Parser.create(initial_state, target)
- let parser = copy(self)
- let parser._line_buffer = []
- let parser._stack = [a:initial_state]
- let parser._target = a:target
- let parser._target.on_stdout = function('s:JobOutput')
- let parser._target.on_stderr = function('s:JobOutput')
- let parser._target._parser = parser
- return parser._target
- endfunction
- " Push a state to the state stack
- function s:Parser.push(state)
- call add(self._stack, a:state)
- endfunction
- " Pop a state from the state stack. Fails if there's only one state remaining.
- function s:Parser.pop()
- if len(self._stack) == 1
- throw 'vimexpect:emptystack:State stack cannot be empty'
- endif
- return remove(self._stack, -1)
- endfunction
- " Replace the state currently in the top of the stack.
- function s:Parser.switch(state)
- let old_state = self._stack[-1]
- let self._stack[-1] = a:state
- return old_state
- endfunction
- " Append a list of lines to the parser line buffer and try to match it the
- " current state. This will shift old lines if the buffer crosses its
- " limit(defined by the LINE_BUFFER_MAX_LEN field). During normal operation,
- " this function is called by the job handler provided by this module, but it
- " may be called directly by the user for other purposes(testing for example)
- function s:Parser.feed(lines)
- if empty(a:lines)
- return
- endif
- let lines = a:lines
- let linebuf = self._line_buffer
- if lines[0] != "\n" && !empty(linebuf)
- " continue the previous line
- let linebuf[-1] .= lines[0]
- call remove(lines, 0)
- endif
- " append the newly received lines to the line buffer
- let linebuf += lines
- " keep trying to match handlers while the line isnt empty
- while !empty(linebuf)
- let match_idx = self.parse(linebuf)
- if match_idx == -1
- break
- endif
- let linebuf = linebuf[match_idx + 1 : ]
- endwhile
- " shift excess lines from the buffer
- while len(linebuf) > self.LINE_BUFFER_MAX_LEN
- call remove(linebuf, 0)
- endwhile
- let self._line_buffer = linebuf
- endfunction
- " Try to match a list of lines with the current state and call the handler if
- " the match succeeds. Return the index in `lines` of the first match.
- function s:Parser.parse(lines)
- let lines = a:lines
- if empty(lines)
- return -1
- endif
- let state = self.state()
- " search for a match using the list of patterns
- for [pattern, handler] in state._patterns
- let matches = matchlist(lines, pattern)
- if empty(matches)
- continue
- endif
- let match_idx = match(lines, pattern)
- call call(state[handler], matches[1:], self._target)
- return match_idx
- endfor
- endfunction
- " Return the current state
- function s:Parser.state()
- return self._stack[-1]
- endfunction
- " Job handler that simply forwards lines to the parser.
- function! s:JobOutput(_id, lines, _event) dict
- call self._parser.feed(a:lines)
- endfunction
- function vimexpect#Parser(initial_state, target)
- return s:Parser.create(a:initial_state, a:target)
- endfunction
- function vimexpect#State(patterns)
- return s:State.create(a:patterns)
- endfunction
|