/neo-pipe

Pipe text through external commands and display the output in a scratch buffer

Primary LanguageVim scriptMIT LicenseMIT

          #     # ####### ####### ######  ### ######  ####### 
          ##    # #       #     # #     #  #  #     # #       
          # #   # #       #     # #     #  #  #     # #       
          #  #  # #####   #     # ######   #  ######  #####   
          #   # # #       #     # #        #  #       #       
          #    ## #       #     # #        #  #       #       
          #     # ####### ####### #       ### #       ####### 

Table of Contents

Introduction

NeoPipe allows users to send text through an external command and display the output in the output buffer. The ':NeoPipe' command takes a range of text (the enter file by default) and pipes it through a user-defined command. NeoPipe defines mappings which can take an operator, work on a visual selection, or work on a line. This allows users to work in an ordinary vim buffer and use it as a repl, make changes to a database, etc.

Dependencies

NeoPipe has no dependencies, but can work with tpope's projectionist plugin. If you are not familiar with projectionist, I strongly encourage you to check it out. It excels at project-level configuration.

NeoPipe also provides an operator-pending mapping (see mappings sections) which works well with textobj-user by kana and the related plugins. If you are not yet familiar with these plugins, they are worth your time to check them out.

Setup

Neopipe allows users to interact with their commands in one of four ways. First, users can define a long running command through which all subsequent text will be piped. This can be as simple as opening a shell or it could open a database, a repl, or any other command that the user wants to keep running for the duration of the session. NeoPipe actually provides two ways to work with long-running commands (see the section npipe_type for details). One uses jobstart() behinds the scenes and one uses termopen().

The second method available to NeoPipe users is to define a command that takes each batch of text through it's stdin and which rights it's output to stdout. In this case, NeoPipe uses the system() command on each invocation.

The third options is to echo the selected lines in the output buffer. This feature could come in handy if a user was testing a user-defined motion or text object and wanted to make sure that the plugin selected the correct text.

Whichever method the user chooses, the NeoPipe sends the output of the command to the output buffer. Users posses the ability to define the filetype, split, height/width, etc. of the output buffer.

Users have the ability to set options at the buffer, projection (see projectionist by Tim Pope and the projections section of this document), or global level. NeoPipe will search for the options in that order and apply the first one that it finds. The examples that follow demonstrate setting each option at the buffer and global level. For examples of setting options in projections, see the projections section of this document. The following are the available options that collectively determine the behavior of NeoPipe. Technically, user do not need to set any of these options, but if none are set, the text is simply echoed to the output buffer that will be opened in a vertical split (evenly split) buffer with no filetype.

Important Note

Once NeoPipe finds a variable at any level, that variable is set at the window level. This is important because it allows us to work with different buffers and pipe the commands to a single output buffer. Let's say for example, that I am working on a sql project and call ':NeoPipe'. Let's further say that this command opens a sql database in the background for use with all subsequent commands. I send a few commands to ':NeoPipe', then I switch to a new buffer in the same window next to the output buffer and execute another ':NeoPipe'. It would be jarring if another output buffer opened and a separate instance of sql (sqlite, mysql, etc.) was started. What I would most likely want is to send commands to the existing sql instance and see the output in the existing output buffer. If, at any time, I desire to start fresh, I simply call ':NeoPipeClose' and when I next call ':NeoPipe', I will be starting fresh. If I need to keep two (or more) separate 'command buffer / output buffer' combinations going at once, I can open them in separate tabs.

npipe_com

The npipe_com command is the actual command through which the text will be sent, either continuously running or through a series of one-offs.

let g:npipe_com = 'zsh'
au filetype sql let b:npipe_com = 'sqlite3 ~/db/myDatabase.db'
au fileytpe mongo let b:npipe_com = 'mongo'

" with b:npipe_type='s'
au fileytpe livescript let b:npipe_com = 'lsc -cb'

npipe_type

NeoPipe's most fundamental option. This options tells NeoPipe how to interact with npipe_com. The 'c' option, which stands for "continuous" is one of two commands in the continuous family. Behind the scenes this option tells NeoPipe to use nvim's jobstart() function to open a long running command and to then pipe all subsequent text through this command and display the results to the output buffer. The second option, which is also a continuously running command is the 't' option, which stands for "terminal". The options instructs NeoPipe to use the termopen() function. This is similar too the 'c' option but the output buffer will simply be a terminal. NeoPipe user can still set the npipe_split option, but the npipe_ft, npipe_append, and npipe_sep options are ignored. Some repl's work better with this setting (pry for ruby developers is one program known to work better this way).

The 's' setting, which stands for "single", leads NeoPipe to send each batch of text through the command separately. In other words, each call to NeoPipe is a "single" invocation of the command.

If npipe_type is not set, NeoPipe will simply echo the text in the scratch buffer. In this situation, any value set on npipe_com will be ignored. It is certainly fine to leave npipe_type unset if echoing is desired, but in most cases NeoPipe's true power comes from the combination of npipe_com and npipe_type.

" run the command continuously
let g:npipe_type='c'

" run command in a terminal buffer
au filetype ruby let b:npipe_type='t'

" run the command each time
au fileytpe coffe let b:npipe_type='s'

" echo the text in the scratch buffer
au filetype txt let b:npipe_type=0

npipe_append

This variable informs NeoPipe of whether to clear the buffer for each invocation, or to append each subsequent write of the output buffer. The text can be appended to the top or bottom of the buffer by setting this to 'top' or 'bottom' respectively. If npipe_append in not set, or set to anything other than 'top' or 'bottom', then the buffer is cleared and rewritten on each invocation. If npipe_type is set to 't' then this option is ignored.

let g:npipe_type='bottom'
au filetype mongo let b:npipe_append='top'
au filetype javascript let b:npipe_append=0

Important Note

If a user always desired to clear the buffer on each invocation, then it is simple enough not to set this variable at all. But remember, all variables are searched at the buffer, then projection, then global levels. If somewhere up the "food chain" this variable was set then it would affect all lower level buffers until overwritten by a "closer" variable. Therefore, it is often a good idea to be explicit. Because the actual value does not matter except for being other than 'top' or 'bottom', users can set it to number 0, to an empty string, or to anything that helps them remember the intent (i.e. 'clear', 'new', 'foobar', etc.)

npipe_ft

The npipe_ft command sets the filetype of the output buffer. For example, if we are pumping text through a mongodb database, we would likely want the output to have json syntax highlighting. If npipe_type is set to 't' then this option is ignored.

{
  "name": "john"
}

npipe_split

The npipe_split option is vnew by default. Meaning, the output buffer will be shown in a vertically and evenly split window. As with all options, this can be set on the buffer, projection or global levels. The option can be either 'new', vnew (default), or tabnew. It's hard to imagine a use case for tabnew, but it is available. When npipe_type is set to 't', the buffer is split before the call to termopen(), and therefore, this value is still relevant.

let g:npipe_split = 'new'
au filetype vim let b:npipe_split = 'vnew'

It is also possible to specify a height or width by supplying a number to the split or vsplit (respectively) command. By default Vim splits each window equally. Below are examples of more explicit splitting behavior.

let g:npipe_split = '40vnew'
au filetype coffee let b:npipe_split = '25new'

User can set other options and commands on the window. Check the Vim documentation for further details:

:h vsplit
:h split
:h ++opt
:h +cmd

npipe_sep

The npipe_sep option defines an array of lines used to separate each group of output lines in the scratch buffer. Only relevant if npipe_append is set to top or bottom. If npipe_type is set to 't' then this option is ignored.

let g:npipe_sep=['', '---'] " default value
au filetype mongo let b:npipe_sep=[] " no sep

Projections

As mentioned previously, this plugin has been specifically designed to integrate smoothly with the projectionist plugin. If you are not familiar with projectionist, I strongly encourage you to give it a look. It allows for a simple and clean method of assigning values on a per-project (or global) basis. Using projectionist can save you from a lot of unnecessary autocommands designed to handle per-project requirements. The following examples could be included in a '.projections.json' file in the root directory of a project.

// compile livescript
"*.ls": {
  "npipe_com": "lsc -cbp",
  "npipe_type": "s",
  "npipe_ft": "javascript",
  "npipe_split": "30new",
  "npipe_append": "bottom"
}

// work with ruby through pry
"*.rb": {
  "npipe_com": "pry --no-pager",
  "npipe_type": "t",
  "npipe_split": "rightbelow split"
}

//connect to a mongodb database
"*.mongo": {
  "npipe_com": "mongo",
  "npipe_type": "c",
  "npipe_ft": "javascript",
  "npipe_append": "top"
}

// connect to a sqlite3 db
"*.sql": {
  "npipe_com": "sqlite3 ~/mydb.db",
  "npipe_type": "c",
  "npipe_split": "new",
  "npipe_append": 0
}

// compile pug to html
"templates/*.pug": {
  "npipe_com": "pug",
  "npipe_type": "s",
  "npipe_ft": "html",
  "npipe_append": 0
}

// compile pug to javascript
"src/*.pug": {
  "npipe_com": "pug -c",
  "npipe_type": "s",
  "npipe_ft": "javascript",
  "npipe_append": 0
}

// connect to a mongodb instance with livescript
"*.mongo.ls": {
  "npipe_com": "lsc -cpb | mongo",
  "npipe_type": "s",
  "npipe_ft": "javascript",
  "npipe_split": "50vnew",
  "npipe_append": "bottom"
}

// view assembly for c files
"*.c": {
  "npipe_com": "gcc -S -xc -c -o - -",
  "npipe_type": "s",
  "npipe_ft": "asm",
  "npipe_append": "top"
}

Commands

The most basic command that NeoPipe provides is the ':NeoPipe' command. This is the command that sends text from the current buffer to the output buffer. If the output buffer is not yet created, it will automatically create it, and if the command type (i.e. npipe_type) is 'c' or 't' (i.e. "continuous" or "terminal"), the ':NeoPipe' command will take care of all of that for us. By default, :NeoPipe sends the current line of text through the npipe_com command, but :NeoPipe also accepts a range.

" whole file
:NeoPipe

" current line
:.NeoPipe

" line 52
:52NeoPipe

" line range (13-26)
:13,26NeoPipe

" current line to bottom of the file
:.,$NeoPipe

NeoPipe provides two additional commands - :NeoPipeClear and :NeoPipeClose. :NeoPipeClear, as the name implies, clears the output buffer. This is only useful if npipe_append is one of 'top' or 'bottom', otherwise it is done on every invocation of :NeoPipe anyway. ':NeoPipeClose' closes the output buffer and if npipe_type is 'c', it will cancel the command. Any invocation of :NeoPipe following :NeoPipeClose will be run from scratch.

" clear the output buffer
:NeoPipeClear

" close the output buffer
:NeoPipeClose

Mappings

NeoPipe privies a 'Plug' mapping which allows users to attach the NeoPipe command to an operator-pending action. This allows us to very quickly send arbitrary regions of text to the command.

<Plug>(npipe-operator)

Important Note

Neopipe is inherently a line-based command. Therefore when using an operator pending action it is important to realize that while character-wise motions are acceptable, any line that is touched by the motion or text object will be included -- in full -- in the command. This is also true for visual mode. You can perform a character-wise visual mode selection, but any line touch by the selection is included in full.

Default Mappings

By default, NeoPipe provides the following convenience mappings:

" operator-pending
nmap ,t <Plug>(npipe-operator)

" current line
nmap ,tt <Plug>(npipe-operator)_

" whole file
nmap ,tg :NeoPipe<cr>

" clear output buffer
nmap ,tc :NeoPipeClear

" close output buffer
nmap ,tq :NeoPipeClose

" visual selection
" would actually run as :'<,'>NeoPipe
vmap ,t :NeoPipe<cr>

If this is not desired simply include the following in your init.vim

let g:neopipe_do_no_mappings=1

Mapping Repeatability

The operator-pending mapping and the 'current line' mapping (as defined above) are naturally repeatable, no external plugins are required because they work with Vim's natural motions. The other commands are not. A great deal of thought went into this, but the rationale is as follows: it is perfectly understandable that a user might be working with a pre-existing file and chose to move around and send various lines/ranges to the :NeoPipe command. However, it is doubtful that a user would want to send an entire file to the command twice without some modifications in between (what would be the point). It is equally doubtful that anyone would need to clear or close a buffer twice without intervening events. Therefore, the two mappings that have the most (and possibly only) utility for a repeat action are the two that are repeatable.

Worth noting, the operator-pending mode will, of course, work with any text object, including user-defined text-objects. The textobj-user and related plugins provide many useful text-objects combine nicely with NeoPipe's operator mapping. If, for example, a user is looking for repeatability for the entire file (still not sure what the use would be), I suggest the textobj-entire plugin also by kana. There are many other useful user-defined text-objects available within that ecosystem. If you are not familiar with it, I encourage you to check out the textobj-user plugin and the related plugins page. Even if not for repeatability, the plugins can be a great benefit.