Library for manipulating text ranges and selections, and assorted other programs that use that

View the Project on GitHub dwachss/bililiteRange

The ex editor for bililiteRange

This is an implementation of the ex line editor, without the visual mode (this is not vi or vim).



That’s it. commandstring is, in general, anything that would be acceptable for the command line in ex.


replaces every 'foo' with 'bar'.

bililiteRange(element).ex('1i "First new line\nSecond new line"');

prepends two new lines to the element.

Commands are separated by '|'. To include that, or any special character, put the parameter in quotes:

range.ex(String.raw`a "foo\nbar|baz"`);

appends two lines after the current one, the second being bar|baz. The string in quotes is passed to JSON.parse, which is why the \n is escaped with String.raw. If not, it would be an actual newline character and as such, illegal in a JSON string. Using range.ex('a foo\nbar|baz'); would get the newline appended correctly but would read the vertical bar as a command separator, append foo\nbar then give an error for the unknown command baz.

The “current line” is set to the line containing the start of range.bounds().

After executing the command, the bounds of the range are set to the end of the “current line” as defined in the ex manual.


Convenience function that returns a function that calls bililiteRange.prototype.ex. Useful as an event handler, or a toolbar run function. Intended to be used directly in the user interface, so it acts on the selection.


button.addEventListener ('click', range.executor (opts) );

The possible options are {command: string, defaultaddress: string = '%%', returnvalue: anything}.

range.executor (opts) returns a function that runs ex(opts.command, opts.defaultaddress) on the selection (the selection at the time the function is run), and returns returnValue. If command is not defined, then the function takes a single argument that is the command to be run. Thus:

Promise.resolve( prompt('Enter a command')).then( range.executor() );

returnValue is necessary because event handlers may want to return false to prevent further handling, but Promise.then functions should return undefined to pass the result through unchanged.

If defaultaddress is not set, then '%%' is used, meaning the entire selection, not the whole line.

Note that ex acts on the range, independent of the actual selection, executor acts on the selection.


Javascript is not directly connected to the file system, so bililiteRange.ex creates several options:

For messages, is a function that displays informational messages, and is a function that displays errors. The defaults are:

bililiteRange.createOption ('stdout', {value: console.log, enumerable: false});
bililiteRange.createOption ('stderr', {value: console.error, enumerable: false});

But you could change that: = alert; = error => alert('⚠️ ' + error.toString());

For “files”, is a function (filename, directoryname) that returns a promise that should resolve to the “read” text. is a function (text, filename, directoryname) that returns a promise that resolves when the text is “written”. The defaults are :

bililiteRange.createOption ('reader', {
	value: async (filename, directoryname) => localStorage.getItem(filename)
bililiteRange.createOption ('writer', {
	value: async (text, filename, directoryname) => localStorage.setItem(filename, text)

using localStorage. But you could use jQuery: = async (filename, directoryname) => $.get(filename); = async (text, filename, directoryname) => $.post(filename, {text: text});
// This assumes your server knows what to do with GET and PUT requests to the URL that 'filename' represents

The async functions turn all the return values into Promises.

file and directory are ex options, set with range.ex('file foo').ex('directory bar'). The defaults are: = this.window.location.protocol + '//' + this.window.location.hostname; = this.window.location.pathname;

which is probably the most useful defaults for AJAX.

Regular Expressions

ex regular expressions are different from Javascript RegExp’s. To make my life easier, bililiteRange.ex uses the Javascript RegExp’s. range.ex(String.raw`/\bfoo\b/`) to find a line with the whole word 'foo',
not range.ex(String.raw`/\<foo\b/>`). For strings with backslashes, String.raw is your friend.

Rather than searching backwards with '?foo?', use the extended flags: '/foo/b'.

Similarly, the replace string for the substitute command uses the Javascript replace replacement string.

Special address mode

ex is line-oriented. range.ex('.') refers to the entire line. bililiteRange.ex adds a special address, %%, which refers to the current bounds, without extending to the entire line. range.ex('%%copy $') appends range.text() to the end of the element.

Specific commands

count and flags on commands are not implemented (flags on regular expressions are).


Not implemented


Not implemented


This is the same as chdir or set directory. It sets the


Treats file as an option rather than a command. This means that file with no parameters sets the file name to the empty string. To get the file name, use file?.


The algorithm may fail if lines are added after the line just after the line that is matched. %g/^/ .+2 a foo will match every line, then add a line 3 lines later, and the loop will increment by one (the number of lines added) and skip the next matched line but match the newly inserted foo.

The algorithm as described in the manual does two passes: first mark all the matching lines, then do the commands on each of them. I’m not sure how to do that with bililiteRange.

There is a bililiteRange option global, that is used for regular expression searches. The global command will allow it to be set like an option: global on, global off, set global, set noglobal, etc. and to send the value to stdout with global ? or set global ?. There is no ambiguity with the command global: global /RegExp/.


Not implemented.


This is meant to set keystrokes for vi, so it isn’t really implemented. As a hook for implementing it, the map command dispatches a custom event of type map on the element. The parameter (the string following 'map') is split on the first space (respecting quoted strings) forming a left hand side and a right hand side. The detail of the event is set to

	command: 'map',
	variant: false /* for 'map' */, true /* for 'map!' */
	lhs: 'left hand side',
	rhs: 'right hand side'

The listener for this event can do what it wants with it, but must reverse it in response to the unmap command.

map without a parameter does not display the list of mappings.

next, number, open

Not implemented.


Saves the entire text to localStorage (the key is `ex-${}/${data.file}`, so change those if you want more than one element to edit).

Sets up an event listener for visibilitychange to save the text whenever the page is hidden (or closed), so if the page is closed without saving, re-open it and do rng.ex('recover'). If you are paranoid, do ` rng.listen (‘input’, evt => rng.ex(‘recover’));`


Just selects the lines.


There’s no way to actually “quit”. If the text was not saved, put up a confirmation dialog. If it was saved, or the user confirms, then a ‘quit’ event with {detail: rng.element} is dispatched on rng.window. The window can listen for that event and remove the editor element if needed.


read pulls the file in after the current lines.

read! is not a shell escape but a Javascript escape. Does this.text(Function (parameter).call(this), {select: 'end'});. So to reverse the first line of the element, do

range.ex('1read! return this.text().split("").reverse().join("")');

If the return value is undefined, then the text is unchanged.


Resets the text to localStorage[`ex-${}/${data.file}`]. See preserve


Not implemented.


Not implemented.


As with edit and read, uses to get the source file.


The confirm option is not implemented. Nonglobal regular expressions (without the g flag) act like javascript: only the first occurence of the match in the range of addresses will be changed, not the first occurence on each line. It uses bililiteRange.prototype.replace.

& is simply a synonym for substitute; both will repeat the last search if no regular expression is given. It’s not clear why that exists, since s works perfectly well.

~ uses the last regular expression seen in the editor (whether in global, substitute or an address) as the search term, and the last replacement from a substitute command (including & and ~), but the flags are reset.

The only way to get flags on a ~ command is with the defaults:, etc.

There is a subtlety of the parser that is worth noting: | separates commands, but the parser looks at s /foo/bar/i | /bar/ and sees two regular expressions, /foo/ and /i | / and won’t split on the |. Use a different delimiter: s #foo#bar#i | /bar/ works fine. The problem is that the substitute command parameter may have one, two, or three delimiters.

suspend, tag, unabbreviate

Not implemented.


Does range.undo(), undo-ing anything that changed the text with an input event. See the documentation for bililiteRange.undo.

Note that bililiteRange.ex automatically does range.initUndo() if necessary.


See map. Dispatches a map event, but with the detail field having a different command, and lhs set to the entire parameter:

	command: 'unmap',
	variant: false /* for 'unmap' */, true /* for 'unmap!' */
	lhs: 'parameter',
	rhs: undefined


Not implemented.


Always sends the whole text to (ignores the addresses)


Not implemented.


Not implemented.


Like read but does not replace the text. Does Function (parameter).call(this).


Not implemented, but should be.


All options are actually bililiteRange options. So autoindent can be set with ` = true or range.ex(‘autoindent on’) or range.ex(‘set autoindent’) and unset with = false or range.ex(‘autoindent off’) or range.ex(‘set noautoindent’)`.


append, change and insert respect the autoindent option,, with the variant reversing the value of the option.

autoprint, autowrite, beautify, edcompatible, errorbells, exrc

Not implemented.


Implemented as described for bililiteRange.bounds(RegExp, flags).


Not implemented.


Implemented as described for bililiteRange.bounds(RegExp, flags). Specifically, nomagic means all characters are taken literally. Escaping them with backslashes does not make them special again.

mesg, number

Not implemented.


Not relevant to ex, so not implemented as an ex option. The idea of a paragraph separator is implemented in the find routines.

prompt, readonly, redraw, remap, report, scroll

Not implemented.


Not implemented, as paragraphs above.


Not implemented.


Alias for

showmatch, showmode, slowopen

Not implemented.


Alias for range.options.tabsize.

taglength, tags, term, terse, warn, window, wrapmargin

Not implemented.


Implemented as described for bililiteRange.bounds(RegExp, flags).


Not implemented.

Nonstandard commands

These were too useful to not include


Does range.redo().


Does range.sendkeys(parameter)