diff options
Diffstat (limited to 'runtime')
-rw-r--r-- | runtime/autoload/vimexpect.vim | 154 |
1 files changed, 154 insertions, 0 deletions
diff --git a/runtime/autoload/vimexpect.vim b/runtime/autoload/vimexpect.vim new file mode 100644 index 0000000000..16e7d30d6c --- /dev/null +++ b/runtime/autoload/vimexpect.vim @@ -0,0 +1,154 @@ +" 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 overriden). 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) + 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 |