Skip to content

Commit

Permalink
Merge branch 'jsevalfunc' into wasm
Browse files Browse the repository at this point in the history
  • Loading branch information
rhysd committed Jul 31, 2019
2 parents 6ad97b5 + d4857c1 commit 05b2723
Show file tree
Hide file tree
Showing 14 changed files with 534 additions and 28 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ src/kword_test

# Wasm
/src/a.out.js
/src/a.out.wasm
/src/vim.bc
/autom4te.cache

Expand Down
92 changes: 92 additions & 0 deletions src/evalfunc.c
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ static void f_items(typval_T *argvars, typval_T *rettv);
static void f_join(typval_T *argvars, typval_T *rettv);
static void f_js_decode(typval_T *argvars, typval_T *rettv);
static void f_js_encode(typval_T *argvars, typval_T *rettv);
#ifdef FEAT_GUI_WASM
static void f_jsevalfunc(typval_T *argvars, typval_T *rettv);
#endif
static void f_json_decode(typval_T *argvars, typval_T *rettv);
static void f_json_encode(typval_T *argvars, typval_T *rettv);
static void f_keys(typval_T *argvars, typval_T *rettv);
Expand Down Expand Up @@ -719,6 +722,9 @@ static struct fst
{"join", 1, 2, f_join},
{"js_decode", 1, 1, f_js_decode},
{"js_encode", 1, 1, f_js_encode},
#ifdef FEAT_GUI_WASM
{"jsevalfunc", 1, 3, f_jsevalfunc},
#endif
{"json_decode", 1, 1, f_json_decode},
{"json_encode", 1, 1, f_json_encode},
{"keys", 1, 1, f_keys},
Expand Down Expand Up @@ -6405,6 +6411,9 @@ f_has(typval_T *argvars, typval_T *rettv)
#ifdef FEAT_GUI_MSWIN
"gui_win32",
#endif
#ifdef FEAT_GUI_WASM
"gui_wasm",
#endif
#ifdef FEAT_HANGULIN
"hangul_input",
#endif
Expand Down Expand Up @@ -8958,6 +8967,89 @@ f_pyxeval(typval_T *argvars, typval_T *rettv)
}
#endif

#ifdef FEAT_GUI_WASM
/*
* "jsevalfunc()" function
*/
static void
f_jsevalfunc(typval_T *argvars, typval_T *rettv)
{
char_u *script;
char_u *args_json = NULL;
char *ret_json = NULL;
int just_notify = 0;

if (check_restricted() || check_secure()) {
return;
}

script = tv_get_string_chk(&argvars[0]);
if (script == NULL) {
return;
}

if (argvars[1].v_type != VAR_UNKNOWN) {
list_T *args_list = NULL;
listitem_T *item;

if (argvars[1].v_type != VAR_LIST) {
emsg(_(e_listreq));
return;
}

if (argvars[2].v_type != VAR_UNKNOWN) {
int error = FALSE;

just_notify = tv_get_number_chk(&argvars[2], &error);
if (error) {
return;
}
}

args_json = json_encode(&argvars[1], 0);
if (*args_json == NUL) {
// Failed to encode argument as JSON
vim_free(args_json);
return;
}
}

GUI_WASM_DBG("jsevalfunc: Will evaluate script '%s' with %s args (notify_only=%d)", script, args_json, just_notify);
ret_json = vimwasm_eval_js((char *)script, (char *)args_json, just_notify);
if (args_json != NULL) {
vim_free(args_json);
}

GUI_WASM_DBG("jsevalfunc: vimwasm_eval_js() returned %s", ret_json);
if (ret_json == NULL) {
// Two cases reach here.
// 1. Error occurred in vimwasm_eval_js() and it returned NULL
// 2. just_notify is 1 so the result was not returned from vimwasm_eval_js()
//
// Note: On 1., error output should already be done by calling emsg() from JavaScript
return;
}

// Note: `ret_json` may be empty. It means undefined was passed to JSON.stringify().
// An empty string is mapped to v:none by json_decode() Vim script function.
{
js_read_T reader;

reader.js_buf = (char_u *) ret_json;
reader.js_fill = NULL;
reader.js_used = 0;

json_decode_all(&reader, rettv, 0);
}

// Note: vim_free is not available since the pointer was allocated by malloc()
// directly in JavaScript runtime.
free(ret_json);

// Note: Do not free `script` since it was not newly allocated
}
#endif

/*
* "range()" function
*/
Expand Down
6 changes: 0 additions & 6 deletions src/gui_wasm.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@
#endif
#define RGB(r, g, b) (((r)<<16) | ((g)<<8) | (b))

#ifdef GUI_WASM_DEBUG
#define GUI_WASM_DBG(fmt, ...) printf("C: %s: " fmt "\n", __func__, __VA_ARGS__)
#else
#define GUI_WASM_DBG(...) ((void) 0)
#endif

static int clipboard_available = TRUE;

/*
Expand Down
6 changes: 6 additions & 0 deletions src/vim.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@

# undef HAVE_SELINUX
# undef HAVE_ICONV

# ifdef GUI_WASM_DEBUG
# define GUI_WASM_DBG(fmt, ...) printf("C: %s: " fmt "\n", __func__, __VA_ARGS__)
# else
# define GUI_WASM_DBG(...)
# endif
#endif/* FEAT_GUI_WASM */

/*
Expand Down
1 change: 1 addition & 0 deletions src/wasm_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ int vimwasm_wait_for_event(int timeout);
int vimwasm_export_file(char *fullpath);
char *vimwasm_read_clipboard(void);
void vimwasm_write_clipboard(char *text, unsigned long size);
char *vimwasm_eval_js(char *script, char *args_json, int just_notify);

#endif /* FEAT_GUI_WASM */

Expand Down
18 changes: 9 additions & 9 deletions wasm/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,6 @@ module.exports = {
// Enabled
'no-console': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'mocha/no-exclusive-tests': 'error',
'mocha/no-skipped-tests': 'error',
'mocha/handle-done-callback': 'error',
'mocha/no-identical-title': 'error',
'mocha/no-mocha-arrows': 'error',
'mocha/no-return-and-callback': 'error',
'mocha/no-sibling-hooks': 'error',
'mocha/prefer-arrow-callback': 'error',
'mocha/no-async-describe': 'error',

// Configured
'@typescript-eslint/array-type': ['error', 'array-simple'],
Expand All @@ -77,6 +68,15 @@ module.exports = {
files: ['test/*.ts'],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'mocha/no-exclusive-tests': 'error',
'mocha/no-skipped-tests': 'error',
'mocha/handle-done-callback': 'error',
'mocha/no-identical-title': 'error',
'mocha/no-mocha-arrows': 'error',
'mocha/no-return-and-callback': 'error',
'mocha/no-sibling-hooks': 'error',
'mocha/prefer-arrow-callback': 'error',
'mocha/no-async-describe': 'error',
},
},
{
Expand Down
99 changes: 90 additions & 9 deletions wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

**WARNING!: This npm package is experimental until v0.1.0 beta release.**

## Introduction

This is an [npm][] package to install pre-built [vim.wasm][project] binary easily. This package contains:

- `vim.wasm`: WebAssembly binary
Expand Down Expand Up @@ -288,12 +290,96 @@ vim.start({
});
```
## TypeScript support
## Evaluate JavaScript from Vim script
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**.
To return a value from JavaScript to Vim script, `return` statement is necessary. Arguments are accessible
via `arguments` 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 returned
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, `jsevalfunc()` call always returns `0` and doesn't wait the JavaScript function
call has completed. If it is omitted, the default value is `v:false`. 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, `undefined`
is translated to `v:none` 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:
[npm package][npm-pkg] provides complete TypeScript support. Type definitions are put in `vimwasm.d.ts`
```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
## TypeScript Support
[This npm package][npm-pkg] provides complete TypeScript support. Type definitions are put in `vimwasm.d.ts`
and automatically referenced by TypeScript compiler.
## Sources
## Ported Vim
- Current version: 8.1.1661
- Current feature: normal and small
## Development
### Sources
This directory contains a browser runtime for `wasm` GUI frontend written in [TypeScript](https://www.typescriptlang.org/).
Expand All @@ -309,7 +395,7 @@ be generated. Please host this directory on web server and access to `index.htm
Files are formatted by [prettier](https://prettier.io/).
## Testing
### Testing
Unit tests are developed at [test](./test) directory. Since `vim.wasm` assumes to be run on browsers, they are run
on headless Chromium using [karma](https://karma-runner.github.io/latest/index.html) test runner.
Expand Down Expand Up @@ -337,11 +423,6 @@ npm run vtest
`npm test` is run at [Travis CI][travis-ci] for every remote push.
## Ported Vim
- Current version: 8.1.1661
- Current feature: normal and small
## Notes
### ES Modules in Worker
Expand Down
12 changes: 9 additions & 3 deletions wasm/common.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ interface SharedBufResponseFromWorker {
readonly buffer: SharedArrayBuffer;
readonly bufId: number;
}
type MessageFromWorkerWithoutTS =
type MessageFromWorkerWithoutTimestamp =
| {
readonly kind: 'draw';
readonly event: DrawEventMessage;
Expand Down Expand Up @@ -103,9 +103,15 @@ type MessageFromWorkerWithoutTS =
readonly kind: 'eval';
readonly path: string;
readonly contents: ArrayBuffer;
}
| {
readonly kind: 'evalfunc';
readonly body: string;
readonly argsJson: string | undefined;
readonly notifyOnly: boolean;
};

type MessageFromWorker = MessageFromWorkerWithoutTS & { timestamp?: number };
type MessageFromWorker = MessageFromWorkerWithoutTimestamp & { timestamp?: number };
type MessageKindFromWorker = MessageFromWorker['kind'];

type EventStatusFromMain = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
type EventStatusFromMain = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
5 changes: 5 additions & 0 deletions wasm/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ module.exports = function(config) {
included: false,
served: true,
},
{
pattern: './test/*.vim',
included: false,
served: true,
},
],
client: {
mocha: {
Expand Down
Loading

0 comments on commit 05b2723

Please sign in to comment.