Skip to content

Releases: ahw/vim-hooks

2.2.0 Auto-refresh, prettier paths, feedkeys support, debug mode

29 Jun 20:25
@ahw ahw

Choose a tag to compare

Auto-refresh Hook Files After Editing

Hook files are just executed as scripts on the command line which means their contents are not stored in memory — with one exception: hook options, which are embedded as lines of text within a given hook script, are. They are parsed out during VimHook initialization and stored within an instance of the VimHook data structure. Thus in previous releases of this plugin, none of the changes a user made to the option settings of a hook script would be reflected until the user either quit and restarted Vim or when they manually ran :FindVimHooks, which wipes out everything and reinitializes all hooks from scratch.

In this release I've fixed that; any edits made to a VimHook script will trigger an automatic re-parsing of the script so that any option settings which may have changed will be immediately reflected. In the background, this is achieved by just setting up an autocmd listener to BufWritePost events on *vimhook* files. Nothing new is required from a user perspective — things should Just Work™.

Feed Keys

This one is actually pretty cool. Suppose you have the vimhook.bufferoutput option setting enabled for some hook, and suppose the output is a few hundred lines long. Perhaps you're writing a bunch of unit tests and each new one you write is outputting stuff to an ever-growing chunk of stdout. Now, every time the hook is triggered, that output will get refreshed just as you wanted, but because of the implementation, the window it's in will get scrolled back up to line 1. This becomes annoying when all you really care about is the most recent stuff at the bottom of the output (in this example, the latest unit test you've just written). Luckily, the vimhook.bufferoutput.feedkeys option has come to the rescue. Now you can pass in an arbitrary string of Normal mode commands to run in the output buffer after every hook execution. So something like:

# vimhook.bufferoutput.feedkeys = G

will make the buffer output window scroll to the very bottom every time it's updated. Cool, huh?

Run Hooks in "Debug" Mode

Can't quite remember why I did this, but I'm sure it was quite useful at the time. There is a new buffer-local mapping to r in the :ListVimHooks window which will run a VimHook manually in "debug" mode, meaning all output is echoed to screen. Useful for troubleshooting a hook that is asynchronous and for some reason not doing what you thought it would.

Ignore Enabled State in VimHook Listing

By default, :ListVimHooks will list enabled scripts first, in lexicographical order, followed by disabled scripts, in lexicographical order. If for whatever reason it makes more sense to you to list all hooks in lexicographical order without grouping them together into "enabled" and "disabled" groups, you can set the global option g:vimhooks_list_enabled_first to something falsey (like 0). The default is false.

Prettier Paths

Took a stab at replacing $HOME with ~ in the :ListVimHooks output just 'cause. It works fine in most cases but breaks down when $HOME is a symlink to someplace else. If you see that happening in your case, you'll have to just ignore it and move on until I fix the logic to handle that case.

Set Buffer Output Filetype

There is a new hook option, vimhook.bufferoutput.filetype, which sets the filetype of the output buffer to whatever value is provided. Useful if you want to get syntax highlighting or some other filetype-specific goodness from the output buffer. The default is unset.

2.1.0: Async, Debounce, Error Messaging

29 Jan 05:42
@ahw ahw

Choose a tag to compare

This update includes some awesome new functionality:

Asynchronous Hooks

You can set the vimhook.async option in a hook script to execute the script in a forked process. It's just appending a & to the end of the shell command. There isn't any callback functionality so any exit codes, stdout, and stderr data will just be lost in the ether. Also worth noting: although hooks will still start execution in lexicographic order, making a hook async means that it's async. Subsequent hooks will not wait for it to finish.

Debounced Hooks

You can set the vimhook.debounce.wait: N option in a hook script to execute the script in a forked process after N seconds have elapsed since the last trigger of this particular hook. Useful if you tend to trigger a hook multiple times in rapid succession but only really care about the result of the last one (e.g., reloading a browser tab after making lots of tiny little tweaks). Debounced hooks are implicitly async, so the disclaimers in the section above hold for debounced hooks too.

Verbose Error Reporting

For synchronous hooks (still the default) that exit with a non-zero code you'll now see any stdout and stderr data echoed back to the screen. So now instead of just telling you that your build script exited with a non-zero code, it'll actually show you the stack traces and messages you'd see if you were running the build script directly in the console.

VimHooks stderr output

2.0.0 One Regex to Rule Them All

09 Jan 05:39
@ahw ahw

Choose a tag to compare

This release introduces breaking changes in the way VimHooks parses out event and filename matching information from the hook filename. Instead of three separate naming schemes for global, extension-specific, and filename-specific hooks, there is now just one single scheme. This simplifies a lot of the code and makes the naming scheme consistent, intuitive, and easier to remember. Additional bonus: the leading dot is now irrelevant, so you can make your hook files regular or hidden and either way is fine. The general form of this pattern is

[.][sortkey.][suffix.]eventname.vimhook[...]

The leading dot, sortkey, and suffix properties are optional. The sortkey property works the same as before, it just allows the user to ensure VimHooks execute in the correct lexicographic order. The suffix property is used to build a regex of the form .*suffix$. Any files whose autocmd event matches the hook event, and whose filename matches against this regex will trigger the hook. Thus, the global, extension-specific, and filename-specific use cases can all still be accomplished, but now this happens with just a single unified naming scheme.

  • Global hooks will have no suffix, since all filenames will match against .*$.
  • Extension-specific hooks will have their extension as a suffix since .*js$ matches all *.js files.
  • Filename-specific hooks will have their entire filename as a suffix. Although technically .*main.js will match any file ending in "main.js", I think in practice the only file that will actually meet this criteria is "main.js" itself.

Fixed a few bugs and as usual made a million edits to the README. Check out that fancy vector graphic in the VimHook naming pattern section.

1.4.1

12 Dec 05:48
@ahw ahw

Choose a tag to compare

Options

This release introduces support for VimHook options which are set in the source code of a VimHook script. I chose this scheme because I wanted to avoid having to rely on some arbitrary config file. Users can simply insert specially-formatted comment lines in their hook scripts to set these options.

The only two options supported in this release are:

  • vimhook.bufferoutput When true, this tells the plugin to dump stdout from this hook script into a new scratch buffer which is opened in a new Vim window. If a window for this output already exists, it is updated with the latest stdout result.
  • vimhook.bufferoutput.vsplit When true, the plugin will open the aforementioned "bufferoutput" window in a vertical split instead of a horizontal one. This option is only relevant when the vimhook.bufferoutput option is true.

Full details of the syntax of these option lines is available in the README.

Buffer Output

When the vimhook.bufferoutput option is set, the plugin will dump stdout from the hook script into a scratch buffer and open that buffer in a new Vim window. It does this by running

execute 'silent %!/path/to/vimhook.sh'

The silent %! part means "run the following external script and dump the result between lines 1 and $ of this buffer (i.e., all the lines).

VimHook Model

The VimHook model now has a new property which I have given the wonderfully confusing name, optional. The optional property is a Dictionary mapping option key Strings to option value Strings or Numbers. The VimHook model also has a new Function, getOptionValue(String key) which attempts to coerce the option value corresponding to key into a Number value 0 or 1, but will just return the option value as a String if that fails.

So if a VimHook script contained the following lines:

# vimhook.bufferoutput
# vimhook.bufferoutput.vsplit
# vimhook.optionFoo = true
# vimhook.option.bar: false

Then the resulting VimHook model would have the following properties:

  • ... (all the other properties I've documented in the past)
  • optional
    • "bufferoutput": 1 (option keys with no value default to 1)
    • "buffferoutput.vsplit": 1
    • "optionFoo": "true"
    • "option.bar": "false"
  • getOptionValue(String key): Function. If value is "1" or "true" return 1. Else if value is "0" or "false" return 0. Else return the option value as a String.

Miscellaneous

  • Pressing d in the :ListVimHooks window will run rm -i on the shell, thus providing a crude method for removing hooks directly from the listing window.
  • :CreateNewVimHook command implemented but not documented yet in the README. It's not quite ready for release but I had some git maintenance issues and ended up having to rewrite some history and in process, put this commit on master. Not ideal, but that's life.
  • Vim will print a warning (in echohl Warning colors) when a hook script returns a non-zero exit code.
  • Spell checking is always disabled in the VimHook listing window (:set nospell)

History Rewrite

As mentioned above, I had to run a git push --force before this release and thus ended up rewriting my history on both master and development. The reason was I had merged a bunch of commits from development to master, ran a git rebase -i on master to clean things up, and then pushed both branches out. I didn't realize this would create a series of very similar (but not exactly identical, depending on which commits I squashed, reordered, etc. during the rebase) commit paths on master and development. Because the rebase forced master and development to become separate branches in the graph, I soon found out that any future merges from development to master would again apply what were effectively the same bunch of commits I had merged earlier. Probably no one will ever read this and even fewer people will care, so I bit the bullet and did what everyone always says to never ever do and ran git push --force to fix things up the way I wanted. So far the sky has not fallen and the Git police have not come knocking on my door to take away my programmer badge. Also I've learned a valuable lesson about git rebase -i and remote branches.

1.3.1

10 Oct 03:56
@ahw ahw

Choose a tag to compare

This release fixes the major bug where VimHooks were not executing in lexicographic order. The underlying implementation changed a fair amount in order to accommodate this fix. Specifically, all VimHooks are stored in a single list, and when one of the listened-for autocmd events fires (e.g., BufWritePost) the plugin will iterate through all members of this master list and for each member, check if the hook pattern matches the current filename. If there is a match, the plugin will add that VimHook to a dictionary which maps filenames to a list of VimHooks whose pattern matches against that filename. After this is done once for a given filename, the plugin will no longer iterate through every VimHook in the master list when it receives subsequent events associated with that filename. Thus, it sets up a sort of cache of VimHooks for each newly-encountered filename in order to minimize the number of times it must scan through the entire list of VimHooks. The benefit of all this is that the plugin can keep VimHooks in lexicographic order while it is adding them to the dictionary of filenames-to-list-of-VimHooks.

There is also a small enhancement which adjusts the width of various columns in the :ListVimHooks buffer in order to accommodate the longest string in each column.

Finally, the VimHook model has changed a bit. A VimHook has the following properties:

  • path the relative path to the hook
  • id the relative path to the hook with ".disabled" removed from the name if it exists. This is done so that we can enable and disable the hook (which changes the hook filename) without having to change the id
  • baseName the filename part of the path
  • isEnabled
  • unixStylePattern the UNIX-style glob pattern corresponding to the pattern regex. Used in the :ListVimHooks output since regexes are more difficult to read
  • event the autocmd event name that triggers the hook
  • pattern the filename matching pattern
  • isIgnoreable used internally to stop triggering a VimHook without actually disabling it. For example, if the hook does not have the execute bit set and the user does not allow it to be set after the prompt, the plugin will set this flag to 1 in order to skip over it for the duration of the session and avoid continuously prompting the user to set the execute bit over and over again.

1.3.0

10 Sep 16:25
@ahw ahw

Choose a tag to compare

Mostly more work on :ListVimHooks. Created a NERDTree-like mapping for o and changed the mapping of <cr> to also act like o. Either of these keys will attempt to open a VimHook script in the previous window. If the buffer in the previous window is unmodified and isn't a special sort of window like quickfix or the NERDTree window itself, vim-hooks will overtake it with the script file. If the buffer is modified but it exists in more than one window it will also be overtaken. In the cases where these conditions are not met, VimHooks just creates a vertical split. I co-opted the code from NERDTree directly.

1.2.0

09 Sep 05:31
@ahw ahw

Choose a tag to compare

Revamped :ListVimHooks implementation. Instead of creating a hacky while loop to capture all keystrokes and handle individually, it just leaves everything as-is and does an nnoremap for the few special keys we want to override. It does this only for the hooks filetype, which is the filetype of the buffer that is opened with :ListVimHooks.

Key mappings for the :ListVimHooks buffer:

  • x: Toggle a VimHook script enabled/disabled state
  • q: Close the buffer
  • <esc>: Close the buffer (duplicate mapping)
  • <cr>: Close the buffer (duplicate mapping)
  • i: Open the relevant VimHook script in a split
  • s: Open the relevant VimHook script in a vertical split

Note: in general, prevents modification of the buffer by setting nomodifiable. Before each x toggle action we do have to set modifiable of course, but then nomodifiable is set again before finishing the function.

1.1.0

17 Aug 03:27
@ahw ahw

Choose a tag to compare

Added a nice pretty output for :ListVimHooks. Uses some Gmail-inspired controls. j and k move up and down. x enables/disables a VimHook. <ESC> exits.

Internally, the code also now uses VimHook class or whatever it's called in VimL to try and encapsulate some functionality and keep things sane.

A VimHook has the following properties:

  • path
  • isEnabled
  • scope ("global", "filename", or "extension")
  • scopeKey (not applicable if a global scope VimHook; the filename or extension for filename- and extension-scoped VimHooks, respectively)

A VimHook has the following functions:

  • enable() Set the executable bit and ensure that self.path doesn't end in ".disabled"
  • disable() Append ".disabled" to self.path
  • toggleIsEnabled() Do one of the above, depending on the value of self.isEnabled

1.0.0

13 Aug 20:25
@ahw ahw

Choose a tag to compare

Hello! Basic functionality described in README is there. Following is an excerpt showing the commands available.

" Find all hook files in the current working directory
command! -nargs=0 FindHookFiles call <SID>findHookFiles()

" Manually execute hook files corresponding to whichever events are given as
" the arguments to this function. Will autocomplete event names. Example:
" :ExecuteHookFiles BufWritePost VimLeave. Currently only executes the
" global hook files.
command! -nargs=+ -complete=event ExecuteHookFiles call <SID>executeHookFiles(<f-args>)

" Manually start and stop executing hooks
command! -nargs=0 StopExecutingHooks call <SID>stopExecutingHooks()
command! -nargs=0 StartExecutingHooks call <SID>startExecutingHooks()

" Pretty-print a list of all the vimhook dictionaries for debugging
command! -nargs=0 ListVimHooks call <SID>listVimHooks()