diff --git a/build.zig b/build.zig index b6b2caece1..97db2c97e1 100644 --- a/build.zig +++ b/build.zig @@ -335,7 +335,7 @@ pub fn build(b: *std.Build) !void { .HAVE_STRPTIME = modern_unix, .HAVE_XATTR = is_linux, .HAVE_SYS_SDT_H = false, - .HAVE_SYS_UTSNAME_H = modern_unix, + .HAVE_SYS_UTSNAME_H = modern_unix or is_wasm, .HAVE_SYS_WAIT_H = false, // unused .HAVE_TERMIOS_H = modern_unix, .HAVE_WORKING_LIBINTL = t.isGnuLibC(), @@ -591,6 +591,7 @@ pub fn build(b: *std.Build) !void { if (is_wasm) { nvim_mod.addCSourceFiles(.{ .files = &.{ "src/wasm_stubs.c", + "src/static_ts_registry.c", }, .flags = &flags }); } @@ -600,6 +601,7 @@ pub fn build(b: *std.Build) !void { const nvim_exe_step = b.step("nvim_bin", "only the binary (not a fully working install!)"); const nvim_exe_install = b.addInstallArtifact(nvim_exe, .{ .dest_dir = if (is_wasm) .{ .override = .{ .custom = "wasm" } } else .default }); + const gen_runtime = try runtime.nvim_gen_runtime(b, nlua0, funcs_data); if (is_wasm) { const s = emscripten_sysroot orelse @panic("-Demscripten-sysroot required for wasm target"); @@ -616,13 +618,30 @@ pub fn build(b: *std.Build) !void { if (treesitter) |dep| emcc.addFileArg(dep.artifact("tree-sitter").getEmittedBin()); emcc.addArg("-Wl,--no-whole-archive"); + const merged_subdir = "wasm-runtime-merged"; + const merged_path = b.getInstallPath(.prefix, merged_subdir); + const install_static_runtime = b.addInstallDirectory(.{ + .source_dir = b.path("runtime"), + .install_dir = .prefix, + .install_subdir = merged_subdir, + }); + const install_gen_runtime = b.addInstallDirectory(.{ + .source_dir = gen_runtime.getDirectory(), + .install_dir = .prefix, + .install_subdir = merged_subdir, + }); + + emcc.step.dependOn(&install_static_runtime.step); + emcc.step.dependOn(&install_gen_runtime.step); + emcc.addArgs(&.{ b.fmt("--sysroot={s}", .{s}), + "-lidbfs.js", "-sALLOW_MEMORY_GROWTH=1", "--profiling-funcs", "-sEXPORTED_FUNCTIONS=_nvim_main,_malloc,_free", "-Wno-undefined", - "-sEXPORTED_RUNTIME_METHODS=stringToUTF8,lengthBytesUTF8,setValue,getValue,UTF8ToString,ENV,FS,HEAPU8", + "-sEXPORTED_RUNTIME_METHODS=stringToUTF8,lengthBytesUTF8,setValue,getValue,UTF8ToString,ENV,FS,HEAPU8,IDBFS", "-sSTACK_SIZE=8388608", "-sINITIAL_MEMORY=268435456", "-sMAXIMUM_MEMORY=2147483648", @@ -640,18 +659,18 @@ pub fn build(b: *std.Build) !void { "-sASSERTIONS=1", "-sASYNCIFY=1", "-sASYNCIFY_STACK_SIZE=65536", + b.fmt("--preload-file={s}@/runtime", .{merged_path}), }); emcc.addArg("-o"); const nvim_js = emcc.addOutputFileArg("nvim.js"); nvim_exe_step.dependOn(&b.addInstallFileWithDir(nvim_js, .bin, "nvim.js").step); nvim_exe_step.dependOn(&b.addInstallFileWithDir(nvim_js.dirname().path(b, "nvim.wasm"), .bin, "nvim.wasm").step); + nvim_exe_step.dependOn(&b.addInstallFileWithDir(nvim_js.dirname().path(b, "nvim.data"), .bin, "nvim.data").step); } else { nvim_exe_step.dependOn(&nvim_exe_install.step); } - const gen_runtime = try runtime.nvim_gen_runtime(b, nlua0, funcs_data); - const test_deps = b.step("test_deps", "test prerequisites"); if (!is_wasm) test_deps.dependOn(&nvim_exe_install.step); // running tests doesn't require copying the static runtime, only the generated stuff const test_runtime_install = b.addInstallDirectory(.{ @@ -747,6 +766,36 @@ pub fn build(b: *std.Build) !void { const parser_query = b.dependency("treesitter_query", .{ .target = target, .optimize = optimize_ts }); parser_deps.dependOn(add_ts_parser(b, "query", parser_query.path("."), false, target, optimize_ts)); + if (is_wasm) { + const WasmTsParser = struct { + name: []const u8, + dir: LazyPath, + has_scanner: bool, + }; + const wasm_ts_parsers = [_]WasmTsParser{ + .{ .name = "lua", .dir = parser_lua.path("."), .has_scanner = true }, + .{ .name = "c", .dir = parser_c.path("."), .has_scanner = false }, + .{ .name = "markdown", .dir = parser_markdown.path("tree-sitter-markdown/"), .has_scanner = true }, + .{ .name = "markdown_inline", .dir = parser_markdown.path("tree-sitter-markdown-inline/"), .has_scanner = true }, + .{ .name = "vim", .dir = parser_vim.path("."), .has_scanner = true }, + .{ .name = "vimdoc", .dir = parser_vimdoc.path("."), .has_scanner = false }, + .{ .name = "query", .dir = parser_query.path("."), .has_scanner = false }, + }; + + for (wasm_ts_parsers) |p| { + nvim_mod.addCSourceFile(.{ + .file = p.dir.path(b, "src/parser.c"), + .flags = &flags, + }); + if (p.has_scanner) { + nvim_mod.addCSourceFile(.{ + .file = p.dir.path(b, "src/scanner.c"), + .flags = &flags, + }); + } + nvim_mod.addIncludePath(p.dir.path(b, "src")); + } + } var unit_headers: ?[]const LazyPath = null; if (support_unittests) { try unittest_include_path.append(b.allocator, gen_headers.getDirectory()); diff --git a/contrib/test_wasm.html b/contrib/test_wasm.html index a33964166d..740596dadb 100644 --- a/contrib/test_wasm.html +++ b/contrib/test_wasm.html @@ -1,68 +1,213 @@ - - -nvim.wasm test - +nvim.wasm runtime + persistence test -

nvim.wasm load test

- +

nvim.wasm runtime bundling

+ - +(async () => { + await runtimeSanityCheck(); + for (const t of tests) await runTest(t); + await treesitterHighlightCapturesTest(); + moduleRef = await freshNvim(); + log("persistent session ready (use 'Persist now' button to sync IDBFS)"); +})().catch(e => log("test run failed: " + e)); + diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index c8b0857cda..34d690e764 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -444,6 +444,7 @@ list(APPEND UNCRUSTIFY_NVIM_SOURCES ${PROJECT_SOURCE_DIR}/src/tee/tee.c ${PROJECT_SOURCE_DIR}/src/xxd/xxd.c ${PROJECT_SOURCE_DIR}/src/wasm_stubs.c + ${PROJECT_SOURCE_DIR}/src/static_ts_registry.c ) foreach(sfile ${NVIM_SOURCES}) diff --git a/src/nvim/lua/treesitter.c b/src/nvim/lua/treesitter.c index dae9e88ee6..2bd8873318 100644 --- a/src/nvim/lua/treesitter.c +++ b/src/nvim/lua/treesitter.c @@ -42,6 +42,10 @@ #define TS_META_QUERYCURSOR "treesitter_querycursor" #define TS_META_QUERYMATCH "treesitter_querymatch" +#ifdef __EMSCRIPTEN__ +extern const TSLanguage *nvim_ts_get_parser(const char *lang); +#endif + typedef struct { LuaRef cb; lua_State *lstate; @@ -133,6 +137,13 @@ static int tslua_add_language_from_object(lua_State *L) static const TSLanguage *load_language_from_object(lua_State *L, const char *path, const char *lang_name, const char *symbol) { +#ifdef __EMSCRIPTEN__ + const TSLanguage *static_lang = nvim_ts_get_parser(symbol); + if (static_lang != NULL) { + return static_lang; + } +#endif + uv_lib_t lib; if (uv_dlopen(path, &lib)) { xstrlcpy(IObuff, uv_dlerror(&lib), sizeof(IObuff)); diff --git a/src/static_ts_registry.c b/src/static_ts_registry.c new file mode 100644 index 0000000000..fba96a8895 --- /dev/null +++ b/src/static_ts_registry.c @@ -0,0 +1,41 @@ +#ifdef __EMSCRIPTEN__ +#include +#include + +#include "tree_sitter/api.h" + +extern const TSLanguage *tree_sitter_lua(void); +extern const TSLanguage *tree_sitter_c(void); +extern const TSLanguage *tree_sitter_vim(void); +extern const TSLanguage *tree_sitter_markdown(void); +extern const TSLanguage *tree_sitter_markdown_inline(void); +extern const TSLanguage *tree_sitter_query(void); +extern const TSLanguage *tree_sitter_vimdoc(void); + +typedef const TSLanguage *(*ts_parser_fn)(void); + +typedef struct { + const char *name; + ts_parser_fn fn; +} TsParserEntry; + +static TsParserEntry parsers[] = { + { "lua", tree_sitter_lua }, + { "c", tree_sitter_c }, + { "vim", tree_sitter_vim }, + { "markdown", tree_sitter_markdown }, + { "markdown_inline", tree_sitter_markdown_inline }, + { "query", tree_sitter_query }, + { "vimdoc", tree_sitter_vimdoc }, +}; + +const TSLanguage *nvim_ts_get_parser(const char *lang) +{ + for (size_t i = 0; i < sizeof(parsers) / sizeof(parsers[0]); i++) { + if (strcmp(lang, parsers[i].name) == 0) { + return parsers[i].fn(); + } + } + return NULL; +} +#endif