Vim editor ported to WebAssembly
npm install vim-wasm[npm][] package for [vim.wasm][project]
=======================================
[![Build Status][travis-ci-badge]][travis-ci]
[![npm version][npm-badge]][npm-pkg]
[![code style: prettier][prettier-badge]][prettier]
WARNING!: This npm package is experimental until v0.1.0 beta release.
This is an [npm][] package to install pre-built [vim.wasm][project] binary easily. This package contains:
- vim.wasm: WebAssembly binary
- vim.js: Web Worker script to drive vim.wasm
- vim.data: Bundled preloaded files loaded into filesystem at start up
- vimwasm.js: ES Module to manage lifetime of Web Worker
- small/vim.{wasm,js,data}: Small feature version
Please read the following instructions to use this package. You can play with [live demo][demo].
For usage of the demo, please read usage documentation.
Install [npm package][npm-pkg] via npm command.
```
npm install --save vim-wasm
NOTE: This npm package is currently dedicated for browsers. It does not work with Wasm interpreters
outside browser like node.
Please see example directory for minimal live example and live demo for
more complicated example.
Put
`html`
Your script index.js must be loaded as type="module" because this npm package provides ES Module
unless you use some JS source bundler.
`javascript
import { VimWasm } from '/path/to/vim-wasm/vimwasm.js';
const vim = new VimWasm({
canvas: document.getElementById('vim-canvas'),
input: document.getElementById('vim-input'),
workerScriptPath: '/path/to/vim-wasm/vim.js',
});
// Setup callbacks if you need...
// Start Vim (give option object if necessary)
vim.start();
`
VimWasm class is provided to manage Web Worker lifecycle where Vim is running. Please import it fromvimwasm.js ES Module.
workerScriptPath is the most important option value which represents a file path to a worker scriptvim-wasm/vim.js
which runs Vim in Web Worker. By switching path to scripts and vim-wasm/small/vim.js,
you can switch feature set of Vim. Please read following 'Normal Feature and Small Feature' section.
VimWasm provides several callbacks to interact with Vim running in Web Worker. Please check
example code for the callbacks setup.
Finally calling start() method starts Vim in new Web Worker. You can pass an options object to the
method call to specify various options.
Serve index.html with HTTP server and access to it from a web browser.
NOTE: This project uses [SharedArrayBuffer][shared-array-buffer] and [Atomics API][atomics-api].
Only Chrome or Chromium-based browsers enable them by default. For Firefox and Safari, feature flag must
be enabled manually for now to enable them. Please also read notices in README.md at [the project page][project].
Following projects are related to this npm package and may be more suitable for your use case.
- react-vim-wasm: React component for [vim.wasm][project].
Vim editor can be embedded in your React web application.
- vimwasm-try-plugin: Command line tool to open vim.wasm including specified
Vim plugin instantly. You can try Vim plugin
without installing it!
- vim.wasm.ipynb: Jupyter Notebook integration with vim.wasm.
Try it online!
This package contains two binaries for 'normal' feature set and 'small' feature set.
'normal' feature set provides almost all Vim's powerful features but binary size is 4x bigger than
'small' feature set. 'small' feature set only provides basic features but binary size is much smaller.
| | Normal Feature | Small Feature |
|-----------------|---------------------|------------------------------------------------------------------------------------|
| Script path | vim-wasm/vim.js | vim-wasm/small/vim.js |
| Data size | 1.3 MB | 13 KB |
| Wasm size | 709 KB | 374 KB |
| Features | Almost all features | Not including syntax highlight, indentation, Vim script support, text objects, ... |
Data size is significantly different because 'normal' feature set includes syntax highlighting and indentation
support for all filetypes. In contrast 'small' feature set only includes colorscheme and minimal vimrc.
By passing a worker script path for each feature to workerScriptPath option, you can specify a feature set.
Please choose one considering your application's requirements.
`javascript
const normalVim = new VimWasm({
canvas,
input,
workerScriptPath: '/path/to/vim-wasm/vim.js',
});
const smallVim = new VimWasm({
canvas,
input,
workerScriptPath: '/path/to/vim-wasm/small/vim.js',
});
`
This npm package runs Vim in Web Worker and the main thread communicates with the worker thread via SharedArrayBuffer.SharedArrayBuffer
Chrome or Chromium based browser supports by default. Safari and Firefox supports it under a feature
flag due to Spectre vulnerability.
To check current browser can use this package, checkBrowserCompatibility() utility function is provided.undefined
It returns an error message as string if it is not compatible (otherwise returns ).
`javascript
const errmsg = checkBrowserCompatibility();
if (errmsg !== undefined) {
alert(errmsg);
return;
}
const vim = new VimWasm({...});
`
Passing debug: true to VimWasm.start() method call enables debug logging with console.log.
`javascript`
vim.start({ debug: true });
Note: Debug logs in C sources are not controlled by the query parameter. It is controlled GUI_WASM_DEBUG preprocessor macro.
Passing perf: true to VimWasm.start() method call enables the performance tracing.:qall!
After Vim exits (e.g. ), it dumps performance measurements in DevTools console as tables.
`javascript`
vim.start({ perf: true });
Note: For performance measurements, please ensure to use release build. Measuring with debug build does not make sense.
Note: Please do not use debug logging at the same time. Outputting console logs in DevTools slows application.
Passing program arguments of vim command is supported.
Passing cmdArgs option to VimWasm.start() method call passes the value as Vim command arguments.
`javascript`
vim.start({
cmdArgs: [ '~/.vim/vimrc', '-c', 'set number' ]
});
As shown in above example, this feature is useful when you want to open specific file at Vim startup.
``
:!/path/to/file.js
:! evaluates the JavaScript source file. In Vim's command line, % is replaced with the current buffer's:!%
file path so evaluates the current buffer.
Currently only *.js files are executable via :! and any other commands are not supported. Consolealert()
output is not captured. You need to open console in DevTools or use to show value.
The JavaScript code is evaluated in main thread so DOM APIs are available. If the code throws an exception,
the error message and stacktrace is shown as an error message in Vim.
Unlike :!, system() and systemlist() Vim script functions are explicitly not supported because their
calls in Vim plugins are intended to execute shell commands.
guifont option is available like other GUI Vim. All font names available in CSS are also available here.serif
Note that only monospace fonts are considered. If you specify other font like , junks may remain
on re-rendering a screen.
`vim`
" Use 'Monaco' font
:set guifont=Monaco
If you want to specify font height also, :h{pixels} suffix is available.
`vim`
" Use 'Monaco' font with 20px font height
:set guifont=Monaco:h20
Note that currently only font height can be specified in this format. And integer value is acceptable
since Vim contains font size as integer internally.
If you want to specify font name and font height from JavaScript, set it via VimWasm.cmdline method.
`javascriptset guifont=${fontName}:h{fontHeight}
vim.cmdline();`
VimWasm.start() method supports filesystem setup before starting Vim through dirs, files, fetchFilespersistentDirs
and options.
dirs option creates new directories on filesystem. They are created on memory using emscripten'sMEMFS by default. Note that nested directory paths are not available.
Notes:
- You must specify each parent directories to create a nested directory.
- Trying to create an existing directory causes an error.
`javascript`
// Create /work/documents directory
vim.start({
dirs: ['/work', '/work/documents'],
});
persistentDirs option marks the directories are persistent. They are stored on [Indexed DB][idb]IDBFS
thanks to emscripten's . They will remain even if user closes a browser tab.
Notes:
- Marking non-existing directories as persistent causes an error. Please set dirs correctly to ensure:quit
they exists.
- Files are synchronized when Vim exits. Closing browser tab without does not save files on Indexed DB.
This behavior may change in the future.
- This option adds overhead to load files from database at Vim startup.
`javascript`
// Create /work/persistent directory. Contents of the directory are persistent
vim.start({
dirs: ['/work', '/work/persistent'],
persistentDirs: ['/work/persistent'],
});
files option creates files on filesystem. It is an object whose keys are file paths and values are files'String
contents as .
Notes:
- Parent directories must exist for the files. If they don't exist, please create them by dirs option.
- You can overwrite default vimrc as below example.
`javascript`
// Create new file /work/hello.txt and overwrite vimrc
vim.start({
dirs: ['/work'],
files: {
'/work/hello.txt': 'hello, world!\n',
'/.vim/vimrc': 'set number\nset noexpandtab\n',
},
});
fetchFiles option fetches remote resources and map them to filesystem entries. It is an object whose
keys are file paths on filesystem and values are remote resource paths (relative file paths or URLs).
It fetches file paths or URLs just before starting Vim and put them on filesystem.
`javascript`
vim.start({
fetchFiles: {
// Fetch hosted 'vim.js' file and put it as '/foo.js' in filesystem
'/foo.js': '/vim.js',
// Fetch 'README.md' file from remote with URL and put it as '/bar.md' in filesystem
'/bar.md': 'https://raw.githubusercontent.com/rhysd/vim.wasm/wasm/README.md',
},
});
By using this option, external files are easily loaded onto filesystem. For example, if you fetch plugin
files, the plugin is available on Vim starting. [vimwasm-try-plugin][] uses this option to load specified
Vim plugin or colorscheme.
Resources (values of the object) are fetched with [fetch()][fetch]. Though all requests are sent
asynchronously, Vim waits all responses. Fetching many files or too large file would slows Vim start
up time so should be avoided.
To integrate JavaScript browser APIs into Vim script, jsevalfunc() Vim script function is implemented.
``
jsevalfunc({script} [, {args} [, {notify_only}]])
The first {script} argument is a string of JavaScript code which represents a function body.return
To return a value from JavaScript to Vim script, statement is necessary. Arguments are accessiblearguments
via object in the code.
The second {args} optional argument is a list value which represents arguments passed to the JavaScript
function. If it is omitted, the function will be called with no argument.
The third {notify_only} optional argument is a number or boolean value which indicates if returnedjsevalfunc()
value from the JavaScript function call is notified back to Vim script or not. If the value is truthy,
function body and arguments are just notified to main thread and the returned value will never be notified
back to Vim. In the case, call always returns 0 and doesn't wait the JavaScript functionv:false
call has completed. If it is omitted, the default value is . This flag is useful when the returned
value is not necessary since returning a value from main thread to Vim in worker may take time to serialize
and convert values.
The JavaScript code is evaluated in main thread as a JavaScript function. So DOM element and other Web APIs
are available.
`vim
" Get Location object in JavaScript as dict
let location = jsevalfunc('return window.location')
" Get element text
let selector = '.description'
let text = jsevalfunc('
\ const elem = document.querySelector(arguments[0]);
\ if (elem === null) {
\ return null;
\ }
\ return elem.textContent;
\', [selector])
" Run script but does not wait for the script being completed
call jsevalfunc('document.title = arguments[0]', ['hello from Vim'], v:true)
`
Since values are passed by being encoded in JSON between Vim script, arguments passed to JavaScript function
call and returned value from JavaScript function must be JSON serializable. As a special case, undefinedv:none
is translated to in Vim script.
`vim
" Error because funcref is not JSON serializable
call jsevalfunc('return "hello"', [function('empty')])
" Error because Function object is not JSON serializable
let f = jsevalfunc('return fetch')
`
The JavaScript function is called in asynchronous context. So await operator is available as follows:
`vim`
let slug = 'rhysd/vim.wasm'
let repo = jsevalfunc('
\ const res = await fetch("https://api.github.com/repos/" + arguments[0]);
\ if (!res.ok) {
\ return null;
\ }
\ return JSON.parse(await res.text());
\ ', [slug])
echo repo
jsevalfunc() throws an exception when:
- some argument passed at 2nd argument is not JSON serializable
- JavaScript code causes syntax error
- evaluating JavaScript code throws an exception
- returned value from the function call is not JSON serializable
[This npm package][npm-pkg] provides complete TypeScript support. Type definitions are put in vimwasm.d.ts
and automatically referenced by TypeScript compiler.
- Current version: 8.2.0055
- Current features: normal and small
This directory contains a browser runtime for wasm GUI frontend written in TypeScript.
- pre.ts, runtime.ts: Runtime to interact with main thread and Vim on Wasm. It runs on Web Worker.main.ts
- , vimwasm.ts: Runtime to render a Vim screen and take user key inputs. It runs on main thread and ispackage.json
responsible for starting Web Worker.
- : Toolchains for this frontend is managed by npm command.npm run build
You can build this runtime by . You can run linters (eslint,stylelint
) by npm run lint.
When you run ./build.sh from root of this repo, vim.wasm, vim.js, vim.data and main.js willindex.html
be generated. Please host this directory on web server and access to .
Files are formatted by prettier.
Unit tests are developed at test directory. Since vim.wasm assumes to be run on browsers, they are runnode_modules
on headless Chromium using karma test runner.
Basically unit test cases run Vim worker and check draw events from it. Headless Chromium is installed in
locally by puppeteer.
`shSingle run
npm test
npm run lint and npm run vtest are run at git push by [husky][] :dog:.npm test is run at [Travis CI][travis-ci] for every remote push.Notes
$3
ES Modules and JS bundlers (e.g. parcel) are not available in worker because of
emcc. emcc preprocesses input JavaScript
source (here runtime.js). It parses the source but the parser only accepts specific format of JavaScript code. The preprocessor
seems to ignore all declarations which don't appear in mergeInto call. Dynamic import is also not available for now.-
import statement is not available since the emcc JS parser cannot parse it
- Dynamic import is not available in dedicated worker: https://bugs.chromium.org/p/chromium/issues/detail?id=680046
- Bundled JS sources by bundlers such as parcel cannot be parsed by the emcc JS parser
- Compiling TS sources into one JS file using --outFile=xxx.js does not work since toplevel constants are ignored by
the emcc JS parser$3
There were 3 trials but all were not available for now.
- Send
to worker (runtime.js) by transferControlToOffscreen() and render draw events there. This is not available
since runtime.js runs synchronously with Atomics API. It sleeps until main thread notifies. In this case, calling draw methods
of OffscreenCanvas does nothing because rendering does not happen until JavaScript context ends (like busy loop in main thread
prevents DOM rendering).
- In main thread, draw to OffscreenCanvas and transfer the rendered result to on-screen as ImageBitmap. I tried
this but it was slower than simply drawing to directly. It is because sending rendered image to causes
re-rending whole screen.
- Create another worker thread (renderer.js) in worker (runtime.js) and send draw events to renderer.js. In renderer.js, it renders
them to OffscreenCanvas passed from main thread via runtime.js. This solution should be possible and is possibly faster than
rendering draw events in main thread. However, it is currently not available due to Chromium bug https://bugs.chromium.org/p/chromium/issues/detail?id=977924.
When a worker thread runs synchronously with Atomics API, new Worker` instance cannot start because new worker is createdDistributed under VIM License.
Example code is based on https://github.com/trekhleb/javascript-algorithms.
> Copyright (c) 2018 Oleksii Trekhleb
[npm]: https://www.npmjs.com/
[npm-pkg]: https://www.npmjs.com/package/vim-wasm
[project]: https://github.com/rhysd/vim.wasm
[shared-array-buffer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
[atomics-api]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics
[idb]: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
[travis-ci-badge]: https://travis-ci.org/rhysd/vim.wasm.svg?branch=wasm
[travis-ci]: https://travis-ci.org/rhysd/vim.wasm
[npm-badge]: https://badge.fury.io/js/vim-wasm.svg
[prettier-badge]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat
[prettier]: https://github.com/prettier/prettier
[demo]: https://rhysd.github.io/vim.wasm
[husky]: https://github.com/typicode/husky
[fetch]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
[vimwasm-try-plugin]: https://github.com/rhysd/vimwasm-try-plugin