Home

Awesome

<neovim-editor> Web Component

Build Status

This component provides <neovim-editor>, an HTML custom element built on Polymer v2 and flux. It provides a frontend for the Neovim editor using Neovim's MessagePack API. It allows you to easily embed a Neovim-backed editor into your application.

This component assumes to be used in Node.js environment. (i.e. Electron)

You can use this component for modern desktop application frameworks such as Electron or NW.js. You can even use it in Electron-based editors such as Atom or VS Code.

This component is designed around the Flux architecture. You can access the UI event notifications and can call Neovim APIs directly via <neovim-editor>'s APIs.

You can install this component as an npm package.

$ npm install neovim-component

Current supported nvim version is v0.1.6 or later.

Examples

Each example only takes 100~300 lines.

Minimal Example

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <script src="/path/to/webcomponents-lite.js"></script>
    <link rel="import" href="/path/to/polymer.html" />
    <link rel="import" href="/path/to/neovim-editor.html" />
  </head>
  <body>
    <neovim-editor></neovim-editor>
  </body>
</html>

Minimal Electron app can be found in the example directory. This is a good start point to use this package and it shows how the component works.

main screenshot

How to run minimal example is:

$ git clone https://github.com/rhysd/neovim-component.git
$ cd neovim-component
$ npm start

Markdown Editor Example

For a more complicated and realistic example, see the markdown editor example. The markdown previewer is integrated with the Neovim GUI using the <neovim-editor> component.

markdown example screenshot

Image Popup Example

This is an image popup widget example here. The gi mapping is defined to show an image under the cursor in a tooltip.

image popup example screenshot

Mini Browser Example

This example shows how to include a mini web-browser using the <webview> tag from Electron.

mini browser example screenshot

Why Did You Create This?

Vim has very powerful editing features, but Vim is an editor (see :help design-not) and unfortunately lacks support for many graphical tools that writers and programmers like. NyaoVim adds support for graphical features without losing Vim's powerful text editing abilities. Neovim's msgpack APIs provide a perfect way to add a GUI layer using HTML and CSS. NyaoVim is a GUI frontend as a proof of concept.

Architecture

data flow

<neovim-editor> has an editor property to access the internal APIs of the component.

<neovim-editor> Properties

You can customize <neovim-editor> with the following properties:

NameDescriptionDefault
widthWidth of the editor in pixels.null
heightHeight of the editor in pixels.null
fontName of the editor's monospace font."monospace"
font-sizeFont-size in pixels.12
line-heightLine height rate relative to font size.1.3
nvim-cmdCommand used to start Neovim."nvim"
argvArguments passed with the Neovim command.[]
on-quitCallback function to run when Neovim quits.null
on-errorCallback function for Neovim errors.null
disable-alt-keyDo not send alt key input to Neovim.false
disable-meta-keyDo not send meta key input to Neovim.false
cursor-draw-delayDelay in millisec before drawing cursor.10
no-blink-cursorBlink cursor or not.false
window-titleSpecify first window title."Neovim"

<neovim-editor> APIs

Receive internal various events

You can receive various events (including UI redraw notifications) from the store. The store is a part of flux architecture. It's a global instance of EventEmitter.

You can also access the state of editor via the store. Note that all values are read only. Do not change the values of the store directly, it will break the internal state of the component.

const neovim_element = document.getElementById('neovim');
const Store = neovim_element.editor.store;

// Handle cursor movements
Store.on('cursor', () => console.log('Cursor is moved to ', Store.cursor));

// Handle mode changes
Store.on('mode', () => console.log('Mode is changed to ', Store.mode));

// Handle text redraws
Store.on('put', () => console.log('UI was redrawn'));

// Accessing the state of the editor.
const bounds = [ Store.size.lines, Store.size.cols ];
const cursor_pos = [ Store.cursor.line, Store.cursor.col ];

Call Neovim APIs

You can call Neovim APIs via the client. When you call APIs via the client, it sends the call to the underlying Neovim process via MessagePack RPC and will return a Promise which resolves to the returned value.

<neovim-component> uses promised-neovim-client package. You can see the all API definitions here. If you know further about Neovim APIs, python client implementation may be helpful.

const neovim_element = document.getElementById('neovim');
const client = neovim_element.editor.getClient();

// Send a command
client.command('vsplit');

// Send input
client.input('<C-w><C-l>');

// Evaluate a Vim script expression
client.eval('"aaa" . "bbb"').then(result => console.log(result));

// Get the 'b:foo' variable
client.getCurrentBuffer()
    .then(buf => buf.getVar('foo'))
    .then(v => console.log(v));

// Query something (windows, buffers, etc.)
// Move to the neighbor window and show its information.
client.getWindows()
    .then(windows => client.secCurrentWindow(windows[1]))
    .then(() => client.getCurrentWindow())
    .then(win => console.log(win));

// Receive an RPC request from Neovim
client.on('request', (n, args, res) => console.log(`Name: ${n}, Args: ${JSON.stringify(args)}, Response: ${res}`));

Editor lifecycle

You can receive notifications related to lifecycle of the editor.

const neovim_element = document.getElementById('neovim');

// Called when the Neovim background process attaches
neovim_element.editor.on('process-attached', () => console.log('Neovim process is ready'));

// Called when the Neovim process is disconnected (usually by :quit)
neovim_element.editor.on('quit', () => console.log('Neovim process died'));

// Called when the <neovim-component> detaches
neovim_element.editor.on('detach', () => console.log('Element does not exist in DOM.'));

// Called upon experiencing an error in the internal process 
neovim_element.editor.on('error', err => alert(err.message));

View APIs

const editor = document.getElementById('neovim').editor;
editor.screen.resize(80, 100); // Resize screen to 80 lines and 100 columns
editor.screen.resizeWithPixels(1920, 1080); // Resize screen to 1920px x 1080px
const editor = document.getElementById('neovim').editor;
editor.screen.changeFontSize(18); // Change font size to 18px
const editor = document.getElementById('neovim').editor;

const loc = editor.screen.convertPositionToLocation(80, 24);
console.log(loc.x, loc.y);  // Coordinates in pixels of (line, col) = (80, 24)

const pos = editor.screen.convertLocationToPosition(400, 300);
const.log(pos.col, pos.line);  // line/col of location (400px, 300px)

When some process has changed the screen-size you must notify the screen. The internal <canvas> element has a fixed size and must update itself if there are size changes. Call screen.checkShouldResize() if the screen size may have changed. Note that you don't need to care about resize event of <body> element. <neovim-editor> component automatically detects this particular resize event and updates automatically. screen.checkShouldResize() will simply be ignored if nothing has actually changed.

const editor = document.getElementById('neovim').editor;

function showUpSomeElementInNeovim() {
    const e = document.getElementById('some-elem');

    // New element shows up!  The screen may be resized by the change.
    // 'none' -> 'block'
    e.style.display = 'block';

    // This call tells to editor to adjust itself in the case that it has been resized
    editor.screen.checkShouldResize();
}

Other APIs

If your app doesn't use Polymer you can set arguments afterwards using JavaScript Note that it is better to use argv property of <neovim-element> if possible.

const editor = document.getElementById('neovim').editor;
editor.setArgv(['README.md']);

<neovim-editor> is just a web-component, so it can be focused just like other elements.
If it loses focus the editor won't receive any input events. The editor instance has a method to re-focus the editor in JavaScript. The store instance contains the current focus state.

const editor = document.getElementById('neovim').editor;
console.log(editor.store.focused);
editor.store.on('focus-changed', () => {
    console.log('Focus was changed: ' + editor.store.focused);
});

// Refocus the editor to ensure it receives user input.
editor.focus();

Log Levels

<neovim-component> prints logs in the browser console. The log level is controlled by the NODE_ENV environment variable:

TODOs