├── .gitattributes ├── .gitignore ├── LICENSE ├── OUTLINE.txt ├── README.md ├── autoload └── vsh │ ├── bind_parser_tests.py │ ├── flush-large-output │ ├── gdb.vim │ ├── origvim_server.py │ ├── origvim_server_setup.py │ ├── py.vim │ ├── vsh.py │ ├── vsh.vim │ ├── vsh_editor_prog │ ├── vsh_inputrc_snippet │ ├── vsh_man_pager │ ├── vsh_shell_start │ └── vsh_tell_editor_bindings.py ├── demo ├── demo-gdb-integration.vsh ├── demo.py ├── demo.txt ├── demo.vsh ├── demo_part2.vsh ├── demo_part3.vsh ├── demo_stored_lines.vsh ├── setup-env.vsh └── tree.c ├── doc └── vsh.txt ├── emacs-tar.sh ├── ftdetect └── vsh.vim ├── ftplugin ├── python.vim └── vsh.vim ├── integration └── with_gdb.py ├── plugin └── vsh.vim ├── syntax └── vsh.vim ├── tests └── test.vader └── vsh-mode-1.0 ├── integration_with_gdb.py ├── vsh-elisp-tests.el ├── vsh-mode-pkg.el ├── vsh-mode.el ├── vsh-tests.erts ├── vsh_man_pager ├── vsh_shell_start └── vsh_tell_editor_bindings.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.vsh linguist-language=Markdown 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/tags 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Matthew Malcomson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OUTLINE.txt: -------------------------------------------------------------------------------- 1 | Basic outline of the project. 2 | 3 | For each buffer of filetype `vsh`, we create a new shell process running in a 4 | new pseudo terminal. 5 | 6 | The automatic creation of the process is done by ftplugin/vsh.vim 7 | This is set up to work on filetypes with a .vsh extension by the 8 | ftdetect/vsh.vim file. 9 | 10 | When a vsh buffer is detected, we set the current prompt with 11 | vsh#vsh#SetPrompt(). 12 | This handles setting the b:vsh_prompt, &l:comments and &l:commentstring 13 | variables for the current buffer, along with the syntax highlighting. 14 | The syntax highlighting needs to be dynamically set based on the 15 | b:vsh_prompt variable, which means we need to use the `execute` command 16 | with dynamically created strings. 17 | We rely on these being updated in a coordinated manner so as not to 18 | cause any confusion. 19 | 20 | Since we need the above variables to be updated together, we lock the 21 | b:vsh_prompt variable. 22 | All this does is protect against any direct tampering, mainly to alert 23 | whoever is doing it that this is probably something that'll cause bugs. 24 | 25 | The prompt can be changed by the user with the vsh#vsh#SetPrompt() 26 | function. 27 | 28 | Setting up the new pseudo terminal is done with the vimL jobstart() 29 | function. 30 | The environment for the new shell is set up in vsh_shell_start. 31 | This: 32 | sets termios values according to how vsh works (e.g. don't repeat 33 | input). 34 | exports some environment variables 35 | ensures the shell for the user starts in the current directory of 36 | the `vsh` buffer 37 | finds the default readline bindings from querying bindings in bash. 38 | See notes on completion below for the use of these bindings. 39 | 40 | Mappings are defined in plugin/vsh.vim 41 | I put them in plugin/vsh.vim so they're defined just the once at startup. 42 | The big list means I can query them interactively, and makes it easy to add 43 | one more at a time. 44 | There are three sets of mappings defined there. 45 | The mappings for inside a vsh buffer. 46 | Global mappings designed to be available everywhere. 47 | Python specific mappings for in a Python buffer. 48 | We have python specific mappings because it's the only language 49 | (I know of) that relies on whitespace differently depending on 50 | whether you're working at a REPL or in a script. 51 | In a REPL blocks are terminated by a blank line, but they aren't in 52 | a script. 53 | To aid sending code between script files and a REPL, I've added 54 | functions that remove blank lines and terminate blocks with blank 55 | lines. 56 | 57 | A number of motions and operators are defined in autoload/vsh/vsh.vim. 58 | These are all pretty self-explanatory. 59 | The main thing to remember is that there are a few slightly varying types of 60 | markers in a vsh buffer. 61 | A line beginning with the b:vsh_prompt variable *excluding trailing 62 | whitespace* marks a line as not being part of output. 63 | 64 | A comment is a line beginning with b:vsh_prompt followed by any amount 65 | of whitespace before a single hash character #. Any remaining text to the 66 | end of the line is part of the comment. 67 | 68 | A command line (that can be run by pressing ) is marked by a line 69 | beginning with the b:vsh_prompt that is not a comment. 70 | The b:vsh_prompt variable usually has some whitespace at its end (e.g. the 71 | default has a single trailing space character), which can be a little 72 | confusing when visually determining whether a line is a command. 73 | 74 | A *motion* line (i.e. lines that and move to) is either a 75 | command line, a line consisting of just b:vsh_prompt, or a line beginning 76 | with b:vsh_prompt . ' # ' . b:vsh_prompt. 77 | 78 | n.b. a subtlety that hasn't yet been a problem, but may in the future is 79 | that a line that starts with b:vsh_prompt and doesn't match 80 | s:commentstart() may still be a comment. 81 | i.e. the function s:commentstart() just provides the minimal string to 82 | start a comment. 83 | There may be more spaces between b:vsh_prompt and the '#' character. 84 | 85 | 86 | Operators are defined slightly differently to the example in the :help g@ 87 | Instead of 88 | `nmap :set operatorfunc=vsh#vsh#RunOperatorFunc()g@`, I use 89 | `nmap vsh#vsh#DoRunOperatorFunc()` 90 | This is originally because I needed to do some extra work when setting the 91 | operator function, but it's kept around so that things like 3_ work on 92 | three lines at a time. 93 | (following the example in :help 3 complains that a count isn't allowed 94 | in the :set opfunc command, and putting in front of the colon just 95 | completely ignores the count). 96 | 97 | 98 | We use python to insert text on the callback in case the callback happens when 99 | the user is in a different buffer. 100 | Python allows modifying text without having to set the current buffer to the 101 | buffer you want to change text in. 102 | 103 | We use python to remove all output from a command so we don't have to worry 104 | about saving and restoring the users cursor position, jumplist etc. 105 | 106 | In order to speed up adding text into the buffer on large outputs, we remember 107 | the end of the current output with the mark 'd and remember the start of the 108 | current output with the mark 'p. 109 | The 'p mark is there as a backup for if the user deletes the 'd mark. 110 | If the user redefines the position of the 'd mark output will simply be placed 111 | there. 112 | On the one hand it seems like using the extended marks PR (when it lands) 113 | would make a cleaner interface (the user can't accidently interfere with 114 | the output positioning). 115 | On the other hand, I've found it very useful to have the 'd and 'p marks 116 | around -- for getting back to the latest ran command, and for operations. 117 | 118 | Completions are done by sending control sequences to the underlying terminal. 119 | The sequences are guessed by querying bash to see what bindings it would be 120 | using, and hoping that the readline bindings are pretty much the same across 121 | all terminal applications. 122 | This can easily fail. 123 | The control sequences are stored in the b:vsh_completions_cmd variable, these 124 | can be changed temporarily in functions by the user if they want special 125 | handling. 126 | Sometimes inserting ^I^U on the command line can provide a workaround. 127 | The guess is based on the readline bindings in the original bash shell. 128 | There may be a more clever way to guess what bindings based on what process is 129 | in the foreground, but I don't think it's wise to go down that route. 130 | 131 | Can manually send a control character with c 132 | 133 | Want to kill subprocesses when the buffer is deleted, but can't access buffer 134 | local variables in the BufDelete autocmd. 135 | Hence store the relevant job number in a global variable g:vsh_closing_jobs 136 | when unloading the buffer, and on the BufDelete autocmd runs check for that 137 | buffer name in the stored closing jobs. 138 | 139 | Opening files with netrw's `gx` and file completion is done by 140 | temporarily changing the current :lcd and reverting afterwards. 141 | For file completion reverting is done with an autocmd, for `gx` revertion 142 | is simply done done after the mapping has been typed. 143 | 144 | Opening files with `gf` and similar is done by setting the 'path' variable to 145 | include the current directory, and an autocmd is set to reset the 'path'. 146 | e.g. when opening a file with gf, we change current directory, set autocmd 147 | BufLeave to change the directory back once gf has been executed, and run 148 | `gf`. 149 | What happens when gf doesn't work? 150 | Currently we appear to just never change the current directory. 151 | I don't know whether this is defined behaviour, or if I'm just 152 | getting lucky. 153 | What happens if gf is on the current file? 154 | Current subprocess stays alive but the b:vsh_job variable gets 155 | removed. 156 | Same happens with :edit % Currently don't know what exactly is 157 | happening, and, honestly have no plans to look into it until it 158 | starts "hurting". 159 | TODO This should be fixed in the future. 160 | 161 | 162 | b:undo_ftplugin 163 | This variable is set to a command string that is called when changing the 164 | filetype of the current buffer. 165 | I use it to call an autoload function that 166 | a) removes all local mappings 167 | b) closes the current process 168 | c) removes all buffer variables 169 | 170 | 171 | Syntax highlighting is generated based on the prompt given by the user. 172 | Highlighting is done before running the ftplugin, so we need to define the 173 | default colors first. 174 | So we can define the output colors programmatically (i.e. in a function) 175 | without redefining the function many times, the syntax/vsh.vim file simply 176 | calls an autoloaded function vsh#vsh#DefaultColors(). 177 | When the ftplugin is run for this buffer, vsh#vsh#SetPrompt() calls colors 178 | again but with the prompt taken from the users environment. 179 | 180 | 181 | $EDITOR is set in the shell to change the arglist in the current nvim instance 182 | to whatever is given on the command line. 183 | You can restore the original arguments with vsh#vsh#RestoreArgs() 184 | 185 | To avoid writing passwords in plaintext there's a command :VshPass that uses 186 | vims password facilities. 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vsh -- Record and replay terminal sessions inside vim and saved to disk 2 | 3 | Vim plugin for experimental shell sessions -- save a session for later/others 4 | and search/modify output with the power of vim. 5 | 6 | It is different to the :terminal command for two main reasons: 7 | 1. You run commands anywhere in the file. 8 | 2. The buffer corresponds to a standard file on disk. 9 | 10 | It can also be thought of as an interactive version of script(1) -- interactive 11 | because you can go back and modify what's stored while you work -- that 12 | facilitates re-running the same session during reading. 13 | 14 | Benefits are mainly around exploratory terminal sessions, where you want to 15 | 16 | 1. Keep a clean record of what you did, for record keeping. 17 | * This often means removing output from unimportant `--help` commands. 18 | 2. Easily repeat those actions you performed -- whether restarting a python 19 | REPL or GDB session, re-running a manual inspection you have not yet 20 | formalised into an automatic testcase, or re-running a shell session given 21 | to you (by yourself a year ago or by your collegue) that demonstrates a 22 | behaviour or reproduces a bug. 23 | 3. Search through and modify output of commands with Vim shortcuts. 24 | 4. Write notes/annotations alongside commands for others to understand what's 25 | going on. 26 | 5. Prepare and replay a live demo 27 | ([see my presentation on GDB walkers](https://www.youtube.com/watch?v=YHLiwvf28fQ&pp=ygUKZm9zZGVtIGdkYg%3D%3D)). 28 | 29 | Some demos are provided in the links below. 30 | 31 | Requires vim with patch 8.0.0764 or nvim version 0.2. 32 | 33 | ### Installation 34 | Install with favourite package manager or Vim's built-in package support. 35 | For emacs can find this package on MELPA. 36 | 37 | After installation, I'd suggest opening up vsh/demo/setup-env.sh to read (and 38 | possibly run) the (small number of) configuration steps there in turn. The 39 | biggest point is to ensure `TERM=dumb` is not overridden in your bashrc (manual 40 | adjustment to a different shell should be relatively simple). 41 | 42 | ### Motivation video 43 | [![Vsh motivation](https://asciinema.org/a/9zn5e69g0by7e9kdsz1vlzgf8.png)](https://asciinema.org/a/9zn5e69g0by7e9kdsz1vlzgf8) 44 | 45 | ### Basic introduction 46 | [![Vsh basics](https://asciinema.org/a/100675.png)](https://asciinema.org/a/100675) 47 | ### Editing output 48 | [![Editing output](https://asciinema.org/a/100676.png)](https://asciinema.org/a/100676) 49 | ### Sending text 50 | [![Sending text](https://asciinema.org/a/100677.png)](https://asciinema.org/a/100677) 51 | ### Editing Files 52 | [![Editing Files](https://asciinema.org/a/100678.png)](https://asciinema.org/a/100678) 53 | ### "Remote editing" -- sort of 54 | [!["Remote editing" -- sort of](https://asciinema.org/a/100680.png)](https://asciinema.org/a/100680) 55 | ### Relative gf anywhere 56 | [![Relative gf anywhere](https://asciinema.org/a/100681.png)](https://asciinema.org/a/100681) 57 | 58 | ## Getting started 59 | There are various demo.vsh files in vsh/demo/demo*.vsh. If you've just 60 | installed and are looking for how to use the plugin that's a great place to 61 | start. 62 | (demo.vsh includes demo_part2.vsh at the end of the demonstration, and that 63 | includes demo_part3.vsh, so they naturally get executed in turn). 64 | 65 | -------------------------------------------------------------------------------- /autoload/vsh/bind_parser_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import vsh_tell_nvim_bindings as v 3 | 4 | class TestParsing(unittest.TestCase): 5 | ''' 6 | Ensure our parsers work nicely. 7 | ''' 8 | bind_descriptions = [ 9 | 'accept-line can be invoked via "\\C-x", "abc".', 10 | 'yank-last-arg can be invoked via "\\e.", "abc".', 11 | 'undo can be invoked via "\\C-x\\C-u", "abc".', 12 | 'vi-rubout can be invoked via "\\C-\\\\", "abc".', 13 | 'vi-rubout can be invoked via "\\C-\\\\\\"", "abc".', 14 | 'vi-rubout can be invoked via "\\C-\\\\\\"".', 15 | 'vi-rubout can be invoked via "\\C-j".', 16 | ] 17 | def test_find_next_binding(self): 18 | ''' 19 | Make sure we skip all characters left in any binding. 20 | ''' 21 | for arg in self.bind_descriptions[:-2]: 22 | first_quote = arg.find('"') 23 | self.assertEqual( 24 | v.find_next_binding(first_quote + 1, arg), 25 | len(arg) - 5 26 | ) 27 | self.assertEqual( 28 | arg[len(arg) - 5:], 29 | 'abc".' 30 | ) 31 | 32 | for arg in self.bind_descriptions[-2:]: 33 | first_quote = arg.find('"') 34 | self.assertEqual( 35 | v.find_next_binding(first_quote + 1, arg), 36 | None 37 | ) 38 | 39 | def test_read_ctrl_char(self): 40 | ''' 41 | Make sure we can parse a control character. 42 | ''' 43 | ctrl_chars = ['C-@'] 44 | ctrl_chars.extend('C-' + chr(i - 1 + ord('a')) 45 | for i in range(1, 27)) 46 | ctrl_chars.extend(['C-[', 'C-\\\\', 'C-]', 'C-^', 'C-_']) 47 | for i, char in enumerate(ctrl_chars): 48 | self.assertEqual( 49 | v.read_ctrl_char(0, char + '"'), 50 | (chr(i), len(char) - 1) 51 | ) 52 | 53 | 54 | def test_find_command(self): 55 | ''' 56 | See if final output works on our test cases. 57 | ''' 58 | keys = ['\x18', '\x1b.', '\x18\x15', '\x1c', '\x1c"', 59 | '\x1c"', '\x0a'] 60 | for desc, key in zip(self.bind_descriptions, keys): 61 | self.assertEqual( 62 | v.find_command_from_output(desc), 63 | key 64 | ) 65 | 66 | self.assertEqual( 67 | v.find_command_from_output('Not a real keybinding "\\C-j"'), 68 | None 69 | ) 70 | 71 | -------------------------------------------------------------------------------- /autoload/vsh/flush-large-output: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Remove large chunks of output from some VSH text. 4 | 5 | Usually used in a pipe from the text editor. 6 | 7 | ''' 8 | import sys 9 | 10 | if __name__ == '__main__': 11 | prev_cmd = None 12 | for idx, line in enumerate(sys.stdin.readlines()): 13 | if line.startswith('vshcmd: >'): 14 | if prev_cmd is not None and (idx - prev_cmd) > 100: 15 | print('...') 16 | prev_cmd = idx 17 | print(line, end='') 18 | continue 19 | if prev_cmd is None: 20 | continue 21 | if idx - prev_cmd >= 100: 22 | continue 23 | print(line, end='') 24 | -------------------------------------------------------------------------------- /autoload/vsh/gdb.vim: -------------------------------------------------------------------------------- 1 | function vsh#gdb#find_marked_window() 2 | for i in range(1, winnr('$')) 3 | if getwinvar(l:i, 'gdb_view') 4 | return i 5 | endif 6 | endfor 7 | return 0 8 | endfunction 9 | 10 | function vsh#gdb#direct_goto(name) 11 | if bufname(a:name) 12 | return 'buffer' 13 | endif 14 | return 'edit' 15 | endfunction 16 | 17 | function vsh#gdb#gohere(open_method, filename, linenum) 18 | let l:open_cmd = a:open_method 19 | if a:open_method == 'default' 20 | let win = vsh#gdb#find_marked_window() 21 | if win 22 | execute win . ' wincmd w' 23 | endif 24 | let l:open_cmd = vsh#gdb#direct_goto(a:filename) 25 | endif 26 | execute l:open_cmd . ' +' . a:linenum . ' ' . a:filename 27 | execute 'silent! ' . a:linenum . 'foldopen!' 28 | endfunction 29 | 30 | function vsh#gdb#showhere(filename, linenum) 31 | let curwin = winnr() 32 | let marked_win = vsh#gdb#find_marked_window() 33 | let do_alert = v:false 34 | if !marked_win 35 | if curwin != 1 36 | let marked_win = curwin - 1 37 | elseif winnr('$') != curwin 38 | let marked_win = curwin + 1 39 | else 40 | " I do wonder whether opening a second window would be best for 41 | " `showhere`. Will think about it. If it is best then should probably 42 | " make the same change for neovim as well. 43 | " I should really make the neovim integration use the same vimscript 44 | " functions rather than write the same thing twice. 45 | let marked_win = curwin 46 | endif 47 | let l:do_alert = v:true 48 | call setwinvar(marked_win, 'gdb_view', 1) 49 | endif 50 | call vsh#gdb#gohere('default', a:filename, a:linenum) 51 | execute curwin . ' wincmd w' 52 | if l:do_alert 53 | echom 'No marked window, choosing window '.marked_win.' and marking with w:gdb_view for future' 54 | endif 55 | endfunction 56 | 57 | function vsh#gdb#add_mark(filename, linenum, letter) 58 | execute 'badd ' . a:filename 59 | let bufnr = bufnr(a:filename) 60 | let setpos_string = "'" . a:letter 61 | call setpos(setpos_string, [bufnr, a:linenum, 0, 0]) 62 | endfunction 63 | -------------------------------------------------------------------------------- /autoload/vsh/origvim_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import socket 3 | import os 4 | import select 5 | import sys 6 | import signal 7 | import json 8 | 9 | if len(sys.argv) > 1 and sys.argv[1] == 'testing': 10 | def debug_print(*args, **kwargs): 11 | print(*args, **kwargs) 12 | are_testing = True 13 | else: 14 | def debug_print(*args, **kwargs): 15 | # with open('/home/matmal01/temp/vsh-vim-natural/dump-file', 'a') as outfile: 16 | # print(*args, **kwargs, file=outfile) 17 | pass 18 | are_testing = False 19 | 20 | def sock_recv_catch_err(sock): 21 | try: 22 | buf = sock.recv(1024) 23 | except ConnectionResetError: 24 | buf = b'' 25 | return buf 26 | 27 | 28 | def request_vim_connect(vclient): 29 | message = ["call", "vsh#vsh#NewChannel", []] 30 | vclient.send(bytes(json.dumps(message), 'utf8')) 31 | 32 | def close_and_exit(mappings, always): 33 | debug_print('Exiting because of message from vim') 34 | for v in socket_mappings.values(): 35 | v.shutdown(socket.SHUT_RDWR) 36 | v.close() 37 | for s in always_listening: 38 | s.shutdown(socket.SHUT_RDWR) 39 | s.close() 40 | sys.exit() 41 | 42 | def open_new_connection(lsock, vsock, vclient, connect_map): 43 | debug_print('Opening new connection', end='') 44 | tmp_l, _ = lsock.accept() 45 | # From tests it looks like if the client shuts down before we 46 | # make the connection we receive a socket that looks valid but 47 | # returns no data. 48 | # This is a special case, so I think it's fine to create a 49 | # channel in vim, then notice that the listen socket is 50 | # readable, get no data from it, then close that socket and the 51 | # channel in vim. 52 | debug_print('-- Requesting Vim connection', end='') 53 | request_vim_connect(vclient) 54 | debug_print('-- waiting on vim', end='') 55 | tmp_v, _ = vsock.accept() 56 | connect_map[tmp_l.fileno()] = tmp_v 57 | connect_map[tmp_v.fileno()] = tmp_l 58 | debug_print(' -- socket ', (tmp_l.fileno(), tmp_v.fileno())) 59 | 60 | def close_one_connection(lsock, connect_map): 61 | # N.b. this works either way (being passed a vim socket and closing the 62 | # listening socket) despite the naming. 63 | lfileno = lsock.fileno() 64 | vsock = connect_map[lfileno] 65 | vfileno = vsock.fileno() 66 | debug_print('Listener', lfileno, 'has closed, closing vim', vfileno) 67 | vsock.shutdown(socket.SHUT_RDWR) 68 | vsock.close() 69 | debug_print(connect_map.keys()) 70 | del connect_map[lfileno] 71 | del connect_map[vfileno] 72 | debug_print(connect_map.keys()) 73 | # TODO Probably don't need the shutdown given that the 74 | # other side has already gone away. 75 | lsock.shutdown(socket.SHUT_RDWR) 76 | lsock.close() 77 | 78 | # Should get a HUP when the vim process closes, so don't need to worry 79 | # about closing. 80 | # Allow the vim instance to connect to us. 81 | # Socket structure is: 82 | # - vimsock waits for any new connection from vim. 83 | # - listensock waits for new connections from otherprocess. 84 | # - vimclient is connected to vim and is for *this process* sending requests 85 | # to vim. 86 | # - We have a set of sockets in socket_mappings connecting "some process" to 87 | # "some vim channel". Any message on one side gets sent to the other. 88 | # Protocol: 89 | # - No connection from vim on vimsock *unless* requested by sending 90 | # something on vimclient. 91 | # - New connection request on listensock 92 | # => Accept request and obtain listen info 93 | # => send "connect back to me" message on vimclient. 94 | # => vim will attempt to connect to vimsock, wait for and accept. 95 | # => Associate listen info and new vim channel together. 96 | # - Message on a listen socket. 97 | # => Send it to associated vim socket. 98 | # - Close a listen socket. 99 | # => Close associated vim socket. 100 | # - Message on any vim socket. 101 | # => Send to associated listen socket. 102 | # - Close any vim socket. 103 | # => Close associated listen socket. 104 | # - Message on, or Close of vimclient 105 | # => Close and exit. 106 | if __name__ == '__main__': 107 | _, vimport, listenport = sys.argv[1:] 108 | vimsock = socket.fromfd(int(vimport), socket.AF_INET, socket.SOCK_STREAM) 109 | listensock = socket.fromfd(int(listenport), socket.AF_INET, socket.SOCK_STREAM) 110 | vimclient, _ = vimsock.accept() 111 | listensock.setblocking(False) 112 | socket_mappings = {listensock.fileno(): vimsock} 113 | always_listening = [listensock, vimclient] 114 | all_listening = always_listening 115 | # Will always contain `listensock` since we never remove that. 116 | debug_print('Entering listen loop') 117 | while all_listening: 118 | readable, writeable, in_err = select.select( 119 | all_listening, [], all_listening) 120 | debug_print(socket_mappings.keys()) 121 | for err_socket in in_err: 122 | debug_print('ERR --- Do not know what to do with this!!! --- ERR') 123 | close_one_connection(err_socket, socket_mappings) 124 | for this_socket in readable: 125 | debug_print('Handling socket: ', this_socket.fileno()) 126 | if this_socket == listensock: 127 | open_new_connection(this_socket, vimsock, vimclient, socket_mappings) 128 | elif this_socket == vimclient: 129 | # Anything read on vimclient means "close all sockets and shut 130 | # down". Same holds for vimclient having nothing to read (i.e. 131 | # being closed). 132 | close_and_exit(socket_mappings, always_listening) 133 | else: 134 | buf = sock_recv_catch_err(this_socket) 135 | if not buf: 136 | close_one_connection(this_socket, socket_mappings) 137 | else: 138 | # If we didn't read all of the message then we'll get it next 139 | # time around in the loop, other side will wait. 140 | tmp_v = socket_mappings[this_socket.fileno()] 141 | debug_print('Sending :', buf, 'to', tmp_v.fileno()) 142 | tmp_v.send(buf) 143 | debug_print('Finished handling this socket') 144 | all_listening = [x for x in socket_mappings.values() if x != vimsock] 145 | all_listening.extend(always_listening) 146 | debug_print('Exiting') 147 | -------------------------------------------------------------------------------- /autoload/vsh/origvim_server_setup.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Only used for original vim. 3 | 4 | We want something similar to NVIM_LISTEN_ADDRESS. 5 | I.e. we want to have a port just listening around for anything that 6 | subprocesses want to send to us, and execute those messages. 7 | 8 | AFAIK vim does not have such a feature built-in, but it does allow connecting 9 | to something else and executing commands sent on the channel that it opened to 10 | that "something". 11 | 12 | Here we create a middleman that presents the interface of "listening for 13 | commands" to subprocesses started in the shell, and also presents the interface 14 | of "waiting for a vim connection, so we can send commands to it" to vim. 15 | 16 | ''' 17 | import socket 18 | import os 19 | import select 20 | import sys 21 | import signal 22 | import json 23 | import vim 24 | import subprocess as sp 25 | import atexit 26 | 27 | if len(sys.argv) > 1 and sys.argv[1] == 'testing': 28 | def vsh_debug_print(*args, **kwargs): 29 | print(*args, **kwargs) 30 | def vsh_set_var(name, value): 31 | pass 32 | def vsh_get_dir(): 33 | return os.path.dirname(os.path.abspath(__file__)) 34 | are_testing = True 35 | else: 36 | def vsh_debug_print(*args, **kwargs): 37 | # with open('/home/matmal01/temp/vsh-vim-natural/dump-file', 'a') as outfile: 38 | # print(*args, **kwargs, file=outfile) 39 | pass 40 | def vsh_set_var(name, value): 41 | vim.vars[name] = value 42 | def vsh_get_dir(): 43 | return sys.argv[0] 44 | are_testing = False 45 | 46 | def vsh_sock_recv_catch(sock): 47 | try: 48 | buf = sock.recv(1024) 49 | except ConnectionResetError: 50 | buf = b'' 51 | return buf 52 | 53 | def vsh_kill_child(proc): 54 | proc.send_signal(signal.SIGHUP) 55 | 56 | vimsock = socket.socket() 57 | vimsock.bind(('localhost', 0)) 58 | vimsock.listen(8) 59 | listensock = socket.socket() 60 | listensock.bind(('localhost', 0)) 61 | listensock.listen(8) 62 | 63 | vimport = vimsock.getsockname()[1] 64 | listenport = listensock.getsockname()[1] 65 | vsh_debug_print('Vim socket port: ', vimport) 66 | vsh_debug_print('Listen socket port: ', listenport) 67 | vsh_set_var('vsh_origvim_server_addr', 'localhost:' + str(vimport)) 68 | vsh_set_var('vsh_origvim_listen_addr', 'localhost:' + str(listenport)) 69 | 70 | vimsock.set_inheritable(True) 71 | listensock.set_inheritable(True) 72 | scriptdir = vsh_get_dir() 73 | scriptname = os.path.join(scriptdir, 'origvim_server.py') 74 | 75 | # None of stdin, stdout, and stderr are redirected, they are all pointing at 76 | # the same place. Since this script does not read from stdin that's fine, and 77 | # anything that it prints to stdout *should* be seen in the above vim instance, 78 | # so this default is exactly what we want. 79 | child_process = sp.Popen( 80 | [scriptname, 'testing' if are_testing else 'x', 81 | str(vimsock.fileno()), str(listensock.fileno())], 82 | pass_fds=(listensock.fileno(), vimsock.fileno())) 83 | 84 | listensock.close() 85 | vimsock.close() 86 | # Close the child process when vim closes. 87 | atexit.register(vsh_kill_child, child_process) 88 | # Advertise the PID to vim so that vim can send a signal if it wants. 89 | vsh_set_var('vsh_origvim_server_pid', child_process.pid) 90 | del vimsock, listensock, vimport, listenport, scriptdir, scriptname 91 | 92 | # XXX Manual Testing XXX 93 | def vsh_test_new_conn(vlisten, vport): 94 | vsh_debug_print(' Opening new vim connection', end='') 95 | message = vsh_sock_recv_catch(vlisten) 96 | # Not going to worry about split messages in this testing framework. 97 | # Just going to assert we see it. 98 | vsh_debug_print(' -- seeing -- ', message, end='') 99 | assert(b'NewChannel' in message) 100 | newv = socket.socket() 101 | newv.connect(('localhost', vport)) 102 | vsh_debug_print(' -- socket ', newv.fileno()) 103 | return newv 104 | 105 | def vsh_test_run(vport): 106 | ''' 107 | Printing out what vim should be seeing, so I can check things look like 108 | what they should. 109 | 110 | These tests require sending messages to the listening sockets and checking 111 | what you see is what you expect. 112 | 113 | Testing approach is: 114 | $ python3 testing 115 | Then in a python REPL: 116 | >>> import socket 117 | >>> listensock = 118 | >>> def make_new_connection(): 119 | >>> s = socket.socket() 120 | >>> s.connect(('localhost', listensock)) 121 | >>> all_sockets.append(s) 122 | >>> return s 123 | 124 | Then create connections and send messages in that REPL to see what happens. 125 | 126 | ''' 127 | time.sleep(1) 128 | vimmux = socket.socket() 129 | vimmux.connect(('localhost', vport)) 130 | vimlistening = [vimmux] 131 | vsh_debug_print(' Entering connection test loop') 132 | while vimlistening: 133 | readable, writeable, in_err = select.select( 134 | vimlistening, [], vimlistening) 135 | vsh_debug_print(' Another select round') 136 | for vimsock in readable: 137 | vsh_debug_print(' Handling', vimsock.fileno()) 138 | if vimsock == vimmux: 139 | vimlistening.append(vsh_test_new_conn(vimsock, vport)) 140 | else: 141 | data = vsh_sock_recv_catch(vimsock) 142 | if not data: 143 | vimlistening.remove(vimsock) 144 | continue 145 | vsh_debug_print(' Recv data on vim connection:', vimsock.fileno(), data) 146 | if data == b'EXIT': 147 | vimmux.send(b'anything should exit') 148 | else: 149 | ret = data + b' received' 150 | vsh_debug_print(' Sending back: ', ret) 151 | vimsock.send(ret) 152 | 153 | if len(sys.argv) > 1 and sys.argv[1] == 'testing': 154 | import time 155 | vsh_test_run(vimport) 156 | -------------------------------------------------------------------------------- /autoload/vsh/py.vim: -------------------------------------------------------------------------------- 1 | " Python is a bit of a special case language. 2 | " In the REPL, it treats empty lines as terminating the current block, but not 3 | " in a script/file. 4 | " This means that sending the text of a function definition in a file to a 5 | " python REPL doesn't behave as expected. 6 | " Hence we provide special case functions that remove empty lines until the 7 | " end, and add a terminating line to a block if requested. 8 | " 9 | " This does have quite a bit of code duplication from the main vsh.vim file, 10 | " but I refuse to add special cases into the general function just for one 11 | " programing language. 12 | function vsh#py#SendRange(buffer, line1, line2, dedent) 13 | let jobnr = getbufvar(a:buffer, 'vsh_job') 14 | if l:jobnr == '' 15 | echoerr 'Buffer ' . a:buffer . ' has no vsh job running' 16 | return 17 | endif 18 | 19 | if a:line1 > a:line2 20 | echoerr 'vsh#py#SendRange() given end before start' 21 | return 22 | endif 23 | 24 | let first_line = getline(a:line1) 25 | let line1 = a:line1 + 1 26 | 27 | let indent = a:dedent == '!' ? match(first_line, '\S') : 0 28 | " Inclusive, indent does not account for tabs mixing with spaces (i.e. if 29 | " the first line is indented 4 spaces, and the second is indented with one 30 | " tab, we will lose 3 characters of the second line). 31 | " I don't think accounting for this would really be worth it... 32 | " Depends on whether people would want it or not. 33 | 34 | " We've already fetched the text for the first line in this range, so we 35 | " may as well send it outside the loop rather than call getline() again 36 | " unnecessarily. 37 | let Include_line = { linetext -> match(linetext, '^\s*$') == -1 } 38 | if Include_line(first_line[indent:]) 39 | call chansend(l:jobnr, first_line[indent:] . "\n") 40 | endif 41 | if a:line2 >= l:line1 42 | for linenr in range(l:line1, a:line2) 43 | let curline = getline(linenr)[indent:] 44 | if Include_line(curline) 45 | call chansend(l:jobnr, curline . "\n") 46 | endif 47 | endfor 48 | endif 49 | if b:vsh_py_terminate_range 50 | call chansend(l:jobnr, "\n") 51 | endif 52 | endfunction 53 | 54 | function vsh#py#SendThis(selection_type) 55 | if ! has_key(b:, 'vsh_alt_buffer') 56 | echom "Don't know what buffer to send the selection to." 57 | echom 'Set b:vsh_alt_buffer to buffer number, this can be done with [count]vb' 58 | return 59 | endif 60 | call vsh#py#SendRange(b:vsh_alt_buffer, 61 | \ line("'["), line("']"), 62 | \ b:vsh_send_dedent) 63 | endfunction 64 | 65 | function vsh#py#SendOperatorFunc(dedent, terminate_range) 66 | " TODO Any way to do this with a script-local vsh#py#SendThis function? 67 | let b:vsh_py_terminate_range = a:terminate_range 68 | let b:vsh_send_dedent = a:dedent ? '!' : '' 69 | set operatorfunc=vsh#py#SendThis 70 | return 'g@' 71 | endfunction 72 | 73 | function vsh#py#VisualSend(dedent, terminate_range) 74 | if v:count 75 | let b:vsh_alt_buffer = bufname(v:count) 76 | endif 77 | let b:vsh_send_dedent = a:dedent ? '!' : '' 78 | let b:vsh_py_terminate_range = a:terminate_range 79 | let [sbuf, sline, scol, soff] = getpos("'<") 80 | let [ebuf, eline, ecol, eoff] = getpos("'>") 81 | call vsh#py#SendRange(b:vsh_alt_buffer, 82 | \ sline, eline, 83 | \ b:vsh_send_dedent) 84 | endfunction 85 | 86 | -------------------------------------------------------------------------------- /autoload/vsh/vsh.py: -------------------------------------------------------------------------------- 1 | import os 2 | import vim 3 | # Neovim legacy vim module does not have `bindeval`. 4 | # Vim `eval` returns a string rather than an integer. 5 | if vim.eval("has('nvim')") != '0': 6 | import pynvim 7 | vsh_FailedToInsert = pynvim.api.nvim.NvimError 8 | vsh_FuncStore = vim.funcs 9 | def vsh_get_mark(vsh_buf, markchar): 10 | return vsh_buf.mark(markchar) 11 | def vsh_insert_text(data, insert_buf): 12 | return vsh_insert_text_1(data, insert_buf) 13 | def vsh_append(data, linenum, insert_buf): 14 | return insert_buf.append(data, linenum) 15 | else: 16 | import collections 17 | vsh_FuncStoreType = collections.namedtuple('vsh_FuncStoreType', 18 | ['match', 'setpos', 19 | 'appendbufline', 'line']) 20 | vsh_FuncStore = vsh_FuncStoreType(vim.Function('match'), 21 | vim.Function('setpos'), 22 | vim.Function('appendbufline'), 23 | vim.Function('line')) 24 | def vsh_insert_text(data, insert_buf): 25 | # I honestly don't know whether getting input from a pty gives me a 26 | # string with newlines in it or calls the callback for each string. 27 | # Will have this assertion and run it a bunch to see what comes up. 28 | assert ('\x00' not in data) 29 | data = data.split('\n') 30 | return vsh_insert_text_1(data, insert_buf) 31 | def vsh_get_mark(vsh_buf, markchar): 32 | tmp = vsh_buf.mark(markchar) 33 | # Original vim returns `None` when a mark does not exist, convert to 34 | # what neovim sees. 35 | # We also have a difference that original vim returns a list while 36 | # neovim returns a list. Doesn't matter, but worth mentioning for a 37 | # reminder. 38 | if tmp is None: 39 | return 0, 0 40 | return tmp 41 | def vsh_append(data, linenum, insert_buf): 42 | vsh_FuncStore.appendbufline(insert_buf.number, linenum, data) 43 | 44 | def _vsh_match_line(line, prompt): 45 | null_loc = line.find('\x00') 46 | if null_loc != -1: 47 | line = line[:null_loc] 48 | # Use vim's match() so that vim regular expressions work. 49 | return vsh_FuncStore.match(line, prompt) != -1 50 | 51 | def vsh_outputlen(buf, curprompt, interactive_near_prompt=False): 52 | ''' 53 | Given a buffer and the Vim line number of a prompt, return number of output 54 | lines from that prompt in the current buffer. 55 | 56 | ''' 57 | # If on last line of buffer the below logic breaks down. Moreover, we know 58 | # the length of the output to be 0, so return early. 59 | if len(buf) <= curprompt: 60 | return 0 61 | 62 | prompt = vim.eval('vsh#vsh#SplitMarker({})'.format(buf.number)) 63 | if not prompt: 64 | return 0 65 | 66 | # curprompt represents the first line of output. 67 | found_prompt = False 68 | count = 0 69 | if interactive_near_prompt: 70 | # Search *back* to line `curprompt`. 71 | # Then search *forwards* for a line with `vsh#vsh#SplitMarker()` at the 72 | # start. 73 | # - Use all relevant flags to avoid unnecessary state change.. 74 | # - Set `count` from the returned number (similarly for 75 | # `found_prompt` -- if returned number is zero prompt not found). 76 | 77 | # Assert the restrictions of calling this function with 78 | # `interactive_near_prompt` set. Special casing is certainly worth it 79 | # due to how much drastically faster this is. 80 | # N.b. the case where the search fails and `start_line` is zero should 81 | # never happen since we should only ever call this function with the 82 | # cursor on or after a prompt. 83 | assert (buf == vim.current.buffer) 84 | start_line = int(vim.eval('vsh#vsh#VshSegmentStart()')) 85 | assert (start_line == curprompt) 86 | end_line = int(vim.eval('vsh#vsh#VshSegmentEnd()')) 87 | found_prompt = (end_line != 0) 88 | end_line = (vsh_FuncStore.line('$') if not found_prompt else end_line) 89 | # Subtract one 90 | count = end_line - start_line - 1 91 | else: 92 | # Even just asking for the remaining buffer is slow when in a very 93 | # large buffer (i.e. in a large enough buffer this is a large 94 | # bottleneck even if I'm working on small outputs -- as long as the 95 | # remaining buffer is large enough). This is a real performance 96 | # bottleneck. Would really like to perform a search, but currently 97 | # don't know how to search "some buffer" rather than searching in "the 98 | # current buffer". 99 | # 100 | # This is why I have two clauses here, one to be used by 101 | # `vsh_recalculate_input_position` (and which has to be general) and 102 | # another to be used by `vsh_clear_output` (which does not have to be 103 | # general and could use a simple search). 104 | # 105 | # That at least avoids the majority of obvious performance problems 106 | # that happen when the user is interacting with this buffer. 107 | # 108 | # If I really care about having just one function then I could start to 109 | # chunk the transfer of the buffer -- though this would not help with 110 | # a good number of the bottlenecks it would at least mean that we 111 | # should be fine when acting on an output that is small but happens to 112 | # be above some large output in the same buffer. 113 | for (count, line) in enumerate(buf[curprompt:]): 114 | # In recent versions of neovim the NULL byte in output can give us 115 | # some problems. https://github.com/neovim/neovim/issues/29855 116 | # Just remove it in the search and say that NULL bytes in your 117 | # prompt are unsupported (which seems reasonable to me). 118 | if _vsh_match_line(line, prompt): 119 | found_prompt = True 120 | break 121 | 122 | # We have either broken out of the loop at the next prompt, or reached the 123 | # end of the buffer. Hence the output length is either count or count+1. 124 | if not found_prompt: 125 | return count + 1 126 | 127 | return count 128 | 129 | 130 | def vsh_recalculate_input_position(vsh_buf, insert_mark): 131 | ''' 132 | Our mark of where to insert text has been lost, see if we can recalculate 133 | it from our mark of which command was last executed. 134 | 135 | If that mark is also lost then we just give up. 136 | 137 | ''' 138 | prompt_mark = vim.vars.get('vsh_prompt_mark', 'p') 139 | prompt_line, _ = vsh_get_mark(vsh_buf, prompt_mark) 140 | if prompt_line == 0: 141 | return False 142 | 143 | insert_line = prompt_line + vsh_outputlen(vsh_buf, prompt_line) 144 | # The previous mark being deleted means the last line of the last output 145 | # was also deleted. Hence the current output should be on a different line 146 | # to what's there at the moment. 147 | vsh_append('', insert_line, vsh_buf) 148 | vsh_FuncStore.setpos("'" + insert_mark, [vsh_buf.number, insert_line + 1, 0]) 149 | return True 150 | 151 | 152 | def vsh_insert_helper(data, vsh_buf): 153 | '''Do main work of inserting text. 154 | 155 | This function does all the work of inserting output from a shell command 156 | and setting relevant marks assuming there are no newlines in the output. 157 | There is only newlines in the output we're given if the shell command 158 | output contains NULL bytes. 159 | 160 | If this happens, an error is raised to the caller. 161 | 162 | ''' 163 | # Default to inserting text at end of file if input mark doesn't exist. 164 | insert_mark = vim.vars.get('vsh_insert_mark', 'd') 165 | insert_line, _ = vsh_get_mark(vsh_buf, insert_mark) 166 | if insert_line == 0: 167 | # Attempt to recalculate the input position from knowledge of which 168 | # prompt was last executed -- this just gives us a little extra 169 | # robustness against the user removing text with our marks in them. 170 | if vsh_recalculate_input_position(vsh_buf, insert_mark): 171 | return vsh_insert_helper(data, vsh_buf) 172 | 173 | # Default to inserting text at end of file if neither of our reference 174 | # marks exist. 175 | # Use the total length of the buffer because insert_line is a Vim 176 | # line number not a python buffer index. 177 | insert_line = len(vsh_buf) 178 | 179 | # TODO It seems worth keeping some buffer-local variable around to indicate 180 | # whether we've just started a command or not. We could add a newline when 181 | # we've not seen any output since the last "enter". This would avoid the 182 | # possibility of `vshcmd: > ` coming from output being treated as a prompt 183 | # and hence some lines getting messed up. This seems to be working for 184 | # emacs, and it should be more robust. 185 | # 186 | # If the insert position is not at the end of a command prompt, assume 187 | # we have already put some of the output from this command into the buffer. 188 | # In that case, we want to allow for flushing of output in the middle of a 189 | # line by joining the next piece of text with the previous line. 190 | # If the last line included a trailing newline, then the last element in 191 | # data would have been '' so this still works. 192 | prompt = vim.eval('vsh#vsh#SplitMarker({})'.format(vsh_buf.number)) 193 | insert_line_text = vsh_buf[insert_line - 1] 194 | # In recent versions of neovim the NULL byte in output can give us some 195 | # problems. https://github.com/neovim/neovim/issues/29855 196 | # Just remove it in the search and say that NULL bytes in your prompt are 197 | # unsupported (which seems reasonable to me). 198 | null_loc = insert_line_text.find('\x00') 199 | match_text = insert_line_text[:null_loc] if null_loc != -1 else insert_line_text 200 | # Use vsh_FuncStore.match() so vim regular expressions in 'prompt' work. 201 | if vsh_FuncStore.match(match_text, prompt) == -1: 202 | firstline = data.pop(0) 203 | try: 204 | vsh_buf[insert_line - 1] = insert_line_text + firstline 205 | except: 206 | # Shouldn't happen 207 | data.insert(0, firstline) 208 | raise 209 | 210 | # Text may be modified between the times that output is flushed. 211 | # We have to hope that our marks are not removed between successive calls 212 | # of this function otherwise output starts being appended to the file. 213 | # 214 | # There are three options I see as useful in increasing order of likelyhood 215 | # that the line will be removed, they are: 216 | # Mark the current command prompt 217 | # Mark the last end of output 218 | # Mark the next command line 219 | # 220 | # Marking the last end of output or the next command line means we don't 221 | # have to count the output lines each time more text is added, which I 222 | # have seen helps performance for commands with a lot of output. 223 | # 224 | # As a backup, I also mark the current command prompt, so that I can 225 | # recalculate the position of the last line if needs be. 226 | if data: 227 | vsh_append(data, insert_line, vsh_buf) 228 | # This should fix issue #14 as soon as neovim issue #5713 is fixed 229 | vsh_FuncStore.setpos("'" + insert_mark, 230 | [vsh_buf.number, len(data) + insert_line, 0]) 231 | 232 | 233 | def vsh_insert_text_1(data, insert_buf): 234 | ''' 235 | Insert text into a vsh buffer in the correct place. 236 | Don't modify the user state and don't interrupt their workflow. 237 | 238 | ''' 239 | try: 240 | vsh_buf = vim.buffers[int(insert_buf)] 241 | except KeyError: 242 | vim.command('echomsg "Vsh text recieved for invalid buffer"') 243 | return 244 | 245 | # As @bfredl mentioned in #neovim, jobstart() process output is a stream of 246 | # bytes not a list of lines, it just looks like a list of lines because of 247 | # how they're represented in vimL. 248 | # Hence we have to manually remove extra '\r' characters from our input 249 | # (i.e. powershells output). 250 | # Unfortunately, some commands (e.g. `git`) don't end their lines with 251 | # '\r\n' when running in powershell, instead they end their lines with 252 | # '\n'. 253 | # This means we can't tell for certain whether a lone '\n' was meant to be 254 | # a character in the middle of a line or a line ending. 255 | # Empirically I've seen '\n' meant as a line ending more often than a 256 | # character in the middle of a line, so that's always our guess. 257 | if vim.eval("has('win32')") == '1': 258 | data = [val[:-1] if (len(val) > 0 and val[-1] == '\r') else val 259 | for val in data] 260 | # Don't print out the starting prompt of the shell. 261 | # This is not a problem with Windows Powershell. 262 | elif 'vsh_initialised' not in vsh_buf.vars or \ 263 | not vsh_buf.vars['vsh_initialised']: 264 | vsh_buf.vars['vsh_initialised'] = 1 265 | # TODO Find a better way to check this is just the starting prompt of 266 | # the shell. This is brittle. 267 | if len(data) == 1: 268 | return 269 | 270 | # # If we're ever given empty output I'll uncomment this so the pop() 271 | # # doesn't raise an exception. 272 | # # I don't think it's possible, so I'll leave it commented for now. 273 | # if not data: 274 | # return 275 | 276 | try: 277 | vsh_insert_helper(data, vsh_buf) 278 | except vsh_FailedToInsert: 279 | # If data from the subshell contains NULL characters, then neovim 280 | # replaces these with '\n' characters. 281 | # This is rarely the case, so try to go without first, if needed, then 282 | # go over all lines in the output and change the characters back. 283 | # Different pynvim versions have different arguments, so it's difficult 284 | # to tell whether the problem is the above one based on checking the 285 | # arguments. 286 | # There may be nice way to find the above out, but I'm just going to 287 | # guess that the problem is newlines and try again. 288 | # If that wasn't the problem then we just re-raise the error anyway. 289 | vsh_insert_helper([val.replace('\n', '\x00') for val in data], 290 | vsh_buf) 291 | 292 | 293 | def vsh_clear_output(curline): 294 | '''Remove all output from a previous command.''' 295 | outputlen = vsh_outputlen(vim.current.buffer, curline, True) 296 | vim.current.buffer[curline:curline + outputlen] = [] 297 | 298 | 299 | def vsh_find_cwd(bash_pid): 300 | ''' 301 | Find the cwd of the foreground process group in vsh_buf. 302 | 303 | This is the process group most likely to be printing to stdout, and hence 304 | most likely to have printed relative path names that the user wants to work 305 | with. 306 | 307 | ''' 308 | # See man proc(5) for what's happening here. 309 | with open('/proc/{}/stat'.format(bash_pid), 'rb') as infile: 310 | status_data = infile.read() 311 | foreground_pid = status_data.split()[7].decode('utf-8') 312 | try: 313 | return os.path.realpath('/proc/{}/cwd'.format(foreground_pid)) 314 | except PermissionError: 315 | # Probably done su/sudo -- can't do anything here, fall back to 316 | # original bash process. 317 | return os.path.realpath('/proc/{}/cwd'.format(bash_pid)) 318 | -------------------------------------------------------------------------------- /autoload/vsh/vsh_editor_prog: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | 6 | if os.getenv('NVIM') or os.getenv('NVIM_LISTEN_ADDRESS'): 7 | import pynvim 8 | nvim_socket_path = os.getenv('NVIM') 9 | if not nvim_socket_path: 10 | nvim_socket_path = os.getenv('NVIM_LISTEN_ADDRESS') 11 | nvim = pynvim.attach('socket', path=nvim_socket_path) 12 | 13 | filenames = [os.path.abspath(arg) for arg in sys.argv[1:]] 14 | nvim.funcs.__getattr__('vsh#vsh#EditFiles')(filenames) 15 | def restore(): 16 | nvim.funcs.__getattr__('vsh#vsh#RestoreArgs')() 17 | elif os.getenv('VSH_VIM_LISTEN_ADDRESS'): 18 | import socket 19 | import json 20 | import re 21 | origvim_socket_addr = os.getenv('VSH_VIM_LISTEN_ADDRESS') 22 | assert origvim_socket_addr is not None 23 | m = re.match(r'localhost:(\d+)', origvim_socket_addr) 24 | assert m 25 | sock = socket.socket() 26 | sock.connect(('localhost', int(m.groups()[0]))) 27 | 28 | filenames = [os.path.abspath(arg) for arg in sys.argv[1:]] 29 | message = ["call", "vsh#vsh#EditFiles", [filenames]] 30 | message_str = json.dumps(message) 31 | sock.send(message_str.encode('utf8')) 32 | def restore(): 33 | message = ["call", "vsh#vsh#RestoreArgs", []] 34 | message = json.dumps(message) 35 | sock.send(message.encode('utf8')) 36 | else: 37 | print('No upper vsh process to communicate with!', file=sys.stderr) 38 | sys.exit(1) 39 | 40 | 41 | # Wait until get told to finish -- with ^D is "complete", with ^C is not. 42 | # Exiting with non-zero status, tells the process calling $EDITOR that the 43 | # files were not edited sucessfully. 44 | ret = None 45 | while True: 46 | try: 47 | input('Press C-d for "successful" edit, C-c otherwise') 48 | except EOFError: 49 | ret = 0 50 | restore() 51 | except KeyboardInterrupt: 52 | ret = 1 53 | restore() 54 | else: 55 | continue 56 | break 57 | 58 | sys.exit(ret) 59 | -------------------------------------------------------------------------------- /autoload/vsh/vsh_inputrc_snippet: -------------------------------------------------------------------------------- 1 | # This is a partial inputrc that we used to generate an inputrc for the user 2 | # when starting a vsh shell. 3 | $if term=dumb 4 | set show-all-if-ambiguous on 5 | # Disable querying when I request `possible-completions` especially inside 6 | # VSH (putting a bunch of text on the screen is not a problem when it's so 7 | # easily removed, and this would always let us use vim-completions to find 8 | # what we wanted. Mentioning VSH since that's when I tend to have TERM=dumb. 9 | set completion-query-items -1 10 | # Do not paginate completions when there are a lot of options. Similar to 11 | # above, this is the best option when in VSH since we are not directly 12 | # interacting with readline but rather bringing in the list of completions to 13 | # the current vim buffer. 14 | set page-completions off 15 | # Don't want special handling of tab character. This will *look* like 16 | # a tab character getting inserted when in `vsh`, so it should *act* like 17 | # that in the underlying terminal. 18 | "\t":tab-insert 19 | $endif 20 | -------------------------------------------------------------------------------- /autoload/vsh/vsh_man_pager: -------------------------------------------------------------------------------- 1 | ../../vsh-mode-1.0/vsh_man_pager -------------------------------------------------------------------------------- /autoload/vsh/vsh_shell_start: -------------------------------------------------------------------------------- 1 | ../../vsh-mode-1.0/vsh_shell_start -------------------------------------------------------------------------------- /autoload/vsh/vsh_tell_editor_bindings.py: -------------------------------------------------------------------------------- 1 | ../../vsh-mode-1.0/vsh_tell_editor_bindings.py -------------------------------------------------------------------------------- /demo/demo-gdb-integration.vsh: -------------------------------------------------------------------------------- 1 | vshcmd: > gcc -g3 tree.c -o tree 2 | demo [09:47:11] $ 3 | vshcmd: > gdb ./tree 4 | vshcmd: > source ~/.vim/bundle/vsh/integration/with_gdb.py 5 | Reading symbols from ./tree... 6 | (gdb) (gdb) 7 | vshcmd: > start 10 8 | vshcmd: > showhere 9 | 10 | vshcmd: > break free_tree 11 | vshcmd: > cont 12 | Breakpoint 2 at 0x55555555534d: file tree.c, line 54. 13 | (gdb) Continuing. 14 | 15 | Breakpoint 2, free_tree (root=0x5555555592a0) at tree.c:54 16 | 54 if (root) { 17 | (gdb) 18 | vshcmd: > showhere 19 | (gdb) 20 | vshcmd: > next 21 | vshcmd: > gohere 22 | 55 free_tree(root->children[larger]); 23 | (gdb) (gdb) 24 | vshcmd: > bt 25 | #0 free_tree (root=0x5555555592a0) at tree.c:55 26 | #1 0x00005555555554be in main (argc=2, argv=0x7fffffffe0d8) at tree.c:93 27 | (gdb) 28 | vshcmd: > # Below is not part of this plugin (just a small function I have locally) 29 | vshcmd: > # vimcmd: delmarks A B 30 | vshcmd: > mark-stack 31 | (gdb) 32 | vshcmd: > quit 33 | vshcmd: > y 34 | A debugging session is active. 35 | 36 | Inferior 1 [process 1244087] will be killed. 37 | 38 | Quit anyway? (y or n) demo [09:51:09] $ 39 | -------------------------------------------------------------------------------- /demo/demo.py: -------------------------------------------------------------------------------- 1 | def test_function(): 2 | print('Hello world') 3 | 4 | # This way, the information of exploratory sessions in terminals doesn't have 5 | # to be copied back into a file to work with later. 6 | # There's an operator on vs by default that works on text objects you 7 | # want to work with. 8 | -------------------------------------------------------------------------------- /demo/demo.txt: -------------------------------------------------------------------------------- 1 | hello everyone 2 | test file 3 | -------------------------------------------------------------------------------- /demo/demo.vsh: -------------------------------------------------------------------------------- 1 | vshcmd: > # Hello there, this is a demo of the vsh plugin. 2 | vshcmd: > # Any lines beginning with 'vshcmd: >' are treated as commands. 3 | vshcmd: > # Lines like these that start with 'vshcmd: > #' are comments, they won't execute. 4 | vshcmd: > # We start a pseudo-terminal running your 'shell' (as defined in vim 5 | vshcmd: > # and can be seen with `:set shell?`), and directly attach to it. 6 | vshcmd: > # 7 | vshcmd: > # You are expected to run commands In normal mode. You can move down 8 | vshcmd: > # a prompt with 'CTRL-N' and up a prompt with 'CTRL-P'. 9 | vshcmd: > # Command lines are executed with '' (i.e. Enter/Return). You can 10 | vshcmd: > # run any command line anywhere in the file at any time. 11 | vshcmd: > pwd 12 | vshcmd: > cd 13 | vshcmd: > !p 14 | vshcmd: > echo 'Hello world' 15 | vshcmd: > # You can treat this as a somewhat limited terminal emulator, 16 | vshcmd: > # inserting text at a prompt, executing it with , and 17 | vshcmd: > # repeating on the new prompt that is inserted. 18 | vshcmd: > # Please try something below. 19 | vshcmd: > 20 | vshcmd: > # 'n' in normal mode starts a new prompt under the 21 | vshcmd: > # output of the current command, and leaves you in insert mode just 22 | vshcmd: > # after it. 23 | vshcmd: > # 24 | vshcmd: > # If you don't want continuation prompts messing up your output, you 25 | vshcmd: > # can select a few lines and send them all at once with '' 26 | vshcmd: > myfun () { 27 | vshcmd: > echo 'Hello World' 28 | vshcmd: > } 29 | vshcmd: > myfun 30 | vshcmd: > # The command ':Vrerun' would also work. 31 | vshcmd: > # 32 | vshcmd: > # Quickly, before I get on to the more useful applications, I'll 33 | vshcmd: > # mention a few handy keybindings. 34 | vshcmd: > # ^ and I are remapped in vsh buffers so they act differently on 35 | vshcmd: > # command lines. In that case they go to the start of the command 36 | vshcmd: > # instead of the start of the line. 37 | vshcmd: > # The text objects 'ic' and 'ac' act on the command after a prompt. 38 | vshcmd: > # 'ac' includes any extra whitespace between the prompt and the 39 | vshcmd: > # command, 'ic' does not. 40 | vshcmd: > # 41 | vshcmd: > # The more powerful applications come from the fact this is in a 42 | vshcmd: > # vim buffer, alongside the fact that commands and outputs are saved 43 | vshcmd: > # in a file for re-running later. 44 | vshcmd: > # 45 | vshcmd: > # We can grep other vsh files, and use their commands interactively, 46 | vshcmd: > # so storing a few clever lines in a file can act as a special 47 | vshcmd: > # history buffer. 48 | vshcmd: > grep '[>] pwd' demo_stored_lines.vsh 49 | vshcmd: > # Run the below for the the next part of the tutorial. 50 | vshcmd: > cat demo_part2.vsh 51 | -------------------------------------------------------------------------------- /demo/demo_part2.vsh: -------------------------------------------------------------------------------- 1 | vshcmd: > # We can munge the output of commands after they have been run with 2 | vshcmd: > # all the power of vim. 3 | vshcmd: > # i.e. you realise after printing something out that you should have 4 | vshcmd: > # piped it through grep. 5 | vshcmd: > # Along with the text objects described above, there is a shortcut to 6 | vshcmd: > # enter the range in the command line, so 'od' would 7 | vshcmd: > # do the same as 'dio'. 8 | vshcmd: > # cat ../autoload/vsh/vsh_shell_start 9 | vshcmd: > # ov/export/d 10 | vshcmd: > # kdio 11 | vshcmd: > cat ../autoload/vsh/vsh_shell_start 12 | vshcmd: > # Because this is directly connected to a pseudo terminal, we can 13 | vshcmd: > # query bash for completions by sending the readline binding for 14 | vshcmd: > # 'possible-completions' (by default this is done with the vim 15 | vshcmd: > # shortcut 'l' in normal mode, or '' in insert 16 | vshcmd: > # mode. 17 | vshcmd: > # read will show completions for "read", and since this is a vim 18 | vshcmd: > # buffer, you can then select the completions with plain vim completion 19 | vshcmd: > # '' and '' 20 | vshcmd: > # Use `` just after the below to see this behaviour. 21 | vshcmd: > read 22 | vshcmd: > # N.b. if that doesn't work, then hopefully the commands in 23 | vshcmd: > # `./setup-env.vsh` should help you figure out why. 24 | vshcmd: > # You can easily run other programs like gdb 25 | vshcmd: > gdb -q 26 | vshcmd: > # Though sometimes you need some config differences 27 | vshcmd: > # (I have this in my gdb config predicated on $TERM='dumb' so this 28 | vshcmd: > # happens automatically. I use `python` in order to do this since I 29 | vshcmd: > # don't know how to check for environment variables in gdb-commands). 30 | vshcmd: > set pagination off 31 | vshcmd: > # Being able to search through input is handy here too. 32 | vshcmd: > # Seeing as it's just text in a vim buffer, you can even filter the 33 | vshcmd: > # output through a shell command or with a vim command, 34 | vshcmd: > # o! cut -d' ' -f1 35 | vshcmd: > # u 36 | vshcmd: > # o s/\S*\zs\s.*// 37 | vshcmd: > # dio 38 | vshcmd: > apropos e 39 | vshcmd: > # I quite often accidentaly end up with way too much text in gdb 40 | vshcmd: > # when looking around a command, and I end up losing the 41 | vshcmd: > # information I found above. 42 | vshcmd: > # Vsh is useful in that manner because the normal way of using it 43 | vshcmd: > # means that you replace the output of a command until you find what 44 | vshcmd: > # you want. 45 | vshcmd: > # apropos variable 46 | vshcmd: > # Then edit the command to 47 | vshcmd: > # help info variable 48 | vshcmd: > # u 49 | vshcmd: > # Then edit the command to 50 | vshcmd: > # help set variable 51 | vshcmd: > # u 52 | vshcmd: > apropos variable 53 | vshcmd: > # The control keys (C-c C-d C-z etc) can be sent with the keybinding 54 | vshcmd: > # 'c', press what type of control key you want to send 55 | vshcmd: > # after that. 56 | vshcmd: > # The output from such control characters will be printed where the 57 | vshcmd: > # last command output was printed (so likely just above this block of 58 | vshcmd: > # comments). 59 | vshcmd: > # cd 60 | vshcmd: > cat demo_part3.vsh 61 | -------------------------------------------------------------------------------- /demo/demo_part3.vsh: -------------------------------------------------------------------------------- 1 | vshcmd: > # Vsh is also handy to use with interpreters, as you don't have to 2 | vshcmd: > # copy and paste things all over the place. 3 | vshcmd: > # There is a global command ':VshSend' that takes a range and a 4 | vshcmd: > # buffer name (the buffer name of this file is: demo.vsh). 5 | vshcmd: > # VshSend sends the lines specified in its range to the terminal 6 | vshcmd: > # attached to the buffer given. 7 | vshcmd: > python 8 | vshcmd: > # Here, open demo.py (Using `gf` on the filename should work). 9 | vshcmd: > # You specify the VSH buffer that you want to send to with 10 | vshcmd: > # vb 11 | vshcmd: > # The number for this buffer can be seen from inside vim. 12 | vshcmd: > # :echo bufnr('%') 13 | vshcmd: > # but it is also in the environment when VSH starts. 14 | vshcmd: > import os 15 | vshcmd: > os.getenv('VSH_VIM_BUFFER') 16 | vshcmd: > # After having set the "buffer to send to" with accordingly, send the 17 | vshcmd: > # definition of test_function() over using the global mapping 18 | vshcmd: > # vsap on the `test_function` definition. 19 | vshcmd: > # To dissect the above, that is running the command defined by 20 | vshcmd: > # `vs` on the range defined by `ap`. Other text objects can 21 | vshcmd: > # be used. 22 | vshcmd: > test_function() 23 | vshcmd: > # Vim's editing capabilities come in handy here too (for searching 24 | vshcmd: > # outputs etc). 25 | vshcmd: > # dir(sys) 26 | vshcmd: > # gqq 27 | vshcmd: > # ?arg 28 | vshcmd: > import sys 29 | vshcmd: > dir(sys) 30 | vshcmd: > # VSH also provides a slightly nicer interface to building up 31 | vshcmd: > # functions in the REPL for later conversion to a script (or possibly 32 | vshcmd: > # just left in the VSH file for later use). 33 | vshcmd: > # ix 34 | vshcmd: > # N.b. the python REPL is a little strange since it requires a 35 | vshcmd: > # double-newline after an indentation to terminate the function 36 | vshcmd: > # definition (and similar for loops). 37 | vshcmd: > # I tend to handle that by putting a literal C-m at the end of the 38 | vshcmd: > # last line in the function, but it could also be done with an empty 39 | vshcmd: > # vshprompt. The second choice is slightly tricky since you need at 40 | vshcmd: > # least one space to define the line as a command prompt, and 41 | vshcmd: > # sometimes it can be confusing if that whitespace goes away. 42 | vshcmd: > def test_function(): 43 | vshcmd: > print('This is a test function') 44 | vshcmd: > print('We defined it in the REPL using vim text editing') 45 | vshcmd: > test_function() 46 | vshcmd: > def test_function2(): 47 | vshcmd: > print('Test 2 Test 2') 48 | vshcmd: > print('Test 2 Test 2 Test 2') 49 | vshcmd: > 50 | vshcmd: > test_function2() 51 | vshcmd: > # You can save interesting output with 's', and 52 | vshcmd: > # activate it again with 'a'. 53 | vshcmd: > exit() 54 | vshcmd: > # You can edit files a few ways, $EDITOR is set up so that things 55 | vshcmd: > # like `git commit` work, though you have to tell the file when 56 | vshcmd: > # you're done. 57 | vshcmd: > # Reminder: when EDITOR requests C-d for successful edit, to send 58 | vshcmd: > # such a control character to the underlying terminal you need to use 59 | vshcmd: > # the mapping 'cd'. 60 | vshcmd: > $EDITOR demo.txt 61 | vshcmd: > echo $EDITOR 62 | vshcmd: > # But the 'gf', 'gF', etc keys are all mapped to use the current 63 | vshcmd: > # foreground process's working directory. 64 | vshcmd: > cd ../ 65 | vshcmd: > ls 66 | vshcmd: > # This works when running a REPL in another directory too. 67 | vshcmd: > python 68 | vshcmd: > import os; os.chdir(os.path.expanduser('~/.config/nvim/bundle/vsh/autoload/vsh')) 69 | vshcmd: > os.listdir('.') 70 | vshcmd: > # You can even do remote editing!!! 71 | vshcmd: > # ... 72 | vshcmd: > # ... 73 | vshcmd: > # well ... 74 | vshcmd: > # sort of 75 | vshcmd: > # cat demo.txt 76 | vshcmd: > # -- modify the text there 77 | vshcmd: > # -- Add `cat << EOF > demo.txt` as a line before the text and `EOF` 78 | vshcmd: > # as a line after the text. 79 | vshcmd: > # viovs 80 | vshcmd: > cat demo.txt 81 | vshcmd: > printf "Thank you for trying VSH please leave feedback, whether \033[0;32mgood\033[0m or \033[0;31mbad\033[0m\n" 82 | Thank you for trying VSH please leave feedback, whether good or bad 83 | demo [22:07:14] $ 84 | -------------------------------------------------------------------------------- /demo/demo_stored_lines.vsh: -------------------------------------------------------------------------------- 1 | 2 | vshcmd: > pwd 3 | -------------------------------------------------------------------------------- /demo/setup-env.vsh: -------------------------------------------------------------------------------- 1 | vshcmd: > # If TERM is not `dumb` then: 2 | vshcmd: > # 1) Some programs may assume they can do clever things with the 3 | vshcmd: > # terminal that will not be seen by VSH. 4 | vshcmd: > # 2) Some programs will give coloured output which will inhibit 5 | vshcmd: > # copy-pasting and text searches inside VSH (e.g. ls and grep). 6 | vshcmd: > # 3) We no longer have an environment variable which is maintained 7 | vshcmd: > # across an SSH connection on which to condition our 8 | vshcmd: > # configuration of things like `pagination` in bash and gdb 9 | vshcmd: > # (most environment variables are cleared when connecting over 10 | vshcmd: > # SSH). 11 | vshcmd: > # 12 | vshcmd: > # N.b. if you use a shell different than `bash` the below `grep` 13 | vshcmd: > # commands are not likely to help. Hopefully you could convert the 14 | vshcmd: > # logical steps to work on the shell you use. 15 | vshcmd: > if [[ "$TERM" != "dumb" ]]; then 16 | vshcmd: > echo '$TERM is not set to "dumb" -- this is set before starting VSH, so it is likely unset during your bash login' 17 | vshcmd: > echo 'Recommend searching looking through your bashrc for anything setting TERM' 18 | vshcmd: > echo 'Grepping for likely lines in bashrc.' 19 | vshcmd: > if [[ -f ~/.bashrc ]]; then 20 | vshcmd: > grep -Hn 'TERM' ~/.bashrc 21 | vshcmd: > fi 22 | vshcmd: > fi 23 | vshcmd: > 24 | 25 | vshcmd: > # Ensuring that GDB turns pagination off when in a dumb terminal. 26 | vshcmd: > # I only know how to check environment variables for the GDB process 27 | vshcmd: > # via `python`. If your GDB does not have python enabled the below 28 | vshcmd: > # will fail. 29 | vshcmd: > gdb -q 30 | vshcmd: > pi 31 | vshcmd: > if gdb.convenience_variable('pagination') is not None: 32 | vshcmd: > print('Pagination is on. Suggest turning it off when in TERM=dumb in gdbinit.') 33 | vshcmd: > print('This has to be done with python.') 34 | vshcmd: > print('Something like the below added to your gdbinit file.') 35 | vshcmd: > # TODO 36 | vshcmd: > # if ~/.config/gdb/gdbinit exists 37 | vshcmd: > # else if ~/.gdbinit exists. 38 | vshcmd: > # N.b. not 100% sure what the load format is here. 39 | vshcmd: > # IIRC I stumbled across this some time earlier. 40 | vshcmd: > with open(os.path.expanduser('~/.gdbinit'), 'a') as outfile: 41 | vshcmd: > print("python", file=outfile) 42 | vshcmd: > print("import os", file=outfile) 43 | vshcmd: > print("if os.getenv('TERM') == 'dumb':", file=outfile) 44 | vshcmd: > print(" gdb.execute('set pagination off')", file=outfile) 45 | vshcmd: > print(" gdb.execute('set width 100')", file=outfile) 46 | vshcmd: > print("end", file=outfile) 47 | vshcmd: > exit() 48 | demo [22:23:52] $ 49 | -------------------------------------------------------------------------------- /demo/tree.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | /* 8 | * Basic binary tree structure written solely to demo my gdb walkers. 9 | */ 10 | 11 | typedef struct node { 12 | struct node* children[2]; 13 | int32_t datum; 14 | } node_t; 15 | 16 | typedef enum direction_t { 17 | smaller = 0, 18 | larger = 1 19 | } direction; 20 | 21 | 22 | bool insert_entry(node_t *root, int32_t datum) 23 | { 24 | if (root == NULL) { 25 | fprintf(stderr, "insert_entry(): Require non-NULL 'root'\n"); 26 | return false; 27 | } 28 | 29 | node_t *newnode = calloc(1, sizeof(node_t)); 30 | if (newnode == NULL) { 31 | fprintf(stderr, "insert_entry(): Failed to malloc memory\n"); 32 | return false; 33 | } 34 | newnode->datum = datum; 35 | 36 | node_t *cur = root; 37 | while (cur) { 38 | direction dir = cur->datum < datum ? larger : smaller; 39 | node_t *subtree = cur->children[dir]; 40 | if (subtree == NULL) { 41 | cur->children[dir] = newnode; 42 | return true; 43 | } 44 | cur = subtree; 45 | } 46 | 47 | // Should never get here 48 | abort(); 49 | return false; 50 | } 51 | 52 | void free_tree(node_t *root) 53 | { 54 | if (root) { 55 | free_tree(root->children[larger]); 56 | free_tree(root->children[smaller]); 57 | free(root); 58 | } 59 | } 60 | 61 | node_t *create_tree(int32_t root_datum) 62 | { 63 | node_t *root = calloc(1, sizeof(node_t)); 64 | root->datum = root_datum; 65 | return root; 66 | } 67 | 68 | node_t *create_random_tree(uint32_t seed) 69 | { 70 | srand(seed); 71 | node_t *root = create_tree(rand()); 72 | 73 | for (int i = 0; i < 10; ++i) { 74 | if (!insert_entry(root, rand())) { 75 | fprintf(stderr, "Error inserting entry number %d\n", i); 76 | free_tree(root); 77 | return NULL; 78 | } 79 | } 80 | 81 | return root; 82 | } 83 | 84 | int main(int argc, char *argv[]) 85 | { 86 | if (argc != 2) { 87 | fprintf(stderr, "Usage: %s \n", argv[0]); 88 | exit(EXIT_FAILURE); 89 | } 90 | 91 | uint32_t seed = atoi(argv[1]); 92 | node_t *tree_root = create_random_tree(seed); 93 | free_tree(tree_root); 94 | return 0; 95 | } 96 | -------------------------------------------------------------------------------- /doc/vsh.txt: -------------------------------------------------------------------------------- 1 | *vsh.txt* Modifiable text pseudo terminal. *vsh* *Vsh* 2 | 3 | Version; 1.0 4 | Author: Matthew Malcomson 5 | License: MIT license {{{ 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | }}} 24 | 25 | Contents~ 26 | *vsh-contents* 27 | Overview |vsh-overview| 28 | Running commands |vsh-running| 29 | Customisation |vsh-customisation| 30 | GDB Integration |vsh-gdb-integration| 31 | Mappings |vsh-mappings| 32 | 33 | ============================================================================== 34 | Overview~ 35 | 36 | *vsh-overview* 37 | |Vsh| is a plugin that behaves similarly to a terminal emulator, but allows 38 | easy editing, searching, and modifying output by using a vim buffer on a 39 | simple text file as the middleman for input and output. 40 | 41 | The benefits of |vsh| over the |:terminal| command are in retroactively 42 | modifying or removing output, easily rerunning commands with slight 43 | variations, and keeping a dynamic record of your shell session for easy 44 | reproducibility. 45 | 46 | It is important to stress that a |vsh| buffer is a simple text file. Any and 47 | all standard vim commands are available in this buffer. 48 | 49 | |Vsh| treats lines beginning with a prompt as commands, prompts with a 50 | ' #' after them are treated as comments. The default prompt is "vshcmd: > ". 51 | For more information, see |vsh-prompt|. 52 | 53 | Lines not beginning with a prompt are treated as output. Running a command 54 | will replace the old output between the command and the next prompt with the 55 | new output. See |vsh-running| for more info. 56 | 57 | Commands may be run in any order, but there is an active, stateful bash 58 | session that commands are sent to, so they may behave differently each time 59 | they are run. 60 | 61 | Text editing commands are provided for ease of working with output and 62 | commands in the |vsh| buffer. There are text objects for a command and an 63 | output, and a conveniance mapping to put the range of an output into the vim 64 | command prompt ready to run an |:ex| command. See |vsh-editing| for more info. 65 | 66 | There are two main difference between |vsh| and things like VimShell and 67 | Conque: 68 | https://github.com/Shougo/vimshell.vim 69 | http://www.vim.org/scripts/script.php?script_id=2771 . 70 | First, |vsh| is aimed around working with file-backed sessions that are saved 71 | and reused, to be updated as required while working. 72 | Second, |vsh| allows re-running commands in place moving commands around, and 73 | modifying them in-place, so that complex sessions can easily be re-run by 74 | re-starting at the top of the file. 75 | 76 | Combining the two means that a complex set of commands often get iteratively 77 | refined as time goes on eventually leaving something which can easily be 78 | turned into a script (for whatever REPL you were interacting with) in a few 79 | vim text-editing commands. 80 | 81 | ============================================================================== 82 | Running~ 83 | *vsh-running* 84 | 85 | On opening a vsh buffer, |vsh| starts a process of your current 'shell' in a 86 | new pseudo terminal. This shell is started in the same directory as the |vsh| 87 | file was opened, and in the same manner as your normal shell apart from 88 | setting the following environment variables. $PAGER is set to be empty, 89 | $MANPAGER is set to "col -b" to remove special control characters, $EDITOR is 90 | set to a |vsh| provided python script (see |vsh-editor|), and $TERM is set to 91 | "dumb". 92 | 93 | Running a command removes all lines between the cursor and the next prompt, 94 | then sends the text after the prompt to the underlying shell process along 95 | with a newline character. What the shell decides to do with this text is up to 96 | it, but in their normal state most run this as a command and print the output. 97 | |Vsh| inserts any output from the shell back into the |vsh| buffer after the 98 | line whose command was last run. This is done asynchronously, so you can 99 | continue to edit the vsh buffer while output is being inserted. 100 | If you need to interrupt a command with a control character, you can use the 101 | |vsh#vsh#SendControlChar()| function, (bound to c by default). 102 | This reads a character from the keyboard, and sends the C-[x] equivalent to 103 | the underlying process, so that cc will send ^C. 104 | NOTE: If you run another command while the previous is still producing output 105 | then the output of the first command will be placed after the prompt of the 106 | second command. This is because there is no reliable way to tell when a 107 | command has finished (especially when accounting for a subcommand taking 108 | input). 109 | NOTE: If you remove all previously inserted text, and the command line that 110 | inserted that text, then remaining output will be inserted at the end of the 111 | buffer. 112 | NOTE: The position to put text into the buffer is remembered with a mark. This 113 | has some consequences. See |vsh-marks| 114 | 115 | *vsh-motion* 116 | Mappings are provided on CTRL-N and CTRL-P by default to move to the next and 117 | previous command line. 118 | 119 | *vsh-prompt* 120 | A |vsh| buffer has no special markings in it for the plugin to store 121 | information (so that anyone without this plugin to view the file and 122 | understand what's going on). The only way that |vsh| determines which lines 123 | are commands and which lines are output is by checking the start of the line 124 | for the |vsh| prompt. The default |vsh| prompt is "vshcmd: > " but can be 125 | changed on a per buffer basis with |vsh#vsh#SetPrompt()|. See 126 | |vsh-customisation| for details. 127 | 128 | This method of distinguishing prompts has some consequences, most notably if 129 | the output of a command includes a line starting with the prompt then that 130 | line will be treated as a command. Hence you can store a "useful commands" 131 | file, and "grep" it to search for tricks you once found. > 132 | 133 | vshcmd: > grep 'neat trick' ~/.vim/vsh_files/useful_commands 134 | vshcmd: > echo 'Hello world' # Such a a neat trick!! 135 | < 136 | These commands can then be run next. 137 | 138 | If you know the output of a command will include a prompt, but you don't want 139 | to treat it as a command, you can use the trick below. > 140 | 141 | vshcmd: > cat other_file.vsh | sed 's/^/ /' 142 | vshcmd: > echo 'Hellow orld' # Can never quite remember this 143 | < 144 | Vsh will not treat the line as a command. 145 | NOTE: Unfortunately there is a bug where vim still treats these lines as 146 | comments, and hence the prompt will still be automatically inserted when 147 | adding a newline when the cursor is on these lines. 148 | 149 | 150 | *vsh-completion* 151 | Because many shells and REPL's provide completion abilities using the readline 152 | library, |Vsh| provides special allowance to send readline control characters 153 | to the underlying shell. When typing a command in insert mode, pressing CTRL-Q 154 | removes previous output and sends characters intended to call the readline 155 | function "possible-completions". This should list the possible completions 156 | under the current prompt so that one can use vim's normal completion keys to 157 | choose one. Similarly, CTRL-S runs the bash readline function 158 | "glob-list-expansions". These commands can also be run in normal mode with 159 | l and g respectively. 160 | 161 | N.b. It may be beneficial to turn of pagination of completions in readline 162 | when using this feature. That means all completions are put into the buffer 163 | immediately rather than being paginated. In order to turn such pagination off 164 | for all "dumb" terminals one can add the following clause to ~/.inputrc . > 165 | $if term=dumb 166 | set page-completions off 167 | $endif 168 | < 169 | 170 | *vsh-shell* 171 | |Vsh| uses the 'shell' option to decide what shell to start. Most of the 172 | features should work with any shell (it's just sending whatever you type to 173 | that shell with a newline appended). When using a shell other than bash, the 174 | features most likely to break are the completion keys. These use a bash 175 | feature to find out what bindings readline is using, and hence any special 176 | setup in your shell startup file will affect this. 177 | NOTE: This program has only been tested extensively with the bash shell so 178 | other shells are much more likely to have problems. 179 | In particular, the C-shell detects that it is running in a dumb terminal 180 | ($TERM=dumb) and disables line editing, which the |vsh| completion keys rely 181 | on. 182 | 183 | *vsh-file-open* *vsh-'path'* 184 | |Vsh| provides helper mappings that allow quick navigation to files printed 185 | by the shell process. Pressing `gf` on a filename will run the normal `gf` 186 | command with the local 'path' set to the working directory of the process 187 | currently in the foreground of the terminal we are communicating with. This 188 | means that it should work with programs like python that have changed 189 | directory. > 190 | vshcmd: > python 191 | Python 3.6.0 (default, Jan 16 2017, 12:12:55) 192 | [GCC 6.3.1 20170109] on linux 193 | Type "help", "copyright", "credits" or "license" for more information. 194 | >>> 195 | vshcmd: > import os; os.chdir(os.path.expanduser('~/.vim/bundle/vsh')) 196 | >>> 197 | vshcmd: > os.listdir('.') 198 | ['ftdetect', '.git', 'test.vsh', 'autoload', 'TODO.txt', 'README.md', 'tests', 'syntax', 'doc', 'demo', 'ftplugin'] 199 | >>> 200 | < 201 | Pressing `gf` on `README.md` will open that file. 202 | 203 | Similarly, |vsh| provides an insert-mode mapping for CTRL-X CTRL-F that runs 204 | the completion in the directory that the current foreground process is in. 205 | NOTE: These have only been tested on Linux ... I don't think it'll work on mac 206 | or BSD (patches welcome :-) -- look in vsh_find_cwd()). 207 | 208 | If the buffer-local variable `b:vsh_dir_store` is set, then just before 209 | running a command, |vsh| will put the text " # " directory after the 210 | command line. When running |vsh-gf| or similar, |vsh| will first look for this 211 | marker after a command line and if it's found will use that directory in 212 | 'path' instead of the current forground process'. 213 | NOTE: This may cause problems in REPL's where the '#' character is not a 214 | comment leader and is hence turned off by default. 215 | To turn this on by default, put `let g:vsh_dir_store = 1` in your vimrc. 216 | 217 | This is not used for CTRL-X CTRL-F, as it is assumed that you want to insert 218 | filenames relative to the working directory of the process you're sending 219 | that text to. 220 | 221 | *vsh-save* *vsh-activate* *vsh-deactivate* 222 | When working in a |vsh| buffer pressing anywhere in the output of a 223 | command will rerun that command. This can be annoying if you have written 224 | notes across that output while you work. You should be able to undo the rerun 225 | with `u` but in order to skip the bother you can deactivate the current 226 | command with s. This simply inserts a command at the start of the 227 | command which means |vsh| won't run it on . The output can be reactivated 228 | with a. 229 | 230 | *vsh-text-objects* *vsh-command-object* *vsh-output-object* 231 | *vsh-output-range* 232 | In order to easily select the output of a command, or the command itself |vsh| 233 | provides text objects bound to `ic` `ac` `ao` and `io` by default. The first 234 | two operate on an inner-command and outer-command respectively, which is the 235 | text after the prompt on the line that would be executed were pressed 236 | where the cursor is now. The difference between `ac` and `ic` is that `ac` 237 | includes any whitespace between the prompt and text on that line. 238 | The second two operate on an inner-output and outer-output, which is the text 239 | between two prompts. `ao` includes the command prompt that likely created that 240 | output. 241 | A special mapping on o is provided to start a command with the 242 | range that refers to the current output of a command pre-inserted in the 243 | command line. This is slightly quicker to run filter commands with |:g|. 244 | 245 | *vsh-command-block-object* 246 | Similarly to the command objects above, there are text objects defined on 247 | command blocks. A group of commands without any output between them can be 248 | selected with `ix`, while a group of commands and comments can be selected 249 | with `ax`. 250 | This is particularly useful when running commands with the operator: 251 | `ix` will run the entire command block in one go, keeping them all 252 | together. 253 | 254 | *vsh-run-multiple* 255 | Sometimes it's convenient to send many lines at once to the underlying shell. 256 | This can be achieved by using the command |vsh-:Vrerun| with a range. 257 | This command sends all command lines in the given range to the underlying 258 | process, and inserts output after the last command in that range. There is a 259 | visual mode convenience mapping on to run this over the current visual 260 | selection. In normal mode is an operatorfunc mapping that acts on a 261 | provided motion. It makes all motions linewise (as the operator only makes 262 | sense linewise), and to act on the specific line one can use any motion in the 263 | current line. 264 | 265 | *vsh-send* *vsh-send-other* *vsh-send-buffer* 266 | You can send text to the underlying terminal from another buffer with 267 | |vsh-:VshSend|. This command takes a range and requires a buffer name as an 268 | argument, it sends the text in that range to the process in the specified 269 | buffer. An operator is provided under the default mapping of `vs` that 270 | sends the text acted upon to the buffer |b:vsh_alt_buffer|. 271 | |b:vsh_alt_buffer| can be set by using the `vb` mapping with a count, 272 | e.g. `3vb` sets |b:vsh_alt_buffer| to the name of buffer number 3 and 273 | does nothing else. 274 | `vd` does the same as `vs` but dedents lines. 275 | 276 | *vsh-send-python* *vsh-send-terminated* 277 | As the python REPL treats blank lines as a terminating block, while python 278 | scripts don't, sending text directly from a source code buffer to a REPL can 279 | be problematic. Some conveniance mappings are provided to help with this: 280 | all these mappings remove blank lines in the range they act upon. Those with 281 | `t` in them add a trailing blank line to mark the end of a function/block, 282 | those ending with a `d` dedend the lines by the amount of the first line in 283 | the range. 284 | The mappings are: `vps`, `vpd`, `vts`, and `vtd`. 285 | 286 | 287 | ============================================================================== 288 | Customisation~ 289 | 290 | *vsh-customisation* 291 | 292 | *g:vsh_insert_mark* *g:vsh_prompt_mark* *vsh-marks* 293 | 294 | An implementation detail of |vsh| is that it uses user-modifiable marks to 295 | remember where to put text read from the underlying process. If these marks 296 | are lost, then |vsh| can't tell where to put text and simply puts it at the 297 | end of the buffer. These marks are 'p and 'd by default, but can be changed 298 | by setting the |g:vsh_insert_mark| and |g;vsh_prompt_mark| variables in your 299 | vimrc. 300 | 301 | 302 | *g:vsh_default_prompt* *vsh#vsh#SetPrompt()* *b:vsh_prompt* 303 | The current prompt is stored in the |b:vsh_prompt| variable. This variable is 304 | locked with |:lockvar|, it must remain in sync with the |syntax| highlighting 305 | and local 'commentstring', and 'comments' settings for the proper working of 306 | this plugin. It may be changed on a per buffer basis with 307 | |vsh#vsh#SetPrompt()|, to change the default you can set 308 | |g:vsh_default_prompt| in your vimrc. 309 | 310 | 311 | *g:vsh_no_default_mappings* *vsh-custom-bindings* *vsh-custom-mappings* 312 | Personalisation of mappings may be done in two different ways. One may 313 | disable all default mappings by setting `g:vsh_no_default_mappings` to 314 | something non-zero before the VSH ftplugin is loaded (i.e. before reading any 315 | VSH file, which implies at early startup). Alternatively, one may specify 316 | different mappings for given actions by setting `g:` before 317 | loading the plugin/vsh.vim file (i.e. at early startup). The override 318 | variables are the second item in each list entry in the mappings lists found 319 | in plugin/vsh.vim and the best documentation would be to read the values 320 | there. 321 | 322 | ============================================================================== 323 | Tips and Tricks~ 324 | 325 | *vsh-python-REPL* 326 | Working with the Python REPL has some slight quirks related to the fact that 327 | Python denotes blocks with whitespace. In Python scripts blank lines in the 328 | middle of functions are fine, but in the REPL a blank line finishes the 329 | definition of a function. 330 | 331 | In order to account for this sending text from a python buffer to a REPL we 332 | have alternate mappings for sending text defined for python buffers (see 333 | |vsh-send-python|). 334 | 335 | However handling python REPL's is still slightly awkward since a blank line 336 | needs to be sent in order to finish the relevant line. This could be done by 337 | including a prompt without a command. > 338 | vshcmd: > def testfunc(): 339 | vshcmd: > print('hello world') 340 | vshcmd: > 341 | ... ... >>> 342 | < 343 | This requires there to be an apparently empty prompt in the file, and 344 | sometimes the single-space after the final prompt gets lost during editing 345 | (removing trailing whitespace) causing the line to no longer be treated as a 346 | command. 347 | 348 | An alternative is to add the control character `^M` at the end of the last 349 | line of the function definition (entered with or ). > 350 | vshcmd: > def testfunc(): 351 | vshcmd: > print('hello world') 352 | ... ... >>> 353 | < 354 | This requires there to be control characters in the file, and they may get 355 | lost when copy-pasting across various programs. 356 | 357 | As it stands there is no answer without trade-off and the choice lies with the 358 | user. 359 | 360 | *vsh-restart-shell* *vsh#vsh#RestartSubprocess()* 361 | Sometimes one may be working over SSH, and the connection may be lost. Rather 362 | than wait for the timeout if you do not have any state in the local shell it 363 | might be sensible to simply restart the underlying shell for this buffer. 364 | 365 | This restart can be done by using |vsh#vsh#RestartSubprocess()|. One can 366 | think of this as closing a terminal (while sending the appropriate SIGHUP to 367 | underlying processes) and opening a new one. 368 | 369 | ============================================================================== 370 | Integration With GDB~ 371 | 372 | *vsh-gdb-integration* 373 | There is some slight integration with GDB for this plugin. This comes in the 374 | form of a GDB plugin which provides 4 new commands to interact with the vim 375 | instance running the current VSH buffer. 376 | 377 | In order to use this integration one would add the below source line to their 378 | `~/.gdbinit` (or one could manually run this command at the GDB prompt). 379 | `source /integration/with_gdb.py` 380 | 381 | This integration provides four commands: 382 | 1) gohere (move cursor to source corresponding to GDB location provided). 383 | Which window to use is determined by command argument, plus whether there 384 | are any windows marked with `w:gdb_view` being truthy. 385 | 2) showhere (run `gohere`, then move cursor back to VSH buffer -- unless 386 | `w:gdb_view` marks the current window, or there is only one window). 387 | 3) mark-this (set mark provided on command line to source corresponding to GDB 388 | location provided on command line). 389 | 4) mark-stack (set marks A, B, etc to the frames 0, 1, etc of the current 390 | backtrace). 391 | 392 | N.b. more extensive help on how to use each command is available via 393 | `help ` at the GDB prompt. 394 | 395 | ============================================================================== 396 | Mappings~ 397 | *vsh-mappings* 398 | 399 | *vsh--mappings* 400 | 401 | *vsh-* *vsh-* *vsh-* 402 | Delete previous output and run current command. The 403 | current command is determined by where the cursor is. 404 | It is the next command above the cursor position. Hence 405 | the action of if the cursor is anywhere marked [x] 406 | below would be the same. > 407 | 408 | [x]vshcmd: > pw[x]d 409 | /home/hardenedapple 410 | ~ [14:0[x]1:56] $ 411 | 412 | < 413 | 414 | *vsh-next-command* *vsh-CTRL-N* 415 | CTRL-N Move to the next command line. 416 | 417 | *vsh-prev-command* *vsh-CTRL-P* 418 | CTRL-P Move to the previous command line. 419 | 420 | *vsh-[[* *vsh-]]* *vsh-][* *vsh-[]* 421 | [[,[],][,]] Move [count] sections backwards or forwards, ending at the 422 | start or end of a section corresponding to the command used. 423 | These mappings are specialising the general motions under 424 | these keys for vsh buffers. See the vim help for |[[| and 425 | associated keys for the logical command. 426 | For `vsh` a section is defined as a number of lines beginning 427 | with |b:vsh_prompt|, comments and commands are both included 428 | in this. 429 | 430 | *vsh-i_M-CR* *vsh-exec-and-newprompt* 431 | i_ Run the current command, create a new prompt under the current 432 | command, and leave the cursor in insert mode at the end of 433 | that new prompt. 434 | NOTE: only available in neovim, since in original vim this 435 | would send . 436 | 437 | *vsh-n* *vsh-newprompt* 438 | n Create a new prompt under the output of the current command 439 | and leave the cursor in insert mode at the end of that new 440 | prompt. 441 | 442 | 443 | *vsh-v_* *vsh-rerun-mapping* *vsh-:Vrerun* 444 | :[range]Vrerun Remove all output in the range, send all command 445 | lines to the underlying shell, and insert output after the 446 | last command. 447 | Visual mode mapping to do the above on the visual selection. 448 | Normal mode mapping to run the above on a given motion. 449 | 450 | 451 | *vsh-v_* *vsh-make-cmds-mapping* *vsh-:VmakeCmds* 452 | :[range]VmakeCmds Put |b:vsh_prompt| at the start of every line in [range]. 453 | This is useful to change output or copied text into a 454 | series of commands at the same time. 455 | (e.g. if copying a few lines from a shell script). 456 | Visual mode mapping to do the above on the visual selection. 457 | Normal mode mapping to run the above on a given motion. 458 | 459 | *vsh-:VshSendUnterminated* 460 | :VshSendUnterminated [input] 461 | If not given an input this requests input from the user and 462 | sends that text to the underlying process without any 463 | terminating newline. 464 | If given an input it sends that input to the underlying 465 | process without a terminating newline. 466 | This mostly useful for interacting with processes which will 467 | act on single character input before the newline is sent. 468 | This is not often used. 469 | 470 | *vsh-:VshConvertCat* 471 | :VshConvertCat [marker] 472 | When run in a command section where the command uses `cat`, 473 | then convert the command to `cat << EOF > ` and add 474 | an `EOF` marker at the end of the command. This helps convert 475 | the output of a `cat` command used previously into something 476 | that creates that file. 477 | This is mostly useful for converting a session showing what 478 | you did into either a replayable session for someone else to 479 | follow. Also this could be used when turning a session into a 480 | script for later use. 481 | As an example running this command from anywhere in the 482 | first command or its output below would turn > 483 | vshcmd: > cat testscript.sh 484 | #!/bin/bash 485 | echo "Hello world" 486 | $ 487 | vshcmd: > 488 | < Into > 489 | vshcmd: > cat < testscript.sh 490 | vshcmd: > ##!/bin/bash 491 | vshcmd: > echo "Hello world" 492 | vshcmd: > EOF 493 | vshcmd: > 494 | < NOTE: the marker is EOF by default, but another marker can be 495 | selected with an argument. 496 | NOTE: `#` characters at the start of a line in the output are 497 | converted to double `##` in order to escape the comment 498 | character so the file created by the converted sequence is the 499 | same as the file that was originally printed to the terminal. 500 | 501 | *vsh-c* *vsh-control-char* 502 | *vsh-send-control-char* *vsh-send-control* 503 | c Read a single character from the user, and send the control 504 | modified version of that character to the underlying pseudo 505 | terminal. i.e. to send a ^C, type cc 506 | 507 | *vsh-possible-completions* *vsh-complete* 508 | *vsh-completions* *vsh-l* 509 | *vsh-i_CTRL-Q* 510 | l Request the current foreground process list possible 511 | CTRL-Q completions of the next word into the current buffer. 512 | NOTE May be fragile to different 'shell' settings than bash. 513 | Should work in python, gdb, and other prompts too. 514 | 515 | *vsh-glob-expansions* *vsh-glob* 516 | *vsh-g* *vsh-i_CTRL-S* 517 | g Request the current foreground process list the glob 518 | CTRL-S expansions of the current word. 519 | NOTE Is known to be fragile to different 'shell' settings. 520 | As a workaround for troublesome shells, might want to have > 521 | let b:vsh_completions_cmd[1] = " echo \n" 522 | < in your `ftplugin/vsh.vim` file. 523 | 524 | *vsh-gx* *vsh-gf* *vsh-gF* *vsh-CTRL-W_gf* 525 | *vsh-CTRL-W_gF* *vsh-CTRL-W_f* *vsh-CTRL-W_F* 526 | gf, gF Same as default vim bindings for these keys, but run 527 | CTRL-W f CTRL-W F accounting for the directory the current foreground 528 | CTRL-W gf CTRL-W gF process is in. See |vsh-file-open| for more information. 529 | 530 | *vsh-i_CTRL-X_CTRL-F* 531 | CTRL-X CTRL-F Run file completion |compl-filename| in the working 532 | directory of the current foreground process. 533 | 534 | *vsh-a* *vsh-s* 535 | s Save current output by commenting out related command line. 536 | a Activate current output by uncommenting related command line. 537 | 538 | *vsh-^* *vsh-I* 539 | I Insert at the start of the current command line. 540 | ^ Move to the start of the current command line. 541 | (i.e. just after the prompt) 542 | 543 | *vsh-ic* *vsh-ac* *vsh-io* *vsh-ao* 544 | ic Act on the text of a command excluding extra whitespace between 545 | the prompt and this text. 546 | ac Act on the text of a command including extra whitespace between 547 | the prompt and this text. 548 | io Act on the output of a command excluding the command line itself. 549 | ao Act on the output of a command including the command line itself. 550 | 551 | *vsh-ix* *vsh-ax* 552 | ix Operator mode mapping to select a group of commands. 553 | ax Operator mode mapping to select a group of commands and comments 554 | together. 555 | 556 | 557 | *vsh-global-mappings* 558 | 559 | :[range]VshSend[!] {bufname} *vsh-:VshSend* 560 | Send the lines in [range] to the process associated with buffer 561 | {bufname}. 562 | With a bang, removes the indentation of the first line in the range 563 | from all lines in the range. 564 | vb *vsh-vb* 565 | Set |b:vsh_alt_buffer| to the name of buffer |v:count| and do nothing 566 | else. Particularly useful before running vs or vd. 567 | vs *vsh-vs* 568 | Send the lines in the motion acted upon to the process associated with 569 | buffer |b:vsh_alt_buffer|. 570 | vd *vsh-vd* 571 | Same as vs, but removes the indentation of the first line off 572 | all lines in the range. 573 | This is useful for sending parts of a python function to a |vsh| 574 | buffer running a python interpreter. 575 | 576 | *vsh-python-mappings* 577 | vps *vsh-vps* 578 | Operator to run on a range of lines in a buffer. 579 | Send that range of lines -- with all blank lines removed -- to the 580 | process associated with buffer |b:vsh_alt_buffer|. 581 | vpd *vsh-vpd* 582 | Send the range of lines -- dedented and with all blank lines removed 583 | -- to the process associated with buffer |b:vsh_alt_buffer|. 584 | vts *vsh-vts* 585 | Send the range of lines -- with all blank lines removed and a 586 | terminating blank line added -- to the process associated with buffer 587 | |b:vsh_alt_buffer|. 588 | vtd *vsh-vtd* 589 | Send the range of lines -- dedented, with all blank lines removed, and 590 | with a terminating blank line added -- to the process associated with 591 | buffer |b:vsh_alt_buffer|. 592 | 593 | vim:tw=78:et:ft=help:norl: 594 | -------------------------------------------------------------------------------- /emacs-tar.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Nothing particularly difficult here -- just storing it for reference more than 3 | # anything. Want to dereference links so that the tarball created for an emacs 4 | # package includes the relevant shell script and python files to get started. 5 | tar -c -f vsh-mode-1.0.tar --dereference vsh-mode-1.0/ 6 | -------------------------------------------------------------------------------- /ftdetect/vsh.vim: -------------------------------------------------------------------------------- 1 | autocmd BufRead,BufNewFile *.vsh set filetype=vsh 2 | -------------------------------------------------------------------------------- /ftplugin/python.vim: -------------------------------------------------------------------------------- 1 | runtime! plugin/vsh.vim 2 | 3 | for [mapmode, overridevar, maptype, trigger, plugmap, final_expansion] in g:vsh_python_mappings_list 4 | execute mapmode . 'noremap ' . maptype . plugmap . final_expansion 5 | endfor 6 | if !exists('g:vsh_no_default_mappings') 7 | for [mapmode, overridevar, maptype, trigger, plugmap, final_expansion] in g:vsh_python_mappings_list 8 | let final_trigger = get(g:, overridevar, trigger) 9 | execute mapmode . 'map ' . final_trigger . plugmap 10 | endfor 11 | endif 12 | -------------------------------------------------------------------------------- /ftplugin/vsh.vim: -------------------------------------------------------------------------------- 1 | if exists("b:did_ftplugin") 2 | finish 3 | endif 4 | let b:did_ftplugin = 1 5 | 6 | " Can't just change the b:vsh_prompt variable, also have to change the syntax 7 | " definitions and the comments/commentstring definitions -- this is done in a 8 | " helper function. 9 | call vsh#vsh#SetPrompt(get(g:, 'vsh_default_prompt', 'vshcmd: > ')) 10 | 11 | " Get a process for this job, and set an autocmd so the process is closed when 12 | " unloaded. 13 | " 14 | " Want to close the subprocess whenever the relevant buffer is about to be 15 | " deleted from the list. 16 | " Otherwise jobs would just be hanging around past their usefullness (reopening 17 | " that buffer just creates a new job as all buffer local variables are 18 | " removed). 19 | " 20 | " Putting an autocmd on the BufDelete event doesn't work because the buffer 21 | " local variables already have been deleted by the time that autocmd is called. 22 | " I think this is silly, but that's the semantics that are happening at the 23 | " moment. 24 | " Having an autocmd on the BufUnload event means we don't know whether the 25 | " buffer is about to be removed or not. 26 | " I can use the BufUnload autocmd to store the vsh_job variable globally and 27 | " kill that in the BufDelete event. 28 | " We'll have to see whether there are unknown problems with this in the future. 29 | " 30 | " 31 | " Questions: 32 | " Do buffer local variables get removed when buffer is deleted? 33 | " get removed after :bdelete, do not get removed under :bunload 34 | " 35 | " Do filetype plugins get executed multiple times under :bdelete ? 36 | " No 37 | " Do filetype plugins get ran each time a file is opened? 38 | " No 39 | " Do filetype plugins get ran after :sb if the buffer had been unloaded or 40 | " deleted? 41 | " Yes 42 | " Under :bunload this dosen't usually matter because the buffer local variables are 43 | " kept, which means the b:did_ftplugin variable is still around and caught 44 | " by the first if clause in this file. 45 | " 46 | " Using getbufvar() on in a BufDelete autocmd gets nothing. 47 | " 48 | " Does using on a buffer without a backing file cause problems 49 | " uniquely identifying the buffer? 50 | 51 | 52 | if !exists('g:vsh_closing_jobs') 53 | let g:vsh_closing_jobs = {} 54 | endif 55 | if getbufvar(bufnr(), 'vsh_job') == '' 56 | call vsh#vsh#StartSubprocess() 57 | augroup VshBufferClose 58 | autocmd BufUnload let g:vsh_closing_jobs[expand('')] = getbufvar(expand(''), 'vsh_job', v:null) 59 | autocmd BufDelete call vsh#vsh#ClosedBuffer() 60 | augroup END 61 | endif 62 | 63 | " Don't insert newlines when writing a long command 64 | setlocal formatoptions-=t 65 | setlocal formatoptions-=c 66 | 67 | " TODO Unfortunately we shouldn't be setting 'conceallevel' and 'concealcursor'. 68 | " They're local to *window* not to *buffer*. 69 | " Would realy like to figure out some way of handing this nicely. I really 70 | " want this setting around. 71 | " 72 | " Documentation says: 73 | " When editing a buffer that has been edited before, the options from the window 74 | " that was last closed are used again. If this buffer has been edited in this 75 | " window, the values from back then are used. Otherwise the values from the 76 | " last closed window where the buffer was edited last are used. 77 | " 78 | " This means that *usually* everything works as expected -- when opening 79 | " up a VSH mode buffer the window we opened it in gets this option set, when 80 | " opening the buffer again in some other window we take the settings from the 81 | " last window closed with this buffer (which would have had the 'conceallevel' 82 | " setting applied). 83 | " 84 | " However it's not 100% foolproof: 85 | " - When opening up a VSH mode buffer in the another tab from the command 86 | " line (with something like `vim -p x.txt a.vsh`) and then splitting to a 87 | " VSH mode buffer in the first tab without having closed the original 88 | " window where the VSH mode buffer was opened, the new window on a VSH mode 89 | " buffer has conceallevel not set. 90 | " I *think* this may be a bug. Reading the explanation in `:help 91 | " local-options` it sounds like it wouldn't be a bug, but reading that 92 | " explanation seems to imply that having a split window on a VSH buffer (and 93 | " never having closed any window on a VSH buffer) then splitting anothing 94 | " window to show the VSH buffer should take the window-local settings from the 95 | " window I split from. 96 | " I.e. seems to imply: 97 | " :e x.txt 98 | " :vnew a.vsh 99 | " :setlocal conceallevel=2 " Automatically from the ftplugin 100 | " :wincmd p 101 | " :sb a.vsh 102 | " Would leave the second window on the VSH buffer with `conceallevel` from the 103 | " x.txt buffer. That doesn't happen. 104 | " 105 | " So honestly not 100% sure what is going on here. Could be something a bug in 106 | " the tab example (and that the help text is a bit misleading), I don't think 107 | " I'm misconfiguring things (though it is still a possibility), could be that 108 | " the help is misleading and everything is fine. 109 | " I'm hoping a bug in the tab example, will see once I eventually raise a bug. 110 | setlocal conceallevel=2 111 | setlocal concealcursor=nc 112 | setlocal formatoptions+=r 113 | setlocal formatoptions+=o 114 | 115 | " Default tabsize in the output of many programs. 116 | setlocal tabstop=8 117 | 118 | " Shells, gdb, and similar have '-' as a keyword. 119 | setlocal iskeyword+=- 120 | 121 | call vsh#vsh#SetupMappings() 122 | 123 | let b:vsh_dir_store = get(g:, 'vsh_dir_store', 0) 124 | let b:undo_ftplugin = 'setlocal tabstop< comments< formatoptions< conceallevel< concealcursor< iskeyword< | call vsh#vsh#Undoftplugin()' 125 | -------------------------------------------------------------------------------- /integration/with_gdb.py: -------------------------------------------------------------------------------- 1 | ../vsh-mode-1.0/integration_with_gdb.py -------------------------------------------------------------------------------- /plugin/vsh.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_vsh') 2 | finish 3 | endif 4 | let g:loaded_vsh = 1 5 | 6 | let g:vsh_buffer_mappings_list = [ 7 | \ ['n' , 'vsh_next_prompt' , '' , '' , ' (vshNextPrompt)' , " :call vsh#vsh#MoveToNextPrompt('n', v:count1)"] , 8 | \ ['n' , 'vsh_prev_prompt' , '' , '' , ' (vshPrevPrompt)' , " :call vsh#vsh#MoveToPrevPrompt('n', v:count1)"] , 9 | \ ['v' , 'vsh_v_next_prompt' , '' , '' , ' (vshVNextPrompt)' , " :call vsh#vsh#MoveToNextPrompt('v', v:count1)"] , 10 | \ ['v' , 'vsh_v_prev_prompt' , '' , '' , ' (vshVPrevPrompt)' , " :call vsh#vsh#MoveToPrevPrompt('v', v:count1)"] , 11 | \ ['o' , 'vsh_o_next_prompt' , '' , '' , ' (vshONextPrompt)' , " V:call vsh#vsh#MoveToNextPrompt('o', v:count1)"] , 12 | \ ['o' , 'vsh_o_prev_prompt' , '' , '' , ' (vshOPrevPrompt)' , " V:call vsh#vsh#MoveToPrevPrompt('o', v:count1)"] , 13 | \ ['n' , 'vsh_replace_output' , '' , ' ' , ' (vshReplaceOutput)' , " :call vsh#vsh#ReplaceOutput()"] , 14 | \ ['i' , 'vsh_run_new_prompt' , '' , '' , ' (vshRunNewPrompt)' , " :call vsh#vsh#ReplaceOutputNewPrompt()"] , 15 | \ ['n' , 'vsh_new_prompt' , '' , 'n ' , ' (vshNewPrompt)' , " :call vsh#vsh#NewPrompt()"] , 16 | \ ['v' , 'vsh_make_cmd' , '' , '' , ' (vshMakeCmd)' , " :VmakeCmds"] , 17 | \ ['v' , 'vsh_rerun' , '' , '' , ' (vshRerun)' , " :Vrerun"] , 18 | \ ['n' , 'vsh_make_cmd_op' , ' ' , '' , ' (vshMakeCmdOp)' , " vsh#vsh#DoMakeCmdOperatorFunc()"] , 19 | \ ['n' , 'vsh_rerun_op' , ' ' , '' , ' (vshRerunOp)' , " vsh#vsh#DoRunOperatorFunc()"] , 20 | \ ['n' , 'vsh_send_control_char' , '' , 'c' , ' (vshSendControlChar)' , " :call vsh#vsh#SendControlChar()"] , 21 | \ ['n' , 'vsh_completions' , '' , 'l' , ' (vshCompletions)' , " :call vsh#vsh#ShowCompletions(0)"] , 22 | \ ['i' , 'vsh_i_completions' , '' , '' , ' (vshICompletions)' , " :call vsh#vsh#ShowCompletions(0)a"] , 23 | \ ['n' , 'vsh_glob_completions' , '' , 'g' , ' (vshGlobCompletions)' , " :call vsh#vsh#ShowCompletions(1)"] , 24 | \ ['i' , 'vsh_i_glob_completions' , '' , '' , ' (vshIGlobCompletions)' , " :call vsh#vsh#ShowCompletions(1)a"] , 25 | \ ['n' , 'vsh_goto_thing' , '' , 'gx' , ' (vshGotoThing)' , ' :call vsh#vsh#WithPathSetSelf("(vshGotoThing)")'], 26 | \ ['n' , 'vsh_goto_file' , '' , 'gf' , ' (vshGotoFile)' , " :call vsh#vsh#WithPathSet('normal! gf')"] , 27 | \ ['n' , 'vsh_goto_FILE' , '' , 'gF' , ' (vshGotoFILE)' , " :call vsh#vsh#WithPathSet('normal! gF')"] , 28 | \ ['n' , 'vsh_split_file' , '' , 'f' , ' (vshSplitFile)' , " :call vsh#vsh#WithPathSet('wincmd f')"] , 29 | \ ['n' , 'vsh_split_FILE' , '' , 'F' , ' (vshSplitFILE)' , " :call vsh#vsh#WithPathSet('wincmd F')"] , 30 | \ ['n' , 'vsh_tab_split_file' , '' , 'gf' , ' (vshTabSplitFile)' , " :call vsh#vsh#WithPathSet('wincmd gf')"] , 31 | \ ['n' , 'vsh_tab_split_FILE' , '' , 'gF' , ' (vshTabSplitFILE)' , " :call vsh#vsh#WithPathSet('wincmd gF')"] , 32 | \ ['i' , 'vsh_file_completion' , '' , '' , ' (vshFileCompletion)' , " vsh#vsh#FileCompletion()"] , 33 | \ ['n' , 'vsh_save_command' , '' , 's' , ' (vshSaveCommand)' , " :call vsh#vsh#SaveOutput(0)"] , 34 | \ ['n' , 'vsh_activate_command' , '' , 'a' , ' (vshActivateCommand)' , " :call vsh#vsh#SaveOutput(1)"] , 35 | \ ['n' , 'vsh_start_ranged_command' , '' , 'o ' , ' (vshStartRangedCommand)' , " :=vsh#vsh#OutputRange()"] , 36 | \ ['n' , 'vsh_BOL' , '' , '^' , ' (vshBOL)' , " :call vsh#vsh#BOLOverride('n')"] , 37 | \ ['o' , 'vsh_BOL' , '' , '^' , ' (vshBOL)' , " :call vsh#vsh#BOLOverride('n')"] , 38 | \ ['v' , 'vsh_BOL' , '' , '^' , ' (vshBOL)' , " vsh#vsh#BOLOverride('v')"] , 39 | \ ['n' , 'vsh_insert_BOL' , '' , 'I' , ' (vshInsertBOL)' , " :call vsh#vsh#InsertOverride()"] , 40 | \ ['x' , 'vsh_inner_command' , '' , 'ic' , ' (vshInnerCommand)' , " :call vsh#vsh#SelectCommand(1)"] , 41 | \ ['x' , 'vsh_inner_COMMAND' , '' , 'io' , ' (vshInnerCOMMAND)' , " :call vsh#vsh#SelectOutput(0)"] , 42 | \ ['x' , 'vsh_outer_command' , '' , 'ac' , ' (vshOuterCommand)' , " :call vsh#vsh#SelectCommand(0)"] , 43 | \ ['x' , 'vsh_outer_COMMAND' , '' , 'ao' , ' (vshOuterCOMMAND)' , " :call vsh#vsh#SelectOutput(1)"] , 44 | \ ['x' , 'vsh_inner_command_block' , '' , 'ix' , ' (vshInnerCommandBlock)' , " :call vsh#vsh#SelectCommandBlock(0)"] , 45 | \ ['x' , 'vsh_outer_command_block' , '' , 'ax' , ' (vshOuterCommandBlock)' , " :call vsh#vsh#SelectCommandBlock(1)"] , 46 | \ ['o' , 'vsh_inner_command' , '' , 'ic' , ' (vshInnerCommand)' , " :call vsh#vsh#SelectCommand(1)"] , 47 | \ ['o' , 'vsh_inner_COMMAND' , '' , 'io' , ' (vshInnerCOMMAND)' , " :call vsh#vsh#SelectOutput(0)"] , 48 | \ ['o' , 'vsh_outer_command' , '' , 'ac' , ' (vshOuterCommand)' , " :call vsh#vsh#SelectCommand(0)"] , 49 | \ ['o' , 'vsh_outer_COMMAND' , '' , 'ao' , ' (vshOuterCOMMAND)' , " :call vsh#vsh#SelectOutput(1)"] , 50 | \ ['o' , 'vsh_inner_command_block' , '' , 'ix' , ' (vshInnerCommandBlock)' , " :call vsh#vsh#SelectCommandBlock(0)"] , 51 | \ ['o' , 'vsh_outer_command_block' , '' , 'ax' , ' (vshOuterCommandBlock)' , " :call vsh#vsh#SelectCommandBlock(1)"] , 52 | \ ['n' , 'vsh_start_of_command_block' , '' , '[[' , ' (vshStartOfCommandBlock)' , " :call vsh#vsh#CommandBlockEnds('n', v:count1, -1, 1)"], 53 | \ ['n' , 'vsh_start_of_next_command_block' , '' , ']]' , ' (vshStartOfNextCommandBlock)' , " :call vsh#vsh#CommandBlockEnds('n', v:count1, 1, 1)"], 54 | \ ['n' , 'vsh_end_of_command_block' , '' , '][' , ' (vshEndOfCommandBlock)' , " :call vsh#vsh#CommandBlockEnds('n', v:count1, 1, -1)"], 55 | \ ['n' , 'vsh_end_of_previous_command_block' , '' , '[]' , ' (vshEndOfPreviousCommandBlock)' , " :call vsh#vsh#CommandBlockEnds('n', v:count1, -1, -1)"], 56 | \ ['v' , 'vsh_start_of_command_block' , '' , '[[' , ' (vshStartOfCommandBlock)' , " :call vsh#vsh#CommandBlockEnds('v', v:count1, -1, 1)"], 57 | \ ['v' , 'vsh_start_of_next_command_block' , '' , ']]' , ' (vshStartOfNextCommandBlock)' , " :call vsh#vsh#CommandBlockEnds('v', v:count1, 1, 1)"], 58 | \ ['v' , 'vsh_end_of_command_block' , '' , '][' , ' (vshEndOfCommandBlock)' , " :call vsh#vsh#CommandBlockEnds('v', v:count1, 1, -1)"], 59 | \ ['v' , 'vsh_end_of_previous_command_block' , '' , '[]' , ' (vshEndOfPreviousCommandBlock)' , " :call vsh#vsh#CommandBlockEnds('v', v:count1, -1, -1)"], 60 | \ ['o' , 'vsh_start_of_command_block' , '' , '[[' , ' (vshStartOfCommandBlock)' , " V:call vsh#vsh#CommandBlockEnds('o', v:count1, -1, 1)"], 61 | \ ['o' , 'vsh_start_of_next_command_block' , '' , ']]' , ' (vshStartOfNextCommandBlock)' , " V:call vsh#vsh#CommandBlockEnds('o', v:count1, 1, 1)"], 62 | \ ['o' , 'vsh_end_of_command_block' , '' , '][' , ' (vshEndOfCommandBlock)' , " V:call vsh#vsh#CommandBlockEnds('o', v:count1, 1, -1)"], 63 | \ ['o' , 'vsh_end_of_previous_command_block' , '' , '[]' , ' (vshEndOfPreviousCommandBlock)' , " V:call vsh#vsh#CommandBlockEnds('o', v:count1, -1, -1)"] 64 | \ ] 65 | 66 | for [mapmode, overridevar, maptype, trigger, plugmap, final_expansion] in g:vsh_buffer_mappings_list 67 | execute mapmode . 'noremap ' . maptype . plugmap . final_expansion 68 | endfor 69 | 70 | command -range -nargs=1 -bang -complete=buffer VshSend :call vsh#vsh#VshSendCommand(, , , '') 71 | " Mappings have 'N' after them so that vim doesn't wait to see if this is the 72 | " 'Dedent' version of the mapping. 73 | let g:vsh_global_mappings_list = [ 74 | \ ['n' , 'vsh_SetSendbuf' , '' , 'vb' , ' VshSetSendbuf' , " vsh#vsh#SetSendbuf()"], 75 | \ ['n' , 'vsh_SendN' , '' , 'vs' , ' VshSendN' , " vsh#vsh#DoOperatorFunc(0)"], 76 | \ ['n' , 'vsh_SendDedent' , '' , 'vd' , ' VshSendDedent' , " vsh#vsh#DoOperatorFunc(1)"], 77 | \ ['n' , 'vsh_SendLineN' , '' , 'vss' , ' VshSendLineN' , " vsh#vsh#DoOperatorFunc(0) . '_'"], 78 | \ ['n' , 'vsh_SendLineDedent' , '' , 'vdd' , ' VshSendLineDedent' , " vsh#vsh#DoOperatorFunc(1) . '_'"], 79 | \ ['v' , 'vsh_SendVN' , '' , 'vs' , ' VshSendVN' , " :call vsh#vsh#VshVisualSend(visualmode(), 0)"] , 80 | \ ['v' , 'vsh_SendVDedent' , '' , 'vd' , ' VshSendVDedent' , " :call vsh#vsh#VshVisualSend(visualmode(), 1)"] , 81 | \ ['v' , 'vsh_SetSendbufV' , '' , 'vb' , ' VshSetSendbufV' , " vsh#vsh#SetSendbuf()"] 82 | \ ] 83 | for [mapmode, overridevar, maptype, trigger, plugmap, final_expansion] in g:vsh_global_mappings_list 84 | execute mapmode . 'noremap ' . maptype . plugmap . final_expansion 85 | endfor 86 | if !exists('g:vsh_no_default_mappings') 87 | for [mapmode, overridevar, maptype, trigger, plugmap, final_expansion] in g:vsh_global_mappings_list 88 | let final_trigger = get(g:, overridevar, trigger) 89 | execute mapmode . 'map ' . final_trigger . plugmap 90 | endfor 91 | endif 92 | 93 | let g:vsh_python_mappings_list = [ 94 | \ ['n' , 'vsh_PySendTerminated' , '' , 'vts' , ' VshPySendTerminated' , ' vsh#py#SendOperatorFunc(0, 1)'], 95 | \ ['n' , 'vsh_PySendDedentTerminated' , '' , 'vtd' , ' VshPySendDedentTerminated' , ' vsh#py#SendOperatorFunc(1, 1)'], 96 | \ ['n' , 'vsh_PySendN' , '' , 'vps' , ' VshPySendN' , ' vsh#py#SendOperatorFunc(0, 0)'], 97 | \ ['n' , 'vsh_PySendDedentN' , '' , 'vpd' , ' VshPySendDedentN' , ' vsh#py#SendOperatorFunc(1, 0)'], 98 | \ ['v' , 'vsh_PySendTerminated' , '' , 'vts' , ' VshPySendTerminated' , ' :call vsh#py#VisualSend(0, 1)'], 99 | \ ['v' , 'vsh_PySendDedentTerminated' , '' , 'vtd' , ' VshPySendDedentTerminated' , ' :call vsh#py#VisualSend(1, 1)'], 100 | \ ['v' , 'vsh_PySendN' , '' , 'vps' , ' VshPySendN' , ' :call vsh#py#VisualSend(0, 0)'], 101 | \ ['v' , 'vsh_PySendDedentN' , '' , 'vpd' , ' VshPySendDedentN' , ' :call vsh#py#VisualSend(1, 0)'], 102 | \ ] 103 | 104 | let g:vsh_autoload_did_mappings = 1 105 | -------------------------------------------------------------------------------- /syntax/vsh.vim: -------------------------------------------------------------------------------- 1 | if exists("b:current_syntax") 2 | finish 3 | endif 4 | let b:current_syntax = 1 5 | call vsh#vsh#DefaultColors() 6 | -------------------------------------------------------------------------------- /vsh-mode-1.0/integration_with_gdb.py: -------------------------------------------------------------------------------- 1 | import os 2 | import string 3 | import gdb 4 | 5 | def linespec_from_address(address): 6 | '''Take an address and return the pc_line from gdb.''' 7 | pos = gdb.find_pc_line(int(gdb.parse_and_eval(address).cast( 8 | gdb.lookup_type('char').pointer()))) 9 | if not pos.symtab: 10 | raise ValueError("Can't find address {}".format(address)) 11 | return pos 12 | 13 | 14 | def mark_position(sal, letter, editor_obj, delete=False): 15 | '''Given a gdb symtab and line object, mark its position with `letter`. 16 | 17 | `delete` determines what should happen when `sal` doesn't contain enough 18 | information to perform the action. 19 | It's a three way choice encoded with booleans and None. 20 | delete = False => raise ValueError() 21 | delete = True => remove mark in the editor instance 22 | delete = None => do nothing, just return 23 | 24 | ''' 25 | # Can only add this position if we know the filename. 26 | # The caller tells us whether they want to delete the mark otherwise or 27 | # just return. 28 | if not sal.symtab: 29 | if delete: 30 | remove_mark(letter, editor_obj) 31 | elif delete is None: 32 | return 33 | else: 34 | raise ValueError('sal has no symtab') 35 | full_filename = sal.symtab.fullname() 36 | add_mark(full_filename, sal.line, letter, editor_obj) 37 | 38 | 39 | class GoHere(gdb.Command): 40 | def __init__(self): 41 | super(GoHere, self).__init__('gohere', gdb.COMMAND_USER) 42 | 43 | def invoke(self, arg, _): 44 | self.dont_repeat() 45 | args = gdb.string_to_argv(arg) 46 | 47 | address, open_method = gohere_args_parse(args) 48 | pos = linespec_from_address(address) 49 | gohere_editor_implementation(open_method, pos) 50 | 51 | 52 | class ShowHere(gdb.Command): 53 | def __init__(self): 54 | super(ShowHere, self).__init__('showhere', gdb.COMMAND_USER) 55 | 56 | # N.b. This naturally adjusts based on selected frame *except* when one 57 | # frame is inlined into the other. In that case when going up the stack we 58 | # see the same $pc and hence jump to the same spot. 59 | # TODO Would be nice to account for that. 60 | def invoke(self, arg, _): 61 | args = gdb.string_to_argv(arg) 62 | if len(args) > 1: 63 | raise ValueError('Usage: showhere [address]') 64 | address = '$pc' if not args else args[0] 65 | pos = linespec_from_address(address) 66 | showhere_editor_implementation(pos) 67 | 68 | 69 | # TODO Have a look at putting this in the quickfix list / emacs search buffer 70 | # instead of storing as marks. 71 | class MarkStack(gdb.Command): 72 | def __init__(self): 73 | super(MarkStack, self).__init__('mark-stack', gdb.COMMAND_USER) 74 | 75 | def invoke(self, arg, _): 76 | self.dont_repeat() 77 | if len(gdb.string_to_argv(arg)): 78 | raise ValueError('mark-stack takes no arguments') 79 | frame = gdb.selected_frame() 80 | m_f_assocs = [] 81 | marks_to_clear = None 82 | for mark in string.ascii_uppercase: 83 | # TODO Sometimes strange things are happening: 84 | # frame.older().pc() == frame.pc() 85 | # frame.older().find_sal().pc == 0 != frame.find_sal().pc 86 | # frame.find_sal().line != frame.older().find_sal().line 87 | # frame.name() != frame.older().name() 88 | # Example problem is in gdb 89 | # #0 c_val_print_array (options=0x7fffffffdde0, original_value=, recurse=, stream=0xf62f40, address=, embedded_offset=0, valaddr=0x172efa0 "u_savecommon", type=) at /home/matthew/share/repos/gdb-source/gdb/c-valprint.c:307 90 | # #1 c_val_print (type=, embedded_offset=0, address=, stream=0xf62f40, recurse=, original_value=, options=0x7fffffffdde0) at /home/matthew/share/repos/gdb-source/gdb/c-valprint.c:511 91 | # #2 0x0000000000697420 in val_print (type=0x0, type@entry=0x18b3870, embedded_offset=0, address=25901168, address@entry=0, stream=stream@entry=0xf62f40, recurse=recurse@entry=0, val=val@entry=0x1811bd0, options=0x7fffffffde90, language=0x88f0e0 ) at /home/matthew/share/repos/gdb-source/gdb/valprint.c:1120 92 | # #3 0x0000000000558eec in c_value_print (val=0x1811bd0, stream=0xf62f40, options=) at /home/matthew/share/repos/gdb-source/gdb/c-valprint.c:698 93 | # #4 0x00000000006273a8 in print_value (val=val@entry=0x1811bd0, fmtp=fmtp@entry=0x7fffffffdfa0) at /home/matthew/share/repos/gdb-source/gdb/printcmd.c:1233 94 | # #5 0x000000000062743e in print_command_1 (exp=, voidprint=1) at /home/matthew/share/repos/gdb-source/gdb/printcmd.c:1261 95 | # #6 0x0000000000495133 in cmd_func (cmd=0xe85630, args=0xe0d816 "$_function_of(&u_savecommon)", from_tty=1) at /home/matthew/share/repos/gdb-source/gdb/cli/cli-decode.c:1888 96 | # #7 0x00000000006837b3 in execute_command (p=, p@entry=0xe0d810 "print $_function_of(&u_savecommon)", from_tty=1) at /home/matthew/share/repos/gdb-source/gdb/top.c:674 97 | # #8 0x00000000005b1abc in command_handler (command=0xe0d810 "print $_function_of(&u_savecommon)") at /home/matthew/share/repos/gdb-source/gdb/event-top.c:590 98 | # #9 0x00000000005b1d88 in command_line_handler (rl=) at /home/matthew/share/repos/gdb-source/gdb/event-top.c:780 99 | # #10 0x00000000005b10fc in gdb_rl_callback_handler (rl=0x18b89f0 "print $_function_of(&u_savecommon)") at /home/matthew/share/repos/gdb-source/gdb/event-top.c:213 100 | # #11 0x00000000006c4463 in rl_callback_read_char () at /home/matthew/share/repos/gdb-source/readline/callback.c:220 101 | # #12 0x00000000005b103e in gdb_rl_callback_read_char_wrapper_noexcept () at /home/matthew/share/repos/gdb-source/gdb/event-top.c:175 102 | # #13 0x00000000005b10a9 in gdb_rl_callback_read_char_wrapper (client_data=) at /home/matthew/share/repos/gdb-source/gdb/event-top.c:192 103 | # #14 0x00000000005b15d0 in stdin_event_handler (error=, client_data=0xd95c10) at /home/matthew/share/repos/gdb-source/gdb/event-top.c:518 104 | # #15 0x00000000005b044d in gdb_wait_for_event (block=block@entry=0) at /home/matthew/share/repos/gdb-source/gdb/event-loop.c:859 105 | # #16 0x00000000005b0587 in gdb_do_one_event () at /home/matthew/share/repos/gdb-source/gdb/event-loop.c:322 106 | # #17 0x00000000005b0725 in gdb_do_one_event () at /home/matthew/share/repos/gdb-source/gdb/common/common-exceptions.h:221 107 | # #18 start_event_loop () at /home/matthew/share/repos/gdb-source/gdb/event-loop.c:371 108 | # #19 0x0000000000602fb8 in captured_command_loop (data=data@entry=0x0) at /home/matthew/share/repos/gdb-source/gdb/main.c:325 109 | # #20 0x00000000005b2693 in catch_errors (func=func@entry=0x602f90 , func_args=func_args@entry=0x0, errstring=errstring@entry=0x832059 "", mask=mask@entry=RETURN_MASK_ALL) at /home/matthew/share/repos/gdb-source/gdb/exceptions.c:236 110 | # #21 0x0000000000603eae in captured_main (data=0x7fffffffe280) at /home/matthew/share/repos/gdb-source/gdb/main.c:1148 111 | # #22 gdb_main (args=args@entry=0x7fffffffe3a0) at /home/matthew/share/repos/gdb-source/gdb/main.c:1158 112 | # #23 0x000000000040ec95 in main (argc=, argv=) at /home/matthew/share/repos/gdb-source/gdb/gdb.c:32 113 | # 114 | # Find out why. 115 | # (maybe something to do with tail-call optimisation?) 116 | # This is why I use frame.find_sal() instead of 117 | # gdb.find_pc_line(frame.pc()) 118 | # (that and it's a cleaner way to reference it) 119 | 120 | # Here we mark c_val_print_array() and c_val_print() in the same 121 | # position. 122 | pc_pos = frame.find_sal() 123 | m_f_assocs.append((mark, pc_pos)) 124 | frame = frame.older() 125 | if frame is None: 126 | # Clear all remaining marks (to make sure the user doesn't get 127 | # confused about what marks are from this run and what marks 128 | # are from previous runs). 129 | marks_to_clear = string.ascii_uppercase[ 130 | string.ascii_uppercase.find(mark) + 1:] 131 | break 132 | if marks_to_clear is None: 133 | print('WARNING: Ran out of marks to use,', 134 | 'frames lower than {} are not marked'.format( 135 | len(string.ascii_uppercase))) 136 | marks_to_clear = '' 137 | mark_stack_editor_implementation(m_f_assocs, marks_to_clear) 138 | 139 | 140 | class MarkThis(gdb.Command): 141 | def __init__(self): 142 | super(MarkThis, self).__init__('mark-this', gdb.COMMAND_USER) 143 | 144 | def invoke(self, arg, _): 145 | self.dont_repeat() 146 | args = gdb.string_to_argv(arg) 147 | address = '$pc' if len(args) < 2 else args[-1] 148 | if not args: 149 | raise ValueError('mark-this must be told which mark to set') 150 | mark_letter = args[0] 151 | mark_this_editor_implementation(mark_letter, address) 152 | 153 | 154 | if os.getenv('VSH_EMACS_BUFFER'): 155 | import subprocess as sp 156 | MarkStack.__doc__ = '''Save each location of the current stack in registers A-Z. 157 | 158 | Can then jump to each of the relevant locations easily with 159 | `jump-to-register' in emacs. 160 | 161 | Usage: 162 | mark-stack 163 | 164 | ''' 165 | GoHere.__doc__ = '''View the current position in emacs buffer. 166 | 167 | Opens up the file using `emacsclient --reuse-frame', so configuration of how 168 | to open up the window could be done with the emacs variable `server-window'. 169 | 170 | Address can be specified in any way that GDB recognises, the default is the 171 | "current position" as determined by $pc. 172 | 173 | Usage: 174 | gohere [address] 175 | 176 | ''' 177 | ShowHere.__doc__ = '''View position in *another* emacs buffer. 178 | 179 | Opens up the file similar to `emacsclient --reuse-frame', except that the 180 | current window will not change the buffer and instead some other window will 181 | be chosen. User configuration of which window to select can be done with 182 | the emacs variable `server-window'. 183 | 184 | Other than that, behaves the same as `gohere'. 185 | 186 | Usage: 187 | showhere [address] 188 | 189 | ''' 190 | MarkThis.__doc__ = '''Store given address under emacs register provided. 191 | 192 | If no address given, mark the current position given by $pc. 193 | 194 | Usage: 195 | mark-this [A-Z0-9] [address] 196 | 197 | ''' 198 | VALID_MARKS = string.ascii_uppercase + string.digits 199 | def emacsclient_args(unique_part): 200 | return ['emacsclient', '--no-wait', '--reuse-frame'] + unique_part 201 | 202 | def gohere_args_parse(args): 203 | address = '$pc' if len(args) < 1 else args[1] 204 | return address, None 205 | 206 | def gohere_editor_implementation(_, pos): 207 | fullname = os.path.abspath(pos.symtab.fullname()) 208 | sp.check_output(emacsclient_args(['+{}'.format(pos.line), fullname])) 209 | 210 | def remove_mark(letter, _): 211 | lisp = '(setf (alist-get ?{} register-alist nil t) nil)'.format(letter) 212 | sp.check_output(emacsclient_args(['--eval', lisp])) 213 | 214 | def add_mark(filename, linenum, letter, _): 215 | lisp = '(vsh--add-mark "{}" {} ?{})'.format( 216 | filename, linenum, letter) 217 | sp.check_output(emacsclient_args(['--eval', lisp])) 218 | 219 | def showhere_editor_implementation(pos): 220 | fullname = os.path.abspath(pos.symtab.fullname()) 221 | lisp = '(vsh--gdb-integration-showhere "{}" {})'.format( 222 | fullname, pos.line) 223 | sp.check_output(emacsclient_args(['--eval', lisp])) 224 | 225 | def mark_this_editor_implementation(mark_letter, address): 226 | if len(mark_letter) != 1 or mark_letter not in string.ascii_uppercase + string.digits: 227 | raise ValueError('mark-this mark should be a single uppercase letter') 228 | pos = linespec_from_address(address) 229 | try: 230 | mark_position(pos, mark_letter, None, False) 231 | except ValueError: 232 | print('Not enough debug information.') 233 | print("Can't find source code location for pc {}".format(address)) 234 | 235 | def mark_stack_editor_implementation(m_f_assocs, marks_to_clear): 236 | for mark, pc_pos in m_f_assocs: 237 | # If we don't know the filename, clear this mark. 238 | # If we didn't clear the mark, then neovim would end up with a 239 | # confusing set of marks. 240 | mark_position(pc_pos, mark, None, True) 241 | for mark in marks_to_clear: 242 | remove_mark(mark, None) 243 | 244 | else: 245 | MarkStack.__doc__ = '''Put marks A-Z on each of the current stack. 246 | 247 | Can then jump to each of the relevant functions reasonably easily. 248 | 249 | Usage: 250 | mark-stack 251 | 252 | ''' 253 | GoHere.__doc__ = '''View the current position in a vim buffer. 254 | 255 | You can specify how to open the other file with arguments between the 256 | address and the command. Default address is "current" as determined by $pc. 257 | Note that using 'edit' on large files with syntax highlighting and folding 258 | will take longer than using 'buffer'. 259 | 260 | The 'default' open method is specially handled by `gohere` to search for a 261 | window marked with the window-local variable `w:gdb_view` and use that if 262 | it exists. If no window is marked, then default will go to that position in 263 | the current window. 264 | 265 | Usage: 266 | # If there is a window with w:gdb_view set go there before moving to 267 | # current window. 268 | # Otherwise, 269 | gohere [default] [address] 270 | # Use current window 271 | gohere e [address] 272 | # Use vertical split window 273 | gohere vnew [address] 274 | # Use horizontal split window 275 | gohere new [address] 276 | 277 | Examples: 278 | gohere 279 | gohere default some_function 280 | gohere d some_function 281 | gohere vnew 282 | gohere vnew some_function 283 | 284 | ''' 285 | ShowHere.__doc__ = '''Run `gohere` with default arguments, and return to the current window. 286 | 287 | Usage: 288 | showhere [address] 289 | 290 | ''' 291 | MarkThis.__doc__ = '''Put the given mark at the address of the given location. 292 | 293 | If no address is given, then mark the position in source code where the 294 | current stage of execution is. 295 | 296 | Usage: 297 | mark-this [A-Z] [address] 298 | 299 | ''' 300 | def gohere_args_parse(args): 301 | address = '$pc' if len(args) < 2 else args[-1] 302 | if not args: 303 | open_method = 'default' 304 | elif len(args) == 1: 305 | open_method = args[0] 306 | else: 307 | open_method = ' '.join(args[:-1]) 308 | return address, open_method 309 | 310 | if os.getenv('NVIM') or os.getenv('NVIM_LISTEN_ADDRESS'): 311 | import pynvim 312 | def get_nvim_instance(): 313 | nvim_socket_path = os.getenv('NVIM') 314 | if not nvim_socket_path: 315 | nvim_socket_path = os.getenv('NVIM_LISTEN_ADDRESS') 316 | if not nvim_socket_path: 317 | raise OSError('No socket path NVIM_LISTEN_ADDRESS in environment') 318 | return pynvim.attach('socket', path=nvim_socket_path) 319 | 320 | def find_marked_window(nvim): 321 | for win in nvim.current.tabpage.windows: 322 | if win.vars.get('gdb_view'): 323 | return win 324 | return None 325 | 326 | def direct_goto(nvim, name): 327 | '''Returns 'buffer' if `name` is the name of a valid neovim buffer, 328 | otherwise returns 'edit' ''' 329 | fullname = os.path.abspath(name) 330 | for buf in nvim.buffers: 331 | if buf.name == fullname: 332 | return 'buffer' 333 | return 'edit' 334 | 335 | def gohere_editor_implementation(open_method, pos): 336 | nvim = get_nvim_instance() 337 | 338 | if open_method == 'default': 339 | win = find_marked_window(nvim) 340 | if win: 341 | nvim.command('{} wincmd w'.format(win.number)) 342 | open_method = direct_goto(nvim, pos.symtab.fullname()) 343 | 344 | nvim.command('{} +{} {}'.format(open_method, pos.line, 345 | os.path.abspath(pos.symtab.fullname()))) 346 | nvim.command('silent! {}foldopen!'.format(pos.line)) 347 | 348 | def showhere_editor_implementation(pos): 349 | nvim = get_nvim_instance() 350 | curwin = nvim.current.window 351 | marked_win = find_marked_window(nvim) 352 | if not marked_win: 353 | num = None 354 | tabwindows = list(nvim.current.tabpage.windows) 355 | if curwin.number != 1: 356 | num = curwin.number - 1 357 | tabwindows[curwin.number - 2].vars['gdb_view'] = 1 358 | else: 359 | try: 360 | tabwindows[curwin.number].vars['gdb_view'] = 1 361 | num = curwin.number + 1 362 | except IndexError: 363 | nvim.command('wincmd v') 364 | nvim.current.window.vars['gdb_view'] = 1 365 | nvim.command('wincmd w') 366 | num = curwin.number 367 | print('No marked window, choosing window #{}'.format(num), 368 | 'and marking with w:gdb_view for future') 369 | 370 | gohere_editor_implementation('default', pos) 371 | nvim.command('{} wincmd w'.format(curwin.number)) 372 | 373 | def remove_mark(letter, nvim): 374 | nvim.command('delmarks {}'.format(letter)) 375 | 376 | def add_mark(filename, linenum, letter, nvim): 377 | # Doesn't matter if the buffer has already been loaded. 378 | # `badd` doesn't do anything if it has. 379 | nvim.command('badd {}'.format(filename)) 380 | bufnr = nvim.funcs.bufnr(filename) 381 | nvim.funcs.setpos("'{}".format(letter), 382 | [bufnr, linenum, 0, 0]) 383 | 384 | def mark_this_editor_implementation(mark_letter, address): 385 | if len(mark_letter) != 1 or mark_letter not in string.ascii_uppercase: 386 | raise ValueError('mark-this mark should be a single uppercase letter') 387 | pos = linespec_from_address(address) 388 | nvim = get_nvim_instance() 389 | try: 390 | mark_position(pos, mark_letter, nvim, False) 391 | except ValueError: 392 | print('Not enough debug information.') 393 | print("Can't find source code location for pc {}".format(address)) 394 | 395 | def mark_stack_editor_implementation(m_f_assocs, marks_to_clear): 396 | nvim = get_nvim_instance() 397 | for mark, pc_pos in m_f_assocs: 398 | # If we don't know the filename, clear this mark. 399 | # If we didn't clear the mark, then neovim would end up with a 400 | # confusing set of marks. 401 | mark_position(pc_pos, mark, nvim, True) 402 | if marks_to_clear: 403 | nvim.command('delmarks {}'.format(marks_to_clear)) 404 | else: 405 | import socket 406 | import json 407 | import re 408 | from contextlib import contextmanager 409 | 410 | @contextmanager 411 | def vim_socket(): 412 | origvim_socket_addr = os.getenv('VSH_VIM_LISTEN_ADDRESS') 413 | m = re.match(r'localhost:(\d+)', origvim_socket_addr) 414 | assert(m) 415 | sock = socket.socket() 416 | sock.connect(('localhost', int(m.groups()[0]))) 417 | try: 418 | yield sock 419 | finally: 420 | sock.shutdown(socket.SHUT_RDWR) 421 | sock.close() 422 | 423 | def run_vim_command(sock, args): 424 | message = json.dumps(args).encode('utf8') 425 | sock.send(message) 426 | 427 | def run_vim_command_direct(args): 428 | with vim_socket() as sock: 429 | run_vim_command(sock, args) 430 | 431 | def gohere_editor_implementation(open_method, pos): 432 | gohere_args = [open_method, 433 | os.path.abspath(pos.symtab.fullname()), 434 | pos.line] 435 | args = ['call', 'vsh#gdb#gohere', gohere_args] 436 | run_vim_command_direct(args) 437 | 438 | def showhere_editor_implementation(pos): 439 | showhere_args = [os.path.abspath(pos.symtab.fullname()), 440 | pos.line] 441 | args = ['call', 'vsh#gdb#showhere', showhere_args] 442 | run_vim_command_direct(args) 443 | 444 | def remove_mark(letter, sock): 445 | run_vim_command(sock, ['ex', 'delmarks {}'.format(letter)]) 446 | 447 | def add_mark(filename, linenum, letter, sock): 448 | run_vim_command(sock, ['call', 'vsh#gdb#add_mark', 449 | [filename, linenum, letter]]) 450 | 451 | def mark_this_editor_implementation(mark_letter, address): 452 | if len(mark_letter) != 1 or mark_letter not in string.ascii_uppercase: 453 | raise ValueError('mark-this mark should be a single uppercase letter') 454 | pos = linespec_from_address(address) 455 | try: 456 | with vim_socket() as sock: 457 | mark_position(pos, mark_letter, sock, False) 458 | except ValueError: 459 | print('Not enough debug information.') 460 | print("Can't find source code location for pc {}".format(address)) 461 | 462 | def mark_stack_editor_implementation(m_f_assocs, marks_to_clear): 463 | with vim_socket() as sock: 464 | for mark, pc_pos in m_f_assocs: 465 | # If we don't know the filename, clear this mark. 466 | # If we didn't clear the mark, then neovim would end up with a 467 | # confusing set of marks. 468 | mark_position(pc_pos, mark, sock, True) 469 | if marks_to_clear: 470 | run_vim_command(sock, ['ex', 'delmarks {}'.format(marks_to_clear)]) 471 | 472 | 473 | GoHere() 474 | MarkStack() 475 | ShowHere() 476 | MarkThis() 477 | -------------------------------------------------------------------------------- /vsh-mode-1.0/vsh-elisp-tests.el: -------------------------------------------------------------------------------- 1 | ;; vsh-elisp-tests.el --- Generated tests for vsh.el -*- lexical-binding: t; -*- 2 | (require 'vsh-mode) 3 | ;; TODO (Maybe) ??? 4 | ;; Feels a little bit of a waste to test on *every* point in the line. 5 | ;; I *could* choose to only run on a random point in the line to reduce this 6 | ;; but maintain the same testing in the long-term. 7 | (defmacro vsh--test-on-linespecs (&rest body) 8 | "Run `body' for each point in each line given in `vsh--internal-testing-lines'. 9 | 10 | Inserts the following local variables in the scope for `body' to use: 11 | - `promptend-offset': Index into line where the prompt ends. 12 | - `no-whitespace-offset': Index of line where prompt without whitespace starts. 13 | - `line': The current line that `body' is running on. 14 | - `linetype': Symbol representing line type, one of `output', `command', 15 | `comment', `saved-command' `empty-comment'. 16 | - `reset-test': Function to reset buffer text to current line only. 17 | - `idx': Index into line that point was before body." 18 | (let ((linespec (make-symbol "linespec")) 19 | (idx (make-symbol "idx"))) 20 | `(with-temp-buffer 21 | (dolist (,linespec vsh--internal-testing-lines) 22 | (erase-buffer) 23 | (let ((promptend-offset (car ,linespec)) 24 | (no-whitespace-offset (cadr ,linespec)) 25 | (line (caddr ,linespec)) 26 | (linetype (cadddr ,linespec))) 27 | (insert line) 28 | (dotimes (idx (point-max)) 29 | (let ((,idx (1+ idx))) 30 | (cl-flet ((reset-test () 31 | (erase-buffer) 32 | (insert line) 33 | (goto-char idx))) 34 | (reset-test) 35 | ,@body)))))))) 36 | 37 | (defvar vsh--internal-testing-lines 38 | '((0 0 "Test output line" output) 39 | (10 10 "vshcmd: > command no space" command) 40 | (12 10 "vshcmd: > command with space" command) 41 | (10 10 "vshcmd: > ## double hash command with space" command) 42 | (12 10 "vshcmd: > ## double hash command space before" command) 43 | (13 13 "vshcmd: > # space before hash" comment) 44 | (13 13 "vshcmd: > # " comment) 45 | (12 12 "vshcmd: > #" comment) 46 | (12 12 "vshcmd: > # " comment) 47 | (11 11 "vshcmd: > #" comment) 48 | (15 13 "vshcmd: > # " comment) 49 | (14 12 "vshcmd: > # " comment) 50 | (12 12 "vshcmd: > #space before hash, not after hash" comment) 51 | (12 12 "vshcmd: > # comment no space" comment) 52 | (11 11 "vshcmd: > #comment no space" comment) 53 | (13 12 "vshcmd: > # comment with space" comment) 54 | (22 22 "vshcmd: > # vshcmd: > deactivated command" saved-command) 55 | (23 22 "vshcmd: > # vshcmd: > deactivated with space" saved-command) 56 | (9 9 "vshcmd: >" empty-comment) ; Comment with no hash (because no space) 57 | (0 0 "vshcmd: " output) ; Output line -- not quite a prompt. 58 | (10 10 "vshcmd: > " command) ; Command line no text. 59 | (12 10 "vshcmd: > " command) ; Command line no text. 60 | )) 61 | 62 | (ert-deftest vsh-bol-test () 63 | "Testing `vsh-bol' always moves cursor to relevant position." 64 | (vsh--test-on-linespecs 65 | (vsh-bol) 66 | (should (eq (point) (1+ promptend-offset))))) 67 | 68 | (ert-deftest vsh-nop-discard-test () 69 | "Testing `vsh-line-discard' for those positions where it is a nop." 70 | (vsh--test-on-linespecs 71 | (cl-flet ((tester (loc index text) 72 | (if (<= index loc) 73 | (should (equal (buffer-string) text)) 74 | (should (equal (buffer-string) 75 | (string-join 76 | (list (substring text 0 loc) 77 | (substring text (1- index))))))))) 78 | (vsh-line-discard nil) 79 | (tester promptend-offset idx line) 80 | (reset-test) 81 | (vsh-line-discard t) 82 | (tester no-whitespace-offset idx line) 83 | (reset-test)))) 84 | 85 | (ert-deftest vsh-activate-deactivate-test () 86 | "Testing `vsh-save-command' and `vsh-activate-command' on lines." 87 | (cl-flet ((test-activate (linetype text) 88 | (vsh-activate-command) 89 | (cl-case linetype 90 | (saved-command 91 | (should 92 | (equal (buffer-string) 93 | (substring text (length (vsh--comment-header)))))) 94 | (t (should (equal (buffer-string) text))))) 95 | (test-save (linetype text) 96 | (vsh-save-command) 97 | (cl-case linetype 98 | (command 99 | (should 100 | (equal (buffer-string) 101 | (string-join (list (vsh--comment-header) text))))) 102 | (t (should (equal (buffer-string) text)))))) 103 | (vsh--test-on-linespecs 104 | (test-activate linetype line) 105 | (test-save linetype line) 106 | (reset-test) 107 | (test-save linetype line) 108 | (test-activate linetype line)))) 109 | 110 | (ert-deftest vsh-mark-segment () 111 | "Testing `vsh-mark-segment' for different lines." 112 | (let ((existing-output "test\noutput\nlines") 113 | (prompt-at-bottom "\nvshcmd: > bottom-command")) 114 | (with-temp-buffer 115 | (dolist (linespec vsh--internal-testing-lines) 116 | (erase-buffer) 117 | (insert existing-output) 118 | (let ((output-start (copy-marker (point-min) t)) 119 | (output-end (point-max-marker)) 120 | (line (caddr linespec)) 121 | (linetype (cadddr linespec))) 122 | (cl-flet ((test-mark-and-point (inc-command goto-pos) 123 | (goto-char goto-pos) 124 | (vsh-mark-segment inc-command) 125 | (should (eql (point) (marker-position output-end))) 126 | ;; N.b. this works for both there being a comment at the 127 | ;; start and not because when there is not a comment at 128 | ;; the start we have (eql (marker-position output-start) 129 | ;; (point-min)) 130 | ;; and both of those are correct for the non-output lines. 131 | (cl-case linetype 132 | (output (should (eql (mark) (point-min)))) 133 | (t (should (eql (mark) 134 | (if inc-command (point-min) 135 | (marker-position output-start)))))) 136 | (deactivate-mark))) 137 | (dolist (to-insert (list "" (string-join (list line "\n")))) 138 | ;; Test with and without a command line at the very start. 139 | (goto-char (point-min)) 140 | (insert to-insert) 141 | (dolist (cur-pos 142 | (list (marker-position output-start) 143 | (marker-position output-end) 144 | (point-min) 145 | ;; Random position in the first line. 146 | ;; N.b. slightly favours start of line since `random' 147 | ;; can give both 0 or 1 which both give very start of 148 | ;; buffer. That's fine, and this is neater. 149 | (random (marker-position output-start)) 150 | ;; Random position in middle of output. 151 | (+ (marker-position output-start) 152 | (random (- (marker-position output-end) 153 | (marker-position output-start)))))) 154 | (test-mark-and-point nil cur-pos) 155 | (test-mark-and-point t cur-pos) 156 | (goto-char (point-max)) 157 | ;; Then try again with the prompt at the bottom. 158 | (insert prompt-at-bottom) 159 | (set-marker output-end (1+ (marker-position output-end))) 160 | (test-mark-and-point nil cur-pos) 161 | (test-mark-and-point t cur-pos) 162 | (set-marker output-end (1- (marker-position output-end))) 163 | (delete-region output-end (point-max)))))))))) 164 | 165 | ;; Gist of the interface for this macro is we need a list of function 166 | ;; definitions. Each of these are defined in a lexical scope which has a bunch 167 | ;; of markers defined (`start-output-start', `block-start', `block-end', 168 | ;; `end-output-end', `block-mid'). 169 | ;; 170 | ;; Then, these functions are called with a buffer set up, with a given linespec, 171 | ;; etc. 172 | ;; 173 | ;; One alternative is that we pass in a literal list of functions that all have 174 | ;; parameters for the above markers. This would be a little less "funky" in 175 | ;; terms of defining symbols for the function definitions to use, but it would 176 | ;; have a bit of extra baggage around having to define functions that take these 177 | ;; extra parameters. 178 | (defun vsh--with-standard-blocks (basic-buffer-func &rest each-linespec-funcs) 179 | ;; Test variants: 180 | ;; - Always start with output surrounding a command block (both top and 181 | ;; bottom). 182 | ;; - With and without prompt at top. 183 | ;; - With and without prompt at bottom. 184 | ;; - With "testing line" in middle of block, at start of block, at end of 185 | ;; block. 186 | ;; Want a cross-product of the three sets of variants. 187 | (let ((basic-block "vshcmd: > ls\nvshcmd: > ls\n") 188 | (output-block "test\noutput\nlines") 189 | (prompt-at-bottom "\nvshcmd: > bottom-command") 190 | (prompt-at-top "vshcmd: > top-command\n")) 191 | (with-temp-buffer 192 | (let ((start-output-start (point-marker)) 193 | (block-start (point-marker)) 194 | (block-end (point-marker)) 195 | (end-output-end (point-marker)) 196 | (block-mid (point-marker))) 197 | ;; Set up the buffer with initial text in it. 198 | ;; Also ensure each marker is at the relevant points. 199 | (cl-flet ((insert-at-marker (marker &rest text) 200 | (set-marker-insertion-type marker t) 201 | (goto-char marker) 202 | (dolist (textchunk text) 203 | (insert textchunk)) 204 | (set-marker-insertion-type marker nil))) 205 | (insert-at-marker end-output-end output-block) 206 | (insert-at-marker block-end basic-block) 207 | ;; 1+ to account for points starting at 1. 208 | (set-marker block-mid (1+ (length "vshcmd: > ls\n"))) 209 | (insert-at-marker block-start output-block "\n")) 210 | ;; Ensure that markers have the marker insertion type that I want. 211 | ;; Everything has `marker-insertion-type' of `nil' at the moment 212 | ;; (because that's the default from `point-marker' and we didn't change 213 | ;; anything with the insertion above. 214 | (set-marker-insertion-type block-end t) 215 | (set-marker-insertion-type start-output-start t) 216 | ;; Functions themselves handle the variants of: 217 | ;; - Special line at start of block. 218 | ;; - Special line in the middle of block. 219 | ;; - Special line at end of block. 220 | (cl-flet (;; To avoid some off-by-one mistakes. 221 | (random-point (limit) (1+ (random (1- limit))))) 222 | ;; Actual loop of tests. 223 | (dolist (bottom-insert-text (list "" prompt-at-bottom)) 224 | (goto-char (point-max)) 225 | ;; N.b. end-output-end has marker type `nil' so that will not move 226 | ;; if it points to the end of the buffer. 227 | (insert bottom-insert-text) 228 | (dolist (top-insert-text (list "" prompt-at-top)) 229 | (goto-char (point-min)) 230 | ;; N.b. start-output-start has marker type `nil' so that will move 231 | ;; forwards. 232 | (insert top-insert-text) 233 | (let ((position-list 234 | (list 235 | ;; Somewhere in the text above the main block. 236 | ;; Region should surround `top-insert-text' if that has 237 | ;; been inserted, otherwise should not be active. 238 | (lambda () (random-point (marker-position block-start))) 239 | ;; Very start of block -- should surround block, adjusted 240 | ;; according to possibly inserted line. 241 | (lambda () (marker-position block-start)) 242 | ;; Middle of block. Region should surround block adjusted 243 | ;; according to possibly inserted line. 244 | (lambda () (+ (marker-position block-start) 245 | (random (- (marker-position block-end) 246 | (marker-position block-start))))) 247 | ;; End of block (but still on last command). 248 | ;; Should surround block, adjusted according to possibly 249 | ;; inserted line. 250 | (lambda () (1- (marker-position block-end))) 251 | ;; Random position in output after block. 252 | (lambda () (+ (marker-position block-end) 253 | (random (- (marker-position end-output-end) 254 | (marker-position block-end))))) 255 | ;; Very end of buffer. 256 | ;; Surround block unless bottom line was added, if bottom 257 | ;; line added should surround that. 258 | (lambda () (marker-position end-output-end))))) 259 | (dolist (cur-pos position-list) 260 | (funcall basic-buffer-func 261 | cur-pos 262 | start-output-start block-start block-mid block-end 263 | end-output-end)) 264 | (dolist (func each-linespec-funcs) 265 | (dolist (cur-pos position-list) 266 | (dolist (linespec vsh--internal-testing-lines) 267 | (let ((line (caddr linespec)) 268 | (linetype (cadddr linespec))) 269 | ;; Currently deciding where to run tests from (i.e. what 270 | ;; positions), how to send that information, and how to 271 | ;; actually test that. 272 | (funcall func cur-pos line linetype 273 | start-output-start block-start block-mid 274 | block-end end-output-end)))))) 275 | (delete-region (point-min) start-output-start)) 276 | (delete-region end-output-end (point-max)))))))) 277 | 278 | 279 | (defun vsh--block-test-basic (base-func) 280 | (lambda 281 | (cur-pos start-output-start block-start block-mid block-end end-output-end) 282 | ;; Lie about linetype in order to get behaviour we want. 283 | (funcall base-func 284 | cur-pos nil 'command start-output-start block-start block-mid 285 | block-end end-output-end))) 286 | (defmacro vsh--block-test-end (testcall) 287 | `(lambda 288 | (cur-pos line linetype 289 | start-output-start block-start block-mid block-end 290 | end-output-end) 291 | ;; Assuming that `block-end' has `marker-insertion-type' `t'. 292 | (let ((orig-end (marker-position block-end))) 293 | (goto-char block-end) 294 | (insert (string-join (list line "\n"))) 295 | ,testcall 296 | (delete-region orig-end block-end)))) 297 | 298 | (defmacro vsh--block-test-start (call-sexp) 299 | `(lambda 300 | (cur-pos line linetype 301 | start-output-start block-start block-mid block-end end-output-end) 302 | ;; Assuming `block-start' has `marker-insertion-type' `nil'. 303 | (let ((offset (1+ (length line)))) 304 | (goto-char block-start) 305 | (insert (string-join (list line "\n"))) 306 | (let ((orig-block-start 307 | (+ (marker-position block-start) offset))) 308 | ,call-sexp 309 | (delete-region block-start orig-block-start))))) 310 | 311 | (defmacro vsh--block-test-mid (call-sexp) 312 | `(lambda 313 | (cur-pos line linetype 314 | start-output-start block-start block-mid block-end end-output-end) 315 | ;; Assuming `block-mid' has `marker-insertion-type' `nil'. 316 | (let ((offset (1+ (length line)))) 317 | (goto-char block-mid) 318 | (insert (string-join (list line "\n"))) 319 | ;; Only time we need to handle is when this line should 320 | ;; not count as part of a block. 321 | (let ((end-inserted-line (+ (marker-position block-mid) offset))) 322 | ,call-sexp 323 | (delete-region block-mid end-inserted-line))))) 324 | 325 | (defun vsh--back-motion-next-pt (pt element-list) 326 | ;; First point is at the very start of the buffer. 327 | ;; |vshcmd: > |here is a point 328 | ;; test 329 | ;; vshcmd: > |here is another point 330 | ;; | <-- last point is at the very end of the buffer 331 | ;; Requirement here is that `element-list' is in order (so we know 332 | ;; that the first position we start out *greater* than we will move 333 | ;; to). 334 | (let ((prev (point-min))) 335 | (while element-list 336 | (if (<= pt (car element-list)) 337 | (setq element-list nil) 338 | (setq prev (car element-list)) 339 | (setq element-list (cdr element-list)))) 340 | prev)) 341 | 342 | (defun vsh--fore-motion-next-pt (pt element-list) 343 | ;; Assumption here that we again have `element-list' in order. 344 | (or (cl-find-if (lambda (x) (< pt x)) element-list) 345 | (point-max))) 346 | (defun vsh--test-motion-func (cur-pos element-list motion-func &optional backwards) 347 | (let ((start-point (funcall cur-pos)) 348 | (element-list (cl-remove-duplicates element-list))) 349 | ;; (message "###\nPoint: %d\nElement-list: %s\nFunction: %s\nBackwards: %s\n%s" 350 | ;; start-point element-list motion-func backwards (buffer-string)) 351 | (should (not (= start-point 0))) 352 | (goto-char start-point) 353 | (funcall motion-func (if backwards 1 -1)) 354 | (should (eq (point) (vsh--back-motion-next-pt start-point element-list))) 355 | (goto-char start-point) 356 | (funcall motion-func (if backwards -1 1)) 357 | (should (eq (point) (vsh--fore-motion-next-pt start-point element-list))))) 358 | 359 | (defmacro vsh--motion-oracle (&rest body) 360 | `(let (ret last-line-prompt temp-var) 361 | (save-excursion 362 | (goto-char (point-min)) 363 | (while (not (eobp)) 364 | ,@body 365 | (forward-line))) 366 | (cons (point-min) (nreverse (cons (point-max) ret))))) 367 | 368 | (ert-deftest vsh-motion-oracle-tests () 369 | "Testing different motions. 370 | 371 | Motions tested are: 372 | - `vsh--beginning-of-block-fn' 373 | - `vsh-beginning-of-block' 374 | - `vsh--end-of-block-fn' 375 | - `vsh-end-of-block' 376 | - `vsh-prev-command' 377 | - `vsh-next-command'" 378 | (cl-flet* 379 | ((beginning-of-block-fn-points () 380 | (vsh--motion-oracle 381 | (if (not (looking-at-p (vsh-split-regexp))) 382 | (setq last-line-prompt nil) 383 | (when (not last-line-prompt) 384 | (setq ret (cons (point) ret))) 385 | (setq last-line-prompt t)))) 386 | 387 | (beginning-of-block-points () 388 | (vsh--motion-oracle 389 | (if (not (looking-at-p (vsh-split-regexp))) 390 | (setq last-line-prompt nil) 391 | (when (not last-line-prompt) 392 | (setq ret (cons (car (vsh--line-beginning-position)) 393 | ret))) 394 | (setq last-line-prompt t)))) 395 | 396 | (end-of-block-fn-points () 397 | (vsh--motion-oracle 398 | (if (looking-at-p (vsh-split-regexp)) 399 | (setq last-line-prompt t) 400 | (when last-line-prompt 401 | (setq ret (cons (point) ret))) 402 | (setq last-line-prompt nil)))) 403 | 404 | (end-of-block-points () 405 | (vsh--motion-oracle 406 | (if (looking-at-p (vsh-split-regexp)) 407 | (if (= (line-end-position) (point-max)) 408 | (setq ret (cons (car (vsh--line-beginning-position)) ret)) 409 | (setq temp-var (car (vsh--line-beginning-position)) 410 | last-line-prompt t)) 411 | (when last-line-prompt 412 | (setq ret (cons temp-var ret))) 413 | (setq last-line-prompt nil)))) 414 | 415 | (get-command-prompt-starts () 416 | (vsh--motion-oracle 417 | (when (looking-at (vsh-motion-marker)) 418 | (setq ret (cons (match-end 2) ret))))) 419 | 420 | (test-beg-block-fn (cur-pos) (vsh--test-motion-func 421 | cur-pos (beginning-of-block-fn-points) 422 | #'vsh--beginning-of-block-fn 'backwards)) 423 | (test-beg-block-cmd (cur-pos) (vsh--test-motion-func 424 | cur-pos (beginning-of-block-points) 425 | #'vsh-beginning-of-block 'backwards)) 426 | (test-end-block-fn (cur-pos) (vsh--test-motion-func 427 | cur-pos (end-of-block-fn-points) 428 | #'vsh--end-of-block-fn)) 429 | (test-end-block-cmd (cur-pos) (vsh--test-motion-func 430 | cur-pos (end-of-block-points) 431 | #'vsh-end-of-block)) 432 | (test-prev-cmd (cur-pos) (vsh--test-motion-func 433 | cur-pos (get-command-prompt-starts) 434 | #'vsh-prev-command 'backwards)) 435 | (test-next-cmd (cur-pos) (vsh--test-motion-func 436 | cur-pos (get-command-prompt-starts) 437 | #'vsh-next-command))) 438 | (let (ret 439 | (func-tester-list 440 | (list #'test-beg-block-fn #'test-beg-block-cmd 441 | #'test-end-block-fn #'test-end-block-cmd 442 | #'test-prev-cmd #'test-next-cmd))) 443 | (dolist (func-tester func-tester-list) 444 | (setq ret (cons (vsh--block-test-end (funcall func-tester cur-pos)) ret)) 445 | (setq ret (cons (vsh--block-test-start (funcall func-tester cur-pos)) ret)) 446 | (setq ret (cons (vsh--block-test-mid (funcall func-tester cur-pos)) ret))) 447 | (setq ret (cons (lambda (cur-pos &rest _args) 448 | (dolist (func-tester func-tester-list) 449 | (funcall func-tester cur-pos))) 450 | ret)) 451 | (apply #'vsh--with-standard-blocks ret)))) 452 | 453 | 454 | (defun vsh--selection-for-point (pt element-list) 455 | ;; `vsh-mark-command-block' behaves like `mark-defun' in that if we are below 456 | ;; a function we select that function, but if we are above all functions we 457 | ;; select the first function. Hence the `or' here. 458 | ;; 459 | ;; If there are any regions that we are *in*, choose that. 460 | (or (cl-find-if (lambda (x) (and (>= pt (car x)) (<= pt (cdr x)))) 461 | element-list) 462 | ;; If we are before any region, choose the first region (because that's 463 | ;; what `mark-defun' does). 464 | (when (< pt (caar element-list)) 465 | (car element-list)) 466 | ;; Otherwise, choose the last region we are *after*. 467 | (cl-find-if (lambda (x) (> pt (cdr x))) (reverse element-list)))) 468 | (defun vsh--test-selection-func (cur-pos region-list selection-func) 469 | (let ((start-point (funcall cur-pos)) 470 | (element-list (cl-remove-duplicates region-list))) 471 | ;; (message "###\nPoint: %d\nElement-list: %s\nFunction: %s\n%s" 472 | ;; start-point element-list selection-func (buffer-string)) 473 | (should (not (= start-point 0))) 474 | (goto-char start-point) 475 | (funcall selection-func) 476 | ;; (message "After: point: %s\nmark: %s" (point) (mark)) 477 | (let ((s-region (vsh--selection-for-point start-point element-list))) 478 | ;; (message "s-region: %s" s-region) 479 | (should (eq (point) (car s-region))) 480 | (should (eq (mark) (cdr s-region))) 481 | (should (region-active-p))) 482 | (deactivate-mark))) 483 | 484 | (defmacro vsh--selection-oracle (&rest body) 485 | `(let (ret last-line-prompt temp-var) 486 | (save-excursion 487 | (goto-char (point-min)) 488 | (while (not (eobp)) 489 | ,@body 490 | (forward-line))) 491 | ;; When there is no *end* to the region, assume this block reaches the last 492 | ;; point in the buffer. 493 | (unless (consp (car ret)) 494 | (setf (car ret) (cons (car ret) (point-max)))) 495 | (nreverse ret))) 496 | (ert-deftest vsh-selection-oracle-tests () 497 | "Testing different selections. 498 | 499 | Selections tested are: 500 | - `vsh-mark-command-block'" 501 | (cl-flet* 502 | ((mark-command-block-points (inc-comments) 503 | (let ((regexp 504 | (if inc-comments (vsh-split-regexp) 505 | (vsh-command-regexp)))) 506 | (vsh--selection-oracle 507 | (if (not (looking-at-p regexp)) 508 | (progn 509 | (when last-line-prompt 510 | (cl-assert (not (consp (car ret)))) 511 | (setf (car ret) (cons (car ret) (point)))) 512 | (setq last-line-prompt nil)) 513 | (when (not last-line-prompt) 514 | (cl-assert (or (not ret) (consp (car ret))) t) 515 | (setq ret (cons (point) ret))) 516 | (setq last-line-prompt t))))) 517 | 518 | 519 | (test-select-defun (cur-pos) (vsh--test-selection-func 520 | cur-pos (mark-command-block-points nil) 521 | (lambda () (vsh-mark-command-block nil)))) 522 | (test-select-cmdblock (cur-pos) (vsh--test-selection-func 523 | cur-pos (mark-command-block-points t) 524 | (lambda () (vsh-mark-command-block t))))) 525 | (let (ret (func-tester-list (list #'test-select-defun))) 526 | (dolist (func-tester func-tester-list) 527 | (setq ret (cons (vsh--block-test-end (funcall func-tester cur-pos)) ret)) 528 | (setq ret (cons (vsh--block-test-start (funcall func-tester cur-pos)) ret)) 529 | (setq ret (cons (vsh--block-test-mid (funcall func-tester cur-pos)) ret))) 530 | (setq ret (cons (lambda (cur-pos &rest _args) 531 | (dolist (func-tester func-tester-list) 532 | (funcall func-tester cur-pos))) 533 | ret)) 534 | (apply #'vsh--with-standard-blocks ret)))) 535 | -------------------------------------------------------------------------------- /vsh-mode-1.0/vsh-mode-pkg.el: -------------------------------------------------------------------------------- 1 | (define-package "vsh-mode" "1.0" 2 | "Alternate PTY interface for complex terminal sessions." 3 | ;; TODO Honestly a bit of a guess which emacs version this requires. 4 | ;; I've not even tested this version, but AFAIK there is nothing special about 5 | ;; this package that requires any new version of emacs and this "feels like" a 6 | ;; version recent enough to probably have things I want and old enough to not 7 | ;; restrict too many people from downloading the package. 8 | '((emacs "24.3"))) 9 | -------------------------------------------------------------------------------- /vsh-mode-1.0/vsh-tests.erts: -------------------------------------------------------------------------------- 1 | Name: Next command 1 2 | Code: (lambda () (call-interactively 'vsh-next-command)) 3 | Point-Char: | 4 | 5 | Checking each thing that could be feasibly be treated as a motion marker -- but 6 | that should not be treated as such -- is ignored. 7 | =-= 8 | |vshcmd: > 9 | vshcmd: > # echo Hello there 10 | vshcmd: > # echo Hello there 11 | vshcmd: > #echo Hello there 12 | vshcmd: > echo Hello there 13 | =-= 14 | vshcmd: > 15 | vshcmd: > # echo Hello there 16 | vshcmd: > # echo Hello there 17 | vshcmd: > #echo Hello there 18 | vshcmd: > |echo Hello there 19 | =-=-= 20 | 21 | Name: Next command 2 22 | No-After-Newline: 23 | No-Before-Newline: 24 | 25 | Moving when there is no next prompt moves to end of buffer. 26 | 27 | N.b. I'm using the no-newline things in order to be able to represent point 28 | going to the end of the buffer. 29 | =-= 30 | vshcmd: > |echo Hello there 31 | 32 | =-= 33 | vshcmd: > echo Hello there 34 | | 35 | =-=-= 36 | 37 | Name: Next command 3 38 | No-After-Newline: 39 | No-Before-Newline: 40 | 41 | Moves to end of buffer, including if there is output in the way. 42 | =-= 43 | vshcmd: > |echo Hello there 44 | Hello there, this is output 45 | 46 | =-= 47 | vshcmd: > echo Hello there 48 | Hello there, this is output 49 | | 50 | =-=-= 51 | 52 | Name: Next command 4 53 | No-After-Newline: 54 | No-Before-Newline: 55 | 56 | Checking special case when the prompt is the last line in the buffer. 57 | 58 | The decision to put the cursor at the start of the *current* command if moving 59 | forward and this line is at the very end of the file was mostly made because 60 | that was how the first function I wrote behaves and it doesn't seem like a 61 | problem. 62 | 63 | It would be nice to make the function behave the most logically consistent 64 | (something like always moving forwards), but for now this seems nice enough. 65 | =-= 66 | vshcmd: > echo Hello there| 67 | =-= 68 | vshcmd: > echo Hello there| 69 | =-=-= 70 | 71 | Name: Next command 5 72 | No-After-Newline: 73 | No-Before-Newline: 74 | 75 | =-= 76 | |Hello there, this is output 77 | vshcmd: > echo Hello there 78 | =-= 79 | Hello there, this is output 80 | vshcmd: > |echo Hello there 81 | =-=-= 82 | 83 | Name: Next command 6 84 | No-After-Newline: 85 | No-Before-Newline: 86 | 87 | =-= 88 | Hello there, this is output| 89 | vshcmd: > echo Hello there 90 | =-= 91 | Hello there, this is output 92 | vshcmd: > |echo Hello there 93 | =-=-= 94 | 95 | Name: Next command 7 96 | No-After-Newline: 97 | No-Before-Newline: 98 | 99 | =-= 100 | |vshcmd: > echo Hello there 101 | =-= 102 | vshcmd: > |echo Hello there 103 | =-=-= 104 | 105 | 106 | Name: Next command 8 107 | No-After-Newline: 108 | No-Before-Newline: 109 | 110 | =-= 111 | vs|hcmd: > echo Hello there 112 | Hello there, this is output 113 | 114 | =-= 115 | vshcmd: > |echo Hello there 116 | Hello there, this is output 117 | 118 | =-=-= 119 | 120 | Name: Next command 9 121 | =-= 122 | vshcmd: > |echo Hello there 123 | vshcmd: > echo Hello there 124 | =-= 125 | vshcmd: > echo Hello there 126 | vshcmd: > |echo Hello there 127 | =-=-= 128 | 129 | Name: Next command 10 130 | =-= 131 | vshcmd: > | 132 | vshcmd: > echo Hello there 133 | =-= 134 | vshcmd: > 135 | vshcmd: > |echo Hello there 136 | =-=-= 137 | 138 | 139 | Name: Next command 11 140 | =-= 141 | |vshcmd: > # vshcmd: > ls 142 | vshcmd: > ls 143 | =-= 144 | vshcmd: > # vshcmd: > |ls 145 | vshcmd: > ls 146 | =-=-= 147 | 148 | Name: Next command 12 149 | =-= 150 | vshcmd: > |ls 151 | vshcmd: > # vshcmd: > ls 152 | =-= 153 | vshcmd: > ls 154 | vshcmd: > # vshcmd: > |ls 155 | =-=-= 156 | 157 | Name: Next command 13 158 | =-= 159 | |ls 160 | vshcmd: > # vshcmd: > ls 161 | =-= 162 | ls 163 | vshcmd: > # vshcmd: > |ls 164 | =-=-= 165 | 166 | 167 | Name: Next command 14 168 | =-= 169 | |vshcmd: > 170 | vshcmd: > ls 171 | =-= 172 | vshcmd: > | 173 | vshcmd: > ls 174 | =-=-= 175 | 176 | Name: Next command 15 177 | =-= 178 | vshcmd: > | 179 | vshcmd: > ls 180 | =-= 181 | vshcmd: > 182 | vshcmd: > |ls 183 | =-=-= 184 | 185 | Name: Next command 16 186 | =-= 187 | vshcmd: > l| 188 | vshcmd: > ls 189 | =-= 190 | vshcmd: > l 191 | vshcmd: > |ls 192 | =-=-= 193 | 194 | Name: Prev command 1 195 | Code: (lambda () (call-interactively 'vsh-prev-command)) 196 | 197 | Checking each thing that could be feasibly be treated as a motion marker -- but 198 | that should not be treated as such -- is ignored. 199 | =-= 200 | vshcmd: > echo Hello there 201 | vshcmd: > #echo Hello there 202 | vshcmd: > # echo Hello there 203 | vshcmd: > # echo Hello there 204 | |vshcmd: > 205 | =-= 206 | vshcmd: > |echo Hello there 207 | vshcmd: > #echo Hello there 208 | vshcmd: > # echo Hello there 209 | vshcmd: > # echo Hello there 210 | vshcmd: > 211 | =-=-= 212 | 213 | Name: Prev command 2 214 | Moving when there is no prev prompt moves to start of buffer. 215 | =-= 216 | 217 | vshcmd: > |echo Hello there 218 | =-= 219 | | 220 | vshcmd: > echo Hello there 221 | =-=-= 222 | 223 | Name: Prev command 3 224 | Moves to start of buffer, including if there is output in the way. 225 | =-= 226 | 227 | Hello there, this is output 228 | vshcmd: > |echo Hello there 229 | =-= 230 | | 231 | Hello there, this is output 232 | vshcmd: > echo Hello there 233 | =-=-= 234 | 235 | Name: Prev command 4 236 | =-= 237 | |vshcmd: > echo Hello there 238 | =-= 239 | |vshcmd: > echo Hello there 240 | =-=-= 241 | 242 | Name: Prev command 5 243 | =-= 244 | vshcmd: > echo Hello there 245 | |Hello there, this is output 246 | =-= 247 | vshcmd: > |echo Hello there 248 | Hello there, this is output 249 | =-=-= 250 | 251 | Name: Prev command 6 252 | =-= 253 | vshcmd: > echo Hello there 254 | Hello there, this is output| 255 | =-= 256 | vshcmd: > |echo Hello there 257 | Hello there, this is output 258 | =-=-= 259 | 260 | Name: Prev command 7 261 | =-= 262 | vshcmd: > |echo Hello there 263 | =-= 264 | |vshcmd: > echo Hello there 265 | =-=-= 266 | 267 | 268 | Name: Prev command 8 269 | =-= 270 | vshcmd: > echo Hello there 271 | vs|hcmd: > echo Hello there 272 | =-= 273 | vshcmd: > |echo Hello there 274 | vshcmd: > echo Hello there 275 | =-=-= 276 | 277 | Name: Prev command 9 278 | =-= 279 | vshcmd: > echo Hello there 280 | vshcmd: > |echo Hello there 281 | =-= 282 | vshcmd: > |echo Hello there 283 | vshcmd: > echo Hello there 284 | =-=-= 285 | 286 | Name: Prev command 10 287 | =-= 288 | vshcmd: > echo Hello there 289 | vshcmd: > | 290 | =-= 291 | vshcmd: > |echo Hello there 292 | vshcmd: > 293 | =-=-= 294 | 295 | 296 | Name: Prev command 11 297 | =-= 298 | |vshcmd: > # vshcmd: > ls 299 | vshcmd: > ls 300 | =-= 301 | |vshcmd: > # vshcmd: > ls 302 | vshcmd: > ls 303 | =-=-= 304 | 305 | Name: Prev command 12 306 | =-= 307 | vshcmd: > # vshcmd: > ls 308 | vshcmd: > |ls 309 | =-= 310 | vshcmd: > # vshcmd: > |ls 311 | vshcmd: > ls 312 | =-=-= 313 | 314 | Name: Prev command 13 315 | =-= 316 | vshcmd: > # vshcmd: > ls 317 | |ls 318 | =-= 319 | vshcmd: > # vshcmd: > |ls 320 | ls 321 | =-=-= 322 | 323 | Name: Prev command 14 324 | =-= 325 | vshcmd: > ls 326 | vshcmd: > l|s 327 | =-= 328 | vshcmd: > ls 329 | vshcmd: > |ls 330 | =-=-= 331 | 332 | Name: Prev command 15 333 | =-= 334 | vshcmd: > ls 335 | vshcmd: > |ls 336 | =-= 337 | vshcmd: > |ls 338 | vshcmd: > ls 339 | =-=-= 340 | 341 | Name: Prev command 16 342 | =-= 343 | vshcmd: > ls 344 | vshcmd: >| ls 345 | =-= 346 | vshcmd: > |ls 347 | vshcmd: > ls 348 | =-=-= 349 | 350 | Name: Indent function 1 351 | Code: (lambda () (vsh-indent-function)) 352 | =-= 353 | Output above 354 | |vshcmd: > ls 355 | =-= 356 | Output above 357 | |vshcmd: > ls 358 | =-=-= 359 | 360 | Name: Indent function 2 361 | =-= 362 | Output above 363 | |vshcmd: > ls 364 | =-= 365 | Output above 366 | |vshcmd: > ls 367 | =-=-= 368 | 369 | Name: Indent function 3 370 | =-= 371 | Output above 372 | vshcmd: > | ls 373 | =-= 374 | Output above 375 | vshcmd: > |ls 376 | =-=-= 377 | 378 | Name: Indent function 4 379 | =-= 380 | Output above 381 | vshcmd: > |ls 382 | =-= 383 | Output above 384 | vshcmd: > |ls 385 | =-=-= 386 | 387 | Name: Indent function 5 388 | =-= 389 | Output above 390 | vshcmd: > l|s 391 | =-= 392 | Output above 393 | vshcmd: > l|s 394 | =-=-= 395 | 396 | Name: Indent function 6 397 | =-= 398 | Output above 399 | vshcmd: >| 400 | =-= 401 | Output above 402 | vshcmd: > | 403 | =-=-= 404 | 405 | Name: Indent function 7 406 | =-= 407 | |Output above 408 | vshcmd: > 409 | =-= 410 | |Output above 411 | vshcmd: > 412 | =-=-= 413 | 414 | Name: Indent function 8 415 | =-= 416 | Out|put above 417 | vshcmd: > 418 | =-= 419 | Out|put above 420 | vshcmd: > 421 | =-=-= 422 | 423 | Name: Indent function 9 424 | =-= 425 | vshcmd: > Command above without indentation 426 | vshcmd: > |Command below with indentation 427 | =-= 428 | vshcmd: > Command above without indentation 429 | vshcmd: > |Command below with indentation 430 | =-=-= 431 | 432 | Name: Indent function 10 433 | =-= 434 | vshcmd: > Command above without indentation 435 | vshcmd: > |Command below without indentation 436 | =-= 437 | vshcmd: > Command above without indentation 438 | vshcmd: > |Command below without indentation 439 | =-=-= 440 | 441 | Name: Indent function 11 442 | =-= 443 | vshcmd: > Command above without indentation 444 | vshcmd: > |Command below without indentation 445 | =-= 446 | vshcmd: > Command above without indentation 447 | vshcmd: > |Command below without indentation 448 | =-=-= 449 | 450 | Name: Indent function 12 451 | =-= 452 | vshcmd: > Command above without indentation 453 | vshcmd: > Comm| below without indentation 454 | =-= 455 | vshcmd: > Command above without indentation 456 | vshcmd: > Comm | below without indentation 457 | =-=-= 458 | 459 | Name: Indent function 13 460 | Does not get affected by a a line above that does not "match" the type of the 461 | current line. 462 | =-= 463 | vshcmd: > # Command above without indentation 464 | vshcmd: > Comm| below without indentation 465 | =-= 466 | vshcmd: > # Command above without indentation 467 | vshcmd: > Comm| below without indentation 468 | =-=-= 469 | 470 | Name: Indent function 14 471 | Similar, but for comments being the "active" line. 472 | =-= 473 | vshcmd: > Comm below without indentation 474 | vshcmd: > # |Command above without indentation 475 | =-= 476 | vshcmd: > Comm below without indentation 477 | vshcmd: > # |Command above without indentation 478 | =-=-= 479 | 480 | Name: Indent function 15 481 | Does not get affected by a line below it. 482 | =-= 483 | vshcmd: > Comm below without indentation 484 | vshcmd: > # |Comment below 485 | vshcmd: > # Comment below 486 | =-= 487 | vshcmd: > Comm below without indentation 488 | vshcmd: > # |Comment below 489 | vshcmd: > # Comment below 490 | =-=-= 491 | 492 | Name: Indent function 16 493 | Does not get affected by a line below it. 494 | =-= 495 | vshcmd: > |Command below 496 | vshcmd: > Comment below. 497 | =-= 498 | vshcmd: > |Command below 499 | vshcmd: > Comment below. 500 | =-=-= 501 | 502 | Name: Indent function 17 503 | =-= 504 | vshcmd: > # Command above without indentation 505 | vshcmd: > # |Command below with indentation 506 | =-= 507 | vshcmd: > # Command above without indentation 508 | vshcmd: > # |Command below with indentation 509 | =-=-= 510 | 511 | Name: Indent function 18 512 | =-= 513 | vshcmd: > # Command above without indentation 514 | vshcmd: > # |Command below without indentation 515 | =-= 516 | vshcmd: > # Command above without indentation 517 | vshcmd: > # |Command below without indentation 518 | =-=-= 519 | 520 | Name: Indent function 19 521 | =-= 522 | vshcmd: > # Command above without indentation 523 | vshcmd: > # |Command below without indentation 524 | =-= 525 | vshcmd: > # Command above without indentation 526 | vshcmd: > # |Command below without indentation 527 | =-=-= 528 | 529 | Name: Indent function 20 530 | =-= 531 | vshcmd: > # Command above without indentation 532 | vshcmd: > # Comm| below without indentation 533 | =-= 534 | vshcmd: > # Command above without indentation 535 | vshcmd: > # Comm | below without indentation 536 | =-=-= 537 | 538 | Name: Indent function 21 539 | Whitespace before the hash does not change behaviour. 540 | =-= 541 | vshcmd: > # Command above without indentation 542 | vshcmd: > # Comm| below without indentation 543 | =-= 544 | vshcmd: > # Command above without indentation 545 | vshcmd: > # Comm | below without indentation 546 | =-=-= 547 | 548 | Name: Indent function 22 549 | Requesting indentation when in prompt does nothing. 550 | =-= 551 | vshcmd: > # Command above without indentation 552 | vs|hcmd: > # Comm below without indentation 553 | =-= 554 | vshcmd: > # Command above without indentation 555 | vs|hcmd: > # Comm below without indentation 556 | =-=-= 557 | 558 | Name: Indent function 23 559 | Requesting indentation when in prompt does nothing. 560 | =-= 561 | vshcmd: > Command above without indentation 562 | vs|hcmd: > Comm below without indentation 563 | =-= 564 | vshcmd: > Command above without indentation 565 | vs|hcmd: > Comm below without indentation 566 | =-=-= 567 | 568 | Name: Indent function 24 569 | Requesting indentation when in prompt does nothing. 570 | =-= 571 | vshcmd: > Command above without indentation 572 | |vshcmd: > Comm below without indentation 573 | =-= 574 | vshcmd: > Command above without indentation 575 | |vshcmd: > Comm below without indentation 576 | =-=-= 577 | 578 | Name: Indent function 25 579 | Requesting indentation when in prompt does nothing. 580 | =-= 581 | vshcmd: > # Command above without indentation 582 | |vshcmd: > # Comm below without indentation 583 | =-= 584 | vshcmd: > # Command above without indentation 585 | |vshcmd: > # Comm below without indentation 586 | =-=-= 587 | 588 | Name: Indent function 26 589 | =-= 590 | vshcmd: > # Command above without indentation 591 | vshcmd: > # | 592 | =-= 593 | vshcmd: > # Command above without indentation 594 | vshcmd: > # | 595 | =-=-= 596 | 597 | Name: Mark segment 1 598 | Code: (lambda () (call-interactively 'vsh-mark-segment) 599 | (goto-char (mark)) 600 | (deactivate-mark)) 601 | =-= 602 | vshcmd: > Command 603 | text 604 | output 605 | |from 606 | above 607 | command 608 | =-= 609 | vshcmd: > Command 610 | |text 611 | output 612 | from 613 | above 614 | command 615 | =-=-= 616 | 617 | Name: Mark segment 2 618 | =-= 619 | vshcm|d: > Command 620 | text 621 | output 622 | from 623 | above 624 | command 625 | =-= 626 | vshcmd: > Command 627 | |text 628 | output 629 | from 630 | above 631 | command 632 | =-=-= 633 | 634 | Name: Mark segment 3 635 | =-= 636 | vshcmd: > Command| 637 | text 638 | output 639 | from 640 | above 641 | command 642 | =-= 643 | vshcmd: > Command 644 | |text 645 | output 646 | from 647 | above 648 | command 649 | =-=-= 650 | 651 | Name: Mark segment 4 652 | =-= 653 | |vshcmd: > Command 654 | text 655 | output 656 | from 657 | above 658 | command 659 | =-= 660 | vshcmd: > Command 661 | |text 662 | output 663 | from 664 | above 665 | command 666 | =-=-= 667 | 668 | Name: Mark command block 1 669 | Code: (lambda () (call-interactively 'vsh-mark-command-block) 670 | (goto-char (mark)) 671 | (deactivate-mark)) 672 | =-= 673 | vshcmd: > ls 674 | vshcmd: > ls| 675 | =-= 676 | |vshcmd: > ls 677 | vshcmd: > ls 678 | =-=-= 679 | 680 | Name: Mark command block 2 681 | =-= 682 | Test output line 683 | vshcmd: > ls 684 | vshcmd: > ls| 685 | =-= 686 | Test output line 687 | |vshcmd: > ls 688 | vshcmd: > ls 689 | =-=-= 690 | 691 | Name: Mark command block 3 692 | This to remind myself of this bug (more obvious than just having a TODO in vsh.el). 693 | =-= 694 | vshcmd: > ls 695 | test output line 696 | vshcmd: > # Commen|t 697 | vshcmd: > ls 698 | test output line 699 | =-= 700 | vshcmd: > ls 701 | test output line 702 | |vshcmd: > # Comment 703 | vshcmd: > ls 704 | test output line 705 | =-=-= 706 | 707 | Name: Automatic command insertion 1 708 | Code: (lambda () (let ((vsh-may-start-server nil) 709 | (vsh-mode-hook '(vsh--initialise-settings vsh--setup-colors))) 710 | (vsh-mode) 711 | (call-interactively 'comment-indent-new-line))) 712 | Similar to above, this to remind myself of this bug. 713 | =-= 714 | vshcmd: > |ls 715 | =-= 716 | vshcmd: > 717 | vshcmd: > |ls 718 | =-=-= 719 | -------------------------------------------------------------------------------- /vsh-mode-1.0/vsh_man_pager: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Simple MANPAGER program that just removes special formatting and prints the 4 | # text to the screen. 5 | # nroff encodes bold and underlines using backspaces. 6 | # Use 'col -b' to remove backspaces and characters just before them. 7 | # Use cat to buffer the text, so the nvim instance doesn't have too many 8 | # interrupts (this is the main cause of slowness). 9 | col -b | cat 10 | -------------------------------------------------------------------------------- /vsh-mode-1.0/vsh_shell_start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # It is appealing to use an interactive shell to run this script so that the 4 | # users bashrc can choose a different INPUTRC. We suggest the user changing 5 | # their .inputrc config file to have a condition for `TERM=dumb` in it and 6 | # choosing `set editing-mode emacs` under that clause. However we also want to 7 | # support users changing the INPUTRC environment variable in their bashrc (on 8 | # the principle that most people are more comfortable changing settings in 9 | # bashrc than anywhere else). 10 | # 11 | # However, on experiment it seems like that comes with downsides in error cases 12 | # which we do not think are worth the trade-off. 13 | # See github issue #32. 14 | 15 | # TODO Have to play around with these settings to find the most useful for vsh. 16 | # stty -echonl -icanon -iexten isig onocr 17 | stty tabs -onlcr icanon -echo -onlcr iexten isig -echonl -crterase 18 | 19 | export PAGER= 20 | # Recently (back in 2002) `groff` changed to emit SGR color sequences instead 21 | # of the overstrike sequences originally for paper typewriters. Until more 22 | # recently (not entirely sure when) this was overridden by default in Debian 23 | # based systems via a setting in /etc/groff/man.local. 24 | # On newer systems (I first noticed on Ubuntu 24.04) this default has been 25 | # removed and the SGR encodings are now "coming through" in the output going to 26 | # MANPAGER. 27 | # 28 | # We now have an interesting question. Do we pass these SGR sequences through 29 | # to the user (so they see bold/underlined/etc output in the emacs VSH buffer 30 | # -- and could see the same thing in vim VSH buffers when I get round to 31 | # implementing that -- and so most other programs used as MANPAGER would also 32 | # work), or do we try and remove them (so that searching the text matches 33 | # things). 34 | # 35 | # We choose trying to remove them on the principle that if users of vim/emacs 36 | # wanted pretty MAN pages they can use the respective viewers within that text 37 | # editor. Providing a quick-and-dirty command line version inside VSH is much 38 | # more useful for search/replace/use as part of a command, and those uses need 39 | # escape sequences removed. 40 | # The below environment variable ensures that the legacy output format is used, 41 | # and the MANPAGER ensures that the overstrike formatting gets converted to 42 | # plain text. 43 | export MANROFFOPT="-c" 44 | export MANPAGER="$1/vsh_man_pager" 45 | export GIT_PAGER='cat' 46 | 47 | # N.b. These variables are in the `vim` environment, but not the `nvim` 48 | # environment. With these set some commands change their output to fit the 49 | # size of the terminal that is reported, I don't think it's a good idea to take 50 | # this info since the vsh file will naturally have different column widths. 51 | unset COLUMNS 52 | unset LINES 53 | 54 | 55 | if [[ -n "$VSH_EMACS_BUFFER" ]]; then 56 | export EDITOR="emacsclient" 57 | vsh_buffer="$VSH_EMACS_BUFFER" 58 | else 59 | export EDITOR="$1/vsh_editor_prog" 60 | vsh_buffer="$VSH_VIM_BUFFER" 61 | fi 62 | 63 | # It's really awkward to find out what the bindings are programmatically 64 | # (or at least I don't know of a nice way to query the readline library). 65 | completions_binding="$(bind -q possible-completions 2>/dev/null)" 66 | glob_binding="$(bind -q glob-list-expansions 2>/dev/null)" 67 | discard_line="$(bind -q unix-line-discard 2>/dev/null)" 68 | 69 | # Set INPUTRC environment variable to a special temporary one. 70 | # Then we can add special configuration according to what is best for this 71 | # plugin. 72 | # Caveats: 73 | # - Does not cross `su` boundaries, nor does it work on the remote 74 | # machine. Hence still want to parse completion commands from the 75 | # environment (on the assumption that those would be whatever the user is 76 | # planning on using and possibly has set up elsewhere). Hence would very 77 | # much like to only *adjust* INPUTRC rather than override it. 78 | # Do this by using `$import` and the original INPUTRC. 79 | # - Caveat of above logic is that it's reasonably likely that the user would 80 | # simply "deal with" whatever the other shell uses outside of `vsh`. So 81 | # things could easily still be completely different. 82 | # - Another problem is that user may have set bindings in `bash` rather than 83 | # readline. 84 | # Assume that if this is the case we've picked it up with `bind -q` above, 85 | # and hence avoid overriding things with our special INPUTRC. 86 | # N.b. Probably best to parse the keys user has set up in their environment, 87 | # then put them into the generated INPUTRC. That way can ensure things like 88 | # GDB have the same commands. 89 | # 90 | # 1) AFAIK there is no easy way to register that we want to remove the tmpfile 91 | # when the shell we start exits. Would have to do something about putting an 92 | # `atexit` in some temporary bashrc. This seems like complexity that I want 93 | # to avoid. 94 | # 2) Hence rather than that be the responsibility of the shell, we make it the 95 | # responsibility of the editor. 96 | # 3) Could create the tempfile here and report back to the editor that this is 97 | # what we're doing. Unfortunately that creates an association between 98 | # current *shell* and a given tmpfile that we then map to a given buffer. 99 | # Restarting the shell in a given buffer generates a new tempfile and 100 | # must *add* to the list of tempfiles to close when this buffer is closed or 101 | # must remove old tmpfile and record new one. 102 | # Places where this could be tricky to maintain: 103 | # - Restarting subprocess. 104 | # Not too bad -- just need to remove old tempfile before restarting. 105 | # - Manually calling `vsh_shell_start` from underlying shell. 106 | # Pretty tricky -- now have two different tempfiles active at same time. 107 | # (Really not supposed to do this, but nice to have a design that avoids 108 | # problems with strange things nonetheless). 109 | # 4) Alternatively we can create the tempfile in the editor and tell 110 | # vsh_shell_start what said tempfile is. This way we could associate one 111 | # tempfile per buffer in the editor (making the editor code easier for 112 | # teardown) at the cost of having to give vsh_shell_start another argument. 113 | # - This is what I try and do. 114 | setup-inputrc () { 115 | temp_file="$1" 116 | if [[ -n "$INPUTRC" ]]; then 117 | echo "\$include $INPUTRC" > $temp_file 118 | elif [[ -f "$HOME/.inputrc" ]]; then 119 | echo "\$include $HOME/.inputrc" > $temp_file 120 | else 121 | echo "\$include /etc/inputrc" > $temp_file 122 | fi 123 | if [[ -n "$2" ]]; then 124 | # N.b., I use the escape version rather than `Meta-=` because this seems to 125 | # work more often. Have not looked out for why exactly that is. 126 | echo '"\e=": possible-completions' >> $temp_file 127 | echo 'Control-x g: glob-list-expansions' >> $temp_file 128 | echo 'Control-u: unix-line-discard' >> $temp_file 129 | fi 130 | # Generate a partial inputrc that we used to generate an inputrc for the user 131 | # when starting a vsh shell. 132 | echo '$if term=dumb' >> $temp_file 133 | echo ' set show-all-if-ambiguous on' >> $temp_file 134 | # Disable querying when I request `possible-completions` especially inside 135 | # VSH (putting a bunch of text on the screen is not a problem when it's so 136 | # easily removed, and this would always let us use vim-completions to find 137 | # what we wanted. Mentioning VSH since that's when I tend to have TERM=dumb. 138 | echo ' set completion-query-items -1' >> $temp_file 139 | # Do not paginate completions when there are a lot of options. Similar to 140 | # above, this is the best option when in VSH since we are not directly 141 | # interacting with readline but rather bringing in the list of completions to 142 | # the current vim buffer. 143 | echo ' set page-completions off' >> $temp_file 144 | # Don't want special handling of tab character. This will *look* like 145 | # a tab character getting inserted when in `vsh`, so it should *act* like 146 | # that in the underlying terminal. 147 | echo ' "\t":tab-insert' >> $temp_file 148 | echo '$endif' >> $temp_file 149 | } 150 | 151 | if [[ "$completions_binding" == *'not bound to any keys'* ]] || \ 152 | [[ "$glob_binding" == *'not bound to any keys'* ]] || \ 153 | [[ "$discard_line" == *'not bound to any keys'* ]]; then 154 | # Just let any errors raise -- we'll see their stack in the buffer. 155 | # Use defaults that should work when there was no existing binding. 156 | setup-inputrc "$3" include-bindings 157 | export INPUTRC="$3" 158 | "$1/vsh_tell_editor_bindings.py" \ 159 | 'possible-completions can be invoked via "\e=".' \ 160 | 'glob-list-expansions can be invoked via "\C-xg".' \ 161 | 'unix-line-discard can be invoked via "\C-u".' \ 162 | "$vsh_buffer" 163 | 164 | else 165 | # Just let any errors raise -- we'll see their stack in the buffer. 166 | setup-inputrc "$3" 167 | export INPUTRC="$3" 168 | "$1/vsh_tell_editor_bindings.py" \ 169 | "$completions_binding" \ 170 | "$glob_binding" \ 171 | "$discard_line" \ 172 | "$vsh_buffer" 173 | fi 174 | exec "$2" 175 | -------------------------------------------------------------------------------- /vsh-mode-1.0/vsh_tell_editor_bindings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' 3 | Find the users bindings for the `possible-completions`, `glob-list-expansions`, 4 | and `unix-line-discard` readline commands. Then store them in the neovim buffer 5 | that is running this shell session. 6 | 7 | ''' 8 | 9 | # TODO 10 | # There must be a way to find the binding directly from the readline 11 | # interface. 12 | # Whether it's possible from *python* or not is another matter. 13 | # I'm not going to make users compile anything, so if I can't find a 14 | # scripting language that can do it I won't bother. 15 | # 16 | # Looking in the readline/readline.h header file, I want to know the bindings 17 | # for rl_possible_completions() and rl_unix_line_discard() for the first and 18 | # last. 19 | # 20 | # I suspect that `glob-list-expansions` is actually a custom bash readline 21 | # action, and so won't be directly available. 22 | # (suspicion comes from the fact that it isn't listed in Man readline(3), 23 | # while `possible-completions`, and `unix-line-discard` are). 24 | 25 | import os 26 | import sys 27 | 28 | def find_next_binding(position, bind_str): 29 | ''' 30 | Called when parsing failed while in the middle of a binding. 31 | 32 | We iterate through bind_str until we have reached two unescaped " 33 | characters or the end of the binding string. 34 | 35 | If we found another binding we can parse, we return its position, otherwise 36 | we return None. 37 | 38 | ''' 39 | quote_count = 0 40 | escape_next = False 41 | for newpos, char in enumerate(bind_str[position:]): 42 | if char == '"' and not escape_next: 43 | if quote_count: return newpos + position + 1 44 | else: quote_count += 1 45 | elif char == '\\': escape_next = not escape_next 46 | else: escape_next = False 47 | 48 | return None 49 | 50 | 51 | def read_ctrl_char(position, bind_str): 52 | # Shortest possible remainder bind_str is: 53 | # C-x" 54 | # This basically means we can index freely without having to worry about 55 | # IndexErrors 56 | assert(position + 3 <= len(bind_str)) 57 | 58 | end_pos = position + 2 59 | ctrl_char = bind_str[end_pos] 60 | if ctrl_char == '\\': 61 | end_pos = position + 3 62 | ctrl_char = bind_str[end_pos] 63 | 64 | # `bind -q` control character output is always lowercase 65 | # see 66 | # bind "\C-L":vi-rubout 67 | # bind -q vi-rubout 68 | ctrl_offset = ord('A') - ord('') 69 | # Use uppercase so things work for \C-\\ and \C-^ etc 70 | ctrl_ord = ord(ctrl_char.upper()) - ctrl_offset 71 | if ctrl_ord > 31 or ctrl_ord < 0: 72 | # Couldn't parse this character -- move on to the next binding and 73 | # return None as the character so the function above us knows what 74 | # happened. 75 | return None, find_next_binding(position, bind_str) 76 | return chr(ctrl_ord), end_pos 77 | 78 | def find_command_from_output(bind_output): 79 | known_string = 'can be invoked via' 80 | string_pos = bind_output.find(known_string) 81 | # Command is not bound to a key -- tell the user? 82 | if string_pos == -1: 83 | return None 84 | 85 | bindings = bind_output[string_pos + len(known_string):].strip() 86 | 87 | # This should hold from the output format of `bind -q` 88 | assert(bindings[0] == '"') 89 | 90 | # We only want the first token -- it doesn't matter if there are many 91 | # key sequences that run the same command, we just want one. 92 | # However, we have to properly parse the input (as we need to watch out for 93 | # if there is a \" character in the binding we want). 94 | binding = '' 95 | escape_next = False 96 | # We trust that the output of `bind -q` never returns anything needlessly 97 | # escaped (i.e. "\j" is never given when "j" could do). 98 | # It certainly looks this way from the outputs I've seen. 99 | # There are hence a small number of options after a backslash 100 | # \ -> treat as a backslash 101 | # e -> treat as an escape character 102 | # C -> treat as start of control character 103 | # " -> treat as normal quote character. 104 | # M -> Treat as start of meta character (I *think* can just send escape) 105 | position = 1 106 | bindings_len = len(bindings) 107 | while position < bindings_len: 108 | char = bindings[position] 109 | if char == '"' and not escape_next: return binding 110 | if char == '\\': 111 | if escape_next: 112 | binding += '\\' 113 | escape_next = False 114 | else: escape_next = True 115 | elif escape_next: 116 | if char == 'e': binding += '\x1b' 117 | elif char == 'M': 118 | position += 1 # To account for the `-` that should follow. 119 | binding += '\x1b' 120 | elif char == 'C': 121 | ctrl_char, position = read_ctrl_char(position, bindings) 122 | binding += ctrl_char 123 | elif char == '"': binding += '"' 124 | # Should never happen -- the only escaped characters should be 125 | # '\\', 'e', 'M', '"', and 'C'. 126 | else: raise ValueError 127 | escape_next = False 128 | else: 129 | binding += char 130 | position += 1 131 | 132 | 133 | 134 | if __name__ == "__main__": 135 | if len(sys.argv) != 5: 136 | sys.exit(1) 137 | 138 | # Just let exceptions raise -- we'll get the information and I can account 139 | # for that case. 140 | possible_completions = find_command_from_output(sys.argv[1]) 141 | list_glob_completions = find_command_from_output(sys.argv[2]) 142 | discard_line = find_command_from_output(sys.argv[3]) 143 | 144 | completions_list = [possible_completions, list_glob_completions, discard_line] 145 | if os.getenv('NVIM') or os.getenv('NVIM_LISTEN_ADDRESS'): 146 | import pynvim 147 | nvim_socket_path = os.getenv('NVIM') 148 | if not nvim_socket_path: 149 | nvim_socket_path = os.getenv('NVIM_LISTEN_ADDRESS') 150 | nvim = pynvim.attach('socket', path=nvim_socket_path) 151 | curbuf = nvim.buffers[int(sys.argv[4])] 152 | curbuf.vars['vsh_completions_cmd'] = completions_list 153 | elif os.getenv('VSH_VIM_LISTEN_ADDRESS'): 154 | import socket 155 | import re 156 | import json 157 | origvim_socket_addr = os.getenv('VSH_VIM_LISTEN_ADDRESS') 158 | m = re.match(r'localhost:(\d+)', origvim_socket_addr) 159 | assert(m) 160 | sock = socket.socket() 161 | sock.connect(('localhost', int(m.groups()[0]))) 162 | message = [ 163 | 'call', 'setbufvar', 164 | [int(sys.argv[4]), 'vsh_completions_cmd', completions_list] 165 | ] 166 | message = json.dumps(message).encode('utf8') 167 | sock.send(message) 168 | sock.shutdown(socket.SHUT_RDWR) 169 | sock.close() 170 | elif os.getenv('VSH_EMACS_BUFFER'): 171 | import subprocess as sp 172 | import base64 173 | lisp_arguments = [ 174 | '"' + base64.b64encode(bytes(x, 'utf8')).decode('utf8') + '"' 175 | for x in completions_list] 176 | sp.check_call(['emacsclient', '--suppress-output', '--eval', 177 | '(vsh--receive-readline-bindings {} {})'.format( 178 | sys.argv[4], ' '.join(lisp_arguments))]) 179 | else: 180 | print('No editor to send info to!', file=sys.stderr) 181 | --------------------------------------------------------------------------------