From 4ece4babadfb1d3d10a06dc051fefe3449347cbe Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 29 Feb 2024 00:00:15 +0100 Subject: [PATCH] Initial public release --- .gitignore | 3 + build.zig | 290 +++ build.zig.version | 1 + build.zig.zon | 42 + help.md | 293 +++ src/Buffer.zig | 1158 ++++++++++ src/Cursor.zig | 220 ++ src/Selection.zig | 61 + src/View.zig | 142 ++ src/color.zig | 62 + src/config.zig | 15 + src/diff.zig | 165 ++ src/file_type.zig | 127 ++ src/file_types.zig | 294 +++ src/location_history.zig | 147 ++ src/log.zig | 158 ++ src/lsp_process.zig | 168 ++ src/main.zig | 319 +++ src/ripgrep.zig | 235 ++ src/syntax.zig | 112 + src/text_manip.zig | 63 + src/tracy_noop.zig | 9 + src/tui/Box.zig | 40 + src/tui/EventHandler.zig | 175 ++ src/tui/MessageFilter.zig | 138 ++ src/tui/Widget.zig | 273 +++ src/tui/WidgetList.zig | 223 ++ src/tui/WidgetStack.zig | 94 + src/tui/command.zig | 194 ++ src/tui/editor.zig | 3290 +++++++++++++++++++++++++++ src/tui/editor_gutter.zig | 327 +++ src/tui/fonts.zig | 185 ++ src/tui/home.zig | 263 +++ src/tui/inputview.zig | 106 + src/tui/inspector_view.zig | 181 ++ src/tui/logview.zig | 132 ++ src/tui/mainview.zig | 374 +++ src/tui/message_box.zig | 44 + src/tui/mode/input/flow.zig | 286 +++ src/tui/mode/input/home.zig | 103 + src/tui/mode/input/vim/insert.zig | 284 +++ src/tui/mode/input/vim/normal.zig | 497 ++++ src/tui/mode/input/vim/visual.zig | 472 ++++ src/tui/mode/mini/find.zig | 231 ++ src/tui/mode/mini/find_in_files.zig | 190 ++ src/tui/mode/mini/goto.zig | 116 + src/tui/mode/mini/move_to_char.zig | 122 + src/tui/mode/mini/open_file.zig | 144 ++ src/tui/scrollbar_v.zig | 171 ++ src/tui/status/filestate.zig | 218 ++ src/tui/status/keystate.zig | 211 ++ src/tui/status/linenumstate.zig | 71 + src/tui/status/minilog.zig | 110 + src/tui/status/modestate.zig | 74 + src/tui/status/modstate.zig | 102 + src/tui/status/selectionstate.zig | 105 + src/tui/status/statusbar.zig | 23 + src/tui/tui.zig | 1021 +++++++++ src/unicode.zig | 39 + test/tests.zig | 7 + test/tests_buffer.zig | 279 +++ test/tests_color.zig | 43 + zig | 59 + 63 files changed, 15101 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.version create mode 100644 build.zig.zon create mode 100644 help.md create mode 100644 src/Buffer.zig create mode 100644 src/Cursor.zig create mode 100644 src/Selection.zig create mode 100644 src/View.zig create mode 100644 src/color.zig create mode 100644 src/config.zig create mode 100644 src/diff.zig create mode 100644 src/file_type.zig create mode 100644 src/file_types.zig create mode 100644 src/location_history.zig create mode 100644 src/log.zig create mode 100644 src/lsp_process.zig create mode 100644 src/main.zig create mode 100644 src/ripgrep.zig create mode 100644 src/syntax.zig create mode 100644 src/text_manip.zig create mode 100644 src/tracy_noop.zig create mode 100644 src/tui/Box.zig create mode 100644 src/tui/EventHandler.zig create mode 100644 src/tui/MessageFilter.zig create mode 100644 src/tui/Widget.zig create mode 100644 src/tui/WidgetList.zig create mode 100644 src/tui/WidgetStack.zig create mode 100644 src/tui/command.zig create mode 100644 src/tui/editor.zig create mode 100644 src/tui/editor_gutter.zig create mode 100644 src/tui/fonts.zig create mode 100644 src/tui/home.zig create mode 100644 src/tui/inputview.zig create mode 100644 src/tui/inspector_view.zig create mode 100644 src/tui/logview.zig create mode 100644 src/tui/mainview.zig create mode 100644 src/tui/message_box.zig create mode 100644 src/tui/mode/input/flow.zig create mode 100644 src/tui/mode/input/home.zig create mode 100644 src/tui/mode/input/vim/insert.zig create mode 100644 src/tui/mode/input/vim/normal.zig create mode 100644 src/tui/mode/input/vim/visual.zig create mode 100644 src/tui/mode/mini/find.zig create mode 100644 src/tui/mode/mini/find_in_files.zig create mode 100644 src/tui/mode/mini/goto.zig create mode 100644 src/tui/mode/mini/move_to_char.zig create mode 100644 src/tui/mode/mini/open_file.zig create mode 100644 src/tui/scrollbar_v.zig create mode 100644 src/tui/status/filestate.zig create mode 100644 src/tui/status/keystate.zig create mode 100644 src/tui/status/linenumstate.zig create mode 100644 src/tui/status/minilog.zig create mode 100644 src/tui/status/modestate.zig create mode 100644 src/tui/status/modstate.zig create mode 100644 src/tui/status/selectionstate.zig create mode 100644 src/tui/status/statusbar.zig create mode 100644 src/tui/tui.zig create mode 100644 src/unicode.zig create mode 100644 test/tests.zig create mode 100644 test/tests_buffer.zig create mode 100644 test/tests_color.zig create mode 100755 zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46ea169 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.cache/ +/zig-out/ +/zig-cache/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..a41799b --- /dev/null +++ b/build.zig @@ -0,0 +1,290 @@ +const std = @import("std"); + +const CrossTarget = std.zig.CrossTarget; + +const cppflags = [_][]const u8{ + "-fcolor-diagnostics", + "-std=c++20", + "-Wall", + "-Wextra", + "-Werror", + "-Wno-unqualified-std-cast-call", + "-Wno-bitwise-instead-of-logical", //for notcurses + "-fno-sanitize=undefined", + "-gen-cdb-fragment-path", + ".cache/cdb", +}; + +pub fn build(b: *std.Build) void { + const enable_tracy_option = b.option(bool, "enable_tracy", "Enable tracy client library (default: no)"); + const optimize_deps_option = b.option(bool, "optimize_deps", "Enable optimization for dependecies (default: yes)"); + const use_llvm_option = b.option(bool, "use_llvm", "Enable llvm backend (default: yes)"); + const use_lld_option = b.option(bool, "use_lld", "Enable lld backend (default: yes)"); + const use_system_notcurses = b.option(bool, "use_system_notcurses", "Build against system notcurses (default: no)") orelse false; + + const tracy_enabled = if (enable_tracy_option) |enabled| enabled else false; + const optimize_deps_enabled = if (optimize_deps_option) |enabled| enabled else true; + + const options = b.addOptions(); + options.addOption(bool, "enable_tracy", tracy_enabled); + options.addOption(bool, "optimize_deps", optimize_deps_enabled); + options.addOption(bool, "use_llvm", use_llvm_option orelse false); + options.addOption(bool, "use_lld", use_lld_option orelse false); + options.addOption(bool, "use_system_notcurses", use_system_notcurses); + + const options_mod = options.createModule(); + + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const dependency_optimize = if (optimize_deps_enabled) .ReleaseFast else optimize; + + const notcurses_dep = b.dependency("notcurses", .{ + .target = target, + .optimize = dependency_optimize, + .use_system_notcurses = use_system_notcurses, + }); + + const clap_dep = b.dependency("clap", .{ + .target = target, + .optimize = dependency_optimize, + }); + + const dizzy_dep = b.dependency("dizzy", .{ + .target = target, + .optimize = dependency_optimize, + }); + + const tracy_dep = if (tracy_enabled) b.dependency("tracy", .{ + .target = target, + .optimize = dependency_optimize, + }) else undefined; + const tracy_mod = if (tracy_enabled) tracy_dep.module("tracy") else b.createModule(.{ + .root_source_file = .{ .path = "src/tracy_noop.zig" }, + }); + + const themes_dep = b.dependency("themes", .{}); + + const thespian_dep = b.dependency("thespian", .{ + .target = target, + .optimize = dependency_optimize, + .enable_tracy = tracy_enabled, + }); + + const thespian_mod = thespian_dep.module("thespian"); + const cbor_mod = thespian_dep.module("cbor"); + const notcurses_mod = notcurses_dep.module("notcurses"); + + const help_mod = b.createModule(.{ + .root_source_file = .{ .path = "help.md" }, + }); + + const config_mod = b.createModule(.{ + .root_source_file = .{ .path = "src/config.zig" }, + .imports = &.{ + .{ .name = "cbor", .module = cbor_mod }, + }, + }); + + const log_mod = b.createModule(.{ + .root_source_file = .{ .path = "src/log.zig" }, + .imports = &.{ + .{ .name = "thespian", .module = thespian_mod }, + }, + }); + + const tree_sitter_dep = b.dependency("tree-sitter", .{ + .target = target, + .optimize = dependency_optimize, + }); + + const color_mod = b.createModule(.{ + .root_source_file = .{ .path = "src/color.zig" }, + }); + + const Buffer_mod = b.createModule(.{ + .root_source_file = .{ .path = "src/Buffer.zig" }, + .imports = &.{ + .{ .name = "notcurses", .module = notcurses_mod }, + .{ .name = "cbor", .module = cbor_mod }, + }, + }); + + const ripgrep_mod = b.createModule(.{ + .root_source_file = .{ .path = "src/ripgrep.zig" }, + .imports = &.{ + .{ .name = "thespian", .module = thespian_mod }, + .{ .name = "cbor", .module = cbor_mod }, + .{ .name = "log", .module = log_mod }, + }, + }); + + const location_history_mod = b.createModule(.{ + .root_source_file = .{ .path = "src/location_history.zig" }, + .imports = &.{ + .{ .name = "thespian", .module = thespian_mod }, + }, + }); + + const syntax_mod = b.createModule(.{ + .root_source_file = .{ .path = "src/syntax.zig" }, + .imports = &.{ + .{ .name = "Buffer", .module = Buffer_mod }, + .{ .name = "tracy", .module = tracy_mod }, + .{ .name = "treez", .module = tree_sitter_dep.module("treez") }, + ts_queryfile(b, tree_sitter_dep, "tree-sitter-agda/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-bash/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-c-sharp/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-c/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-cpp/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-css/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-diff/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-dockerfile/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-git-rebase/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-gitcommit/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-go/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-fish/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-haskell/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-html/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-java/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-javascript/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-jsdoc/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-json/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-lua/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-make/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-markdown/tree-sitter-markdown/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-markdown/tree-sitter-markdown-inline/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-nasm/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-ninja/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-nix/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-ocaml/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-openscad/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-org/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-php/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-python/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-purescript/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-regex/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-ruby/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-rust/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-ssh-config/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-scala/queries/scala/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-scheme/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-toml/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-typescript/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-xml/dtd/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-xml/xml/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-zig/queries/highlights.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-ziggy/tree-sitter-ziggy/queries/highlights.scm"), + + ts_queryfile(b, tree_sitter_dep, "tree-sitter-cpp/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-gitcommit/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-html/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-javascript/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-lua/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-markdown/tree-sitter-markdown-inline/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-markdown/tree-sitter-markdown/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-nasm/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-nix/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-openscad/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-php/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-purescript/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-purescript/vim_queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-rust/queries/injections.scm"), + ts_queryfile(b, tree_sitter_dep, "tree-sitter-zig/queries/injections.scm"), + }, + }); + + const diff_mod = b.createModule(.{ + .root_source_file = .{ .path = "src/diff.zig" }, + .imports = &.{ + .{ .name = "thespian", .module = thespian_mod }, + .{ .name = "Buffer", .module = Buffer_mod }, + .{ .name = "tracy", .module = tracy_mod }, + .{ .name = "dizzy", .module = dizzy_dep.module("dizzy") }, + .{ .name = "log", .module = log_mod }, + .{ .name = "cbor", .module = cbor_mod }, + }, + }); + + const text_manip_mod = b.createModule(.{ + .root_source_file = .{ .path = "src/text_manip.zig" }, + .imports = &.{}, + }); + + const tui_mod = b.createModule(.{ + .root_source_file = .{ .path = "src/tui/tui.zig" }, + .imports = &.{ + .{ .name = "notcurses", .module = notcurses_mod }, + .{ .name = "thespian", .module = thespian_mod }, + .{ .name = "cbor", .module = cbor_mod }, + .{ .name = "config", .module = config_mod }, + .{ .name = "log", .module = log_mod }, + .{ .name = "location_history", .module = location_history_mod }, + .{ .name = "syntax", .module = syntax_mod }, + .{ .name = "text_manip", .module = text_manip_mod }, + .{ .name = "Buffer", .module = Buffer_mod }, + .{ .name = "ripgrep", .module = ripgrep_mod }, + .{ .name = "theme", .module = themes_dep.module("theme") }, + .{ .name = "themes", .module = themes_dep.module("themes") }, + .{ .name = "tracy", .module = tracy_mod }, + .{ .name = "build_options", .module = options_mod }, + .{ .name = "color", .module = color_mod }, + .{ .name = "diff", .module = diff_mod }, + .{ .name = "help.md", .module = help_mod }, + }, + }); + + const exe = b.addExecutable(.{ + .name = "flow", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + if (use_llvm_option) |enabled| exe.use_llvm = enabled; + if (use_lld_option) |enabled| exe.use_lld = enabled; + + exe.root_module.addImport("build_options", options_mod); + exe.root_module.addImport("clap", clap_dep.module("clap")); + exe.root_module.addImport("cbor", cbor_mod); + exe.root_module.addImport("config", config_mod); + exe.root_module.addImport("tui", tui_mod); + exe.root_module.addImport("thespian", thespian_mod); + exe.root_module.addImport("log", log_mod); + exe.root_module.addImport("tracy", tracy_mod); + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const tests = b.addTest(.{ + .root_source_file = .{ .path = "test/tests.zig" }, + .target = target, + .optimize = optimize, + }); + + tests.root_module.addImport("build_options", options_mod); + tests.root_module.addImport("log", log_mod); + tests.root_module.addImport("Buffer", Buffer_mod); + tests.root_module.addImport("color", color_mod); + // b.installArtifact(tests); + + const test_run_cmd = b.addRunArtifact(tests); + + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&test_run_cmd.step); +} + +fn ts_queryfile(b: *std.Build, dep: *std.Build.Dependency, comptime sub_path: []const u8) std.Build.Module.Import { + return .{ + .name = sub_path, + .module = b.createModule(.{ + .root_source_file = dep.path(sub_path), + }), + }; +} diff --git a/build.zig.version b/build.zig.version new file mode 100644 index 0000000..a7b1b48 --- /dev/null +++ b/build.zig.version @@ -0,0 +1 @@ +0.12.0-dev.3059+27f589dea diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..ddf1889 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,42 @@ +.{ + .name = "flow", + .version = "0.0.1", + + .dependencies = .{ + .notcurses = .{ + .url = "https://github.com/neurocyte/notcurses-zig/archive/99dd02c42fc8b276fed0890369c9136316747ab9.tar.gz", + .hash = "1220be39bfc0c45e66f7e99cbddf3836dbe32b1fb36a8b5c616f4b13f97a275d36cf", + }, + .clap = .{ + .url = "https://github.com/Hejsil/zig-clap/archive/9c23bcb5aebe0c2542b4de4472f60959974e2222.tar.gz", + .hash = "12209e829da9d7d0bc089e4e0cbc07bb882f6192cd583277277da34df53cd05b8f2a", + }, + .@"tree-sitter" = .{ + .url = "https://github.com/neurocyte/tree-sitter/releases/download/master-87dba6ef605389dc2e61cf8ef498ea68e816f4b1/source.tar.gz", + .hash = "12208fe4c5317901bf05642d1740bccbe653e989693b5cc4ed5d7a3f7814e213cbc3", + }, + .tracy = .{ + .url = "https://github.com/neurocyte/zig-tracy/archive/d2113e7d778ebe7a063e95b5182ff145343aac38.tar.gz", + .hash = "122005c37f1324dcdd00600f266b64f0a66a73428550015ece51d52ae40a552608d1", + }, + .dizzy = .{ + .url = "https://github.com/SuperAuguste/dizzy/archive/d4aaf67d0f5ef69d0a0287ae472ddfece064d341.tar.gz", + .hash = "1220a7cf5f59b61257993bc5b02991ffc523d103f66842fa8d8ab5c9fdba52799340", + }, + .thespian = .{ + .url = "https://github.com/neurocyte/thespian/archive/522813dae1ef02eb1348a3eda5710b6626a65477.tar.gz", + .hash = "1220d1a80d5419e5a89df908d44d586b99f84c7dbf37e074f1123522b81d7473a700", + }, + .themes = .{ + .url = "https://github.com/neurocyte/flow-themes/releases/download/master-9ee6d7bc28256202aa7b3b20555bf480715c4e5c/flow-themes.tar.gz", + .hash = "1220cd21ee1f3e194f1cca5d52175c20d2c663a53eadaa9057997e72aa828f5d3864", + }, + }, + .paths = .{ + "include", + "src", + "test", + "build.zig", + "build.zig.zon", + }, +} diff --git a/help.md b/help.md new file mode 100644 index 0000000..218e3be --- /dev/null +++ b/help.md @@ -0,0 +1,293 @@ +# Flow Control: a programmer's text editor + +## Searching + +Press Ctrl-f to search this help file. Type a search term and press +Ctrl-n/Ctrl-p or F3/Shift-F3 to jump through the matches. Press Enter +to exit find mode at the current match or Escape to return to your +starting point. + +## Input Modes + +Flow Control supports multiple input modes that may be changed +interactively at runtime. The current input mode (and some other +settings) is persisted in the configuration file automatically. + +- F2 => Cycle major input modes (flow, vim, ...) + +The current input mode Input mode is displayed in the `modestatus` +widget at the left side of the statusbar. + +## Flow mode + +The default input mode, called just flow, is based on common GUI +programming editors. It most closely resembles Visual Studio Code, but +also takes some inspiration from Emacs and others. This mode focuses +on powerful multi cursor support with a find -> select -> modify +cycle style of editing. + +### Navigation Commands + +- Up, Down, Left, Right => + Move the cursor + +- Home, End => + Move to the beginning/end of the line + +- PageUp, PageDown => + Move up/down one screen + +- Ctrl-Left, Ctrl-Right, Alt-b, Alt-f => + Move the cursor word wise + +- Ctrl-Home, Ctrl-End => + Move to the beginning/end of the file + +- Alt-Left, Alt-Right => + Jump to previous/next location in the location history + +- Ctrl-f => + Enter find mode + +- Ctrl-g => + Enter goto line mode + +- Ctrl-t, Ctrl-b => + Enter move to next/previous character mode + +- Ctrl-n, Ctrl-p, F3, Shift-F3, Alt-n, Alt-p => + Goto next/previous match + +- Ctrl-l => + Scroll cursor to center of screen, cycle cursor to + top/bottom of screen + +- MouseLeft => + Clear all cursors and selections and place cursor at mouse pointer + +- MouseWheel => + Scroll + +- Ctrl-MouseWheel => + Fast scroll + +### Selection Commands + +- Shift-Left, Shift-Right => + Add next character to selection + +- Ctrl-Shift-Left, Ctrl-Shift-Right => + Add next word to selection + +- Shift-Up, Shift-Down => + Add next line to selection + +- Ctrl-Shift-Up, Ctrl-Shift-Down => + Add next line to selection and scroll + +- Shift-Home, Shift-End => + Add begging/end of line to selection + +- Ctrl-Shift-Home, Ctrl-Shift-End => + Add begging/end of file to selection + +- Shift-PageUp, Shift-PageDown => + Add next screen to selection + +- Ctrl-a => + Select entire file + +- Ctrl-d => + Select word under cursor, or add cursor at next match + (see Multi Cursor Commands) + +- Ctrl-Space => + Reverse selection direction + +- Double-MouseLeft => + Select word at mouse pointer + +- Triple-MouseLeft => + Select line at mouse pointer + +- Drag-MouseLeft => + Extend selection to mouse pointer + +- MouseRight => + Extend selection to mouse pointer + +### Multi Cursor Commands + +- Ctrl-d => + Add cursor at next match (either find match, or auto match) + +- Alt-Shift-Down, Alt-Shift-Up => + Add cursor on the previous/next line + +- Ctrl-MouseLeft => + Add cursor at mouse click + +- Ctrl-u => + Remove last added cursor (pop) + +- Ctrl-k -> Ctrl-d => + Move primary cursor to next match (skip) + +- Escape => + Remove all cursors and selections + +### Editing Commands + +- Ctrl-Enter, Ctrl-Shift-Enter => + Insert new line after/before current line + +- Ctrl-Backspace, Ctrl-Delete => + Delete word left/right + +- Ctrl-k -> Ctrl-u => + Delete to beginning of line + +- Ctrl-k -> Ctrl-k => + Delete to end of line + +- Ctrl-Shift-d, Alt-Shift-d => + Duplicate current line or selection down/up + +- Alt-Down, Alt-Up => + Pull current line or selection down/up + +- Ctrl-c => + Copy selected text + +- Ctrl-x => + Cut selected text, or line if there is no selection + +- Ctrl-v => + Paste previously copied/cut text + +- Ctrl-z => + Undo last change + +- Ctrl-Shift-z, Ctrl-y => + Redo last undone change + +- Tab, Shift-Tab => + Indent/Unindent line + +- Ctrl-/ => + Toggle comment prefix in line + +- Alt-s => + Sort file or selection + +- Alt-Shift-f => + Reformat file or selection + +### File Commands + +- Ctrl-s => + Save file + +- Ctrl-o => + Open file + +- Ctrl-q => + Exit + +- Ctrl-q => + Exit + +- Ctrl-q => + Exit + +- Ctrl-Shift-q => + Force exit without saving + +- Ctrl-Shift-r => + Restart Flow Control/reload file + +### Configuration Commands + +- F9 => + Select previous theme + +- F10 => + Select next theme + +- Ctrl-F10 => + Toggle visible whitespace mode + +### Debugging Commands + +- F5 => + Toggle inspector view + +- F6 => + Dump buffer AST for current line to log view + +- F7 => + Dump current line to log view + +- F11, Ctrl-J, Ctrl-Shift-l => + Toggle log view + +- F12, Ctrl-Shift-i => + Toggle input view + +- Ctrl-Shift-/ => + Dump current widget tree to log view + +## Vim mode + +The vim mode, called NOR or INS, follows the basic modal editing +style of vim. Normal and insert mode basics follow vim closely, +but more advanced vim functions (e.g. macros and registers) are +not supported yet. Keybindings from flow mode that do not +conflict with common vim keybindings also work in vim mode. + +The vim command prompt (:) is not available yet. To save/load/quit +you will have to use the flow mode keybindings. + +(work in progress) + +## Configuration + +Configuration is stored in the standard location +`${XDG_CONFIG_HOME}/flow/config.json`. This is usually +`~/.config/flow/config.json`. + +The default configuration will be written the first time +Flow Control is started and looks similar to this: +``` +{ + "frame_rate": 60, + "theme": "default", + "input_mode": "flow", + "modestate_show": true, + "selectionstate_show": true, + "modstate_show": false, + "keystate_show": false, + "gutter_line_numbers": true, + "gutter_line_numbers_relative": false, + "enable_terminal_cursor": false, + "highlight_current_line": true, + "highlight_current_line_gutter": true, + "show_whitespace": false, + "animation_min_lag": 0, + "animation_max_lag": 150 +} +``` + +Most of these options are fairly self explanitory. + +`theme`, `input_mode` and `show_whitespace` are automatically +persisted when changed interactively with keybindings. + +`frame_rate` can be tuned to control the maximum number +of frames rendered. + +`*state_show` toggle various parts of the statusbar. + +`animation_max_lag` controls the maximum amount of time allowed +for rendering scrolling animations. Set to 0 to disable scrolling +animation altogether. diff --git a/src/Buffer.zig b/src/Buffer.zig new file mode 100644 index 0000000..15031bb --- /dev/null +++ b/src/Buffer.zig @@ -0,0 +1,1158 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const nc = @import("notcurses"); +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const cwd = std.fs.cwd; + +const Self = @This(); + +const default_leaf_capacity = 64; +const max_imbalance = 7; +pub const Root = *const Node; +pub const unicode = @import("unicode.zig"); + +pub const Cursor = @import("Cursor.zig"); +pub const View = @import("View.zig"); +pub const Selection = @import("Selection.zig"); +pub const MetaWriter = std.ArrayList(u8).Writer; + +arena: std.heap.ArenaAllocator, +a: Allocator, +external_a: Allocator, +root: Root, +file_path: []const u8 = "", +last_save: ?Root = null, +file_exists: bool = true, + +undo_history: ?*UndoNode = null, +redo_history: ?*UndoNode = null, +curr_history: ?*UndoNode = null, + +const UndoNode = struct { + root: Root, + next: ?*UndoNode = null, + branches: ?*UndoBranch = null, + meta: []const u8, +}; + +const UndoBranch = struct { + redo: *UndoNode, + next: ?*UndoBranch, +}; + +pub const WalkerMut = struct { + keep_walking: bool = false, + found: bool = false, + replace: ?Root = null, + err: ?anyerror = null, + + pub const keep_walking = WalkerMut{ .keep_walking = true }; + pub const stop = WalkerMut{ .keep_walking = false }; + pub const found = WalkerMut{ .found = true }; + + const F = *const fn (ctx: *anyopaque, leaf: *const Leaf) WalkerMut; +}; + +pub const Walker = struct { + keep_walking: bool = false, + found: bool = false, + err: ?anyerror = null, + + pub const keep_walking = Walker{ .keep_walking = true }; + pub const stop = Walker{ .keep_walking = false }; + pub const found = Walker{ .found = true }; + + const F = *const fn (ctx: *anyopaque, leaf: *const Leaf) Walker; +}; + +pub const Weights = struct { + bols: u32 = 0, + eols: u32 = 0, + len: u32 = 0, + depth: u32 = 1, + + fn add(self: *Weights, other: Weights) void { + self.bols += other.bols; + self.eols += other.eols; + self.len += other.len; + self.depth = @max(self.depth, other.depth); + } +}; + +pub const Branch = struct { + left: *const Node, + right: *const Node, + weights: Weights, + weights_sum: Weights, + + const walker = *const fn (ctx: *anyopaque, branch: *const Branch) WalkerMut; + + fn is_balanced(self: *const Branch) bool { + const left: isize = @intCast(self.left.weights_sum().depth); + const right: isize = @intCast(self.right.weights_sum().depth); + return @abs(left - right) < max_imbalance; + } + + fn merge_results_const(_: *const Branch, left: Walker, right: Walker) Walker { + var result = Walker{}; + result.err = if (left.err) |_| left.err else right.err; + result.keep_walking = left.keep_walking and right.keep_walking; + result.found = left.found or right.found; + return result; + } + + fn merge_results(self: *const Branch, a: Allocator, left: WalkerMut, right: WalkerMut) WalkerMut { + var result = WalkerMut{}; + result.err = if (left.err) |_| left.err else right.err; + if (left.replace != null or right.replace != null) { + const new_left = if (left.replace) |p| p else self.left; + const new_right = if (right.replace) |p| p else self.right; + result.replace = if (new_left.is_empty()) + new_right + else if (new_right.is_empty()) + new_left + else + Node.new(a, new_left, new_right) catch |e| return .{ .err = e }; + } + result.keep_walking = left.keep_walking and right.keep_walking; + result.found = left.found or right.found; + return result; + } +}; + +pub const Leaf = struct { + buf: []const u8, + bol: bool = true, + eol: bool = true, + + fn new(a: Allocator, piece: []const u8, bol: bool, eol: bool) !*const Node { + if (piece.len == 0) + return if (!bol and !eol) &empty_leaf else if (bol and !eol) &empty_bol_leaf else if (!bol and eol) &empty_eol_leaf else &empty_line_leaf; + const node = try a.create(Node); + node.* = .{ .leaf = .{ .buf = piece, .bol = bol, .eol = eol } }; + return node; + } + + inline fn weights(self: *const Leaf) Weights { + var len = self.buf.len; + if (self.eol) + len += 1; + return .{ .bols = if (self.bol) 1 else 0, .eols = if (self.eol) 1 else 0, .len = @intCast(len) }; + } + + inline fn is_empty(self: *const Leaf) bool { + return self.buf.len == 0 and !self.bol and !self.eol; + } + + fn pos_to_width(self: *const Leaf, pos: *usize, abs_col_: usize) usize { + var col: usize = 0; + var abs_col = abs_col_; + var cols: c_int = 0; + var buf = self.buf; + while (buf.len > 0 and pos.* > 0) { + if (buf[0] == '\t') { + cols = @intCast(8 - (abs_col % 8)); + buf = buf[1..]; + pos.* -= 1; + } else { + const bytes = egc_len(buf, &cols, abs_col); + buf = buf[bytes..]; + pos.* -= bytes; + } + col += @intCast(cols); + abs_col += @intCast(cols); + } + return col; + } + + fn width(self: *const Leaf, abs_col: usize) usize { + var pos: usize = std.math.maxInt(usize); + return self.pos_to_width(&pos, abs_col); + } + + inline fn width_to_pos(self: *const Leaf, col_: usize, abs_col_: usize) !usize { + var abs_col = abs_col_; + var col = col_; + var cols: c_int = 0; + var buf = self.buf; + return while (buf.len > 0) { + if (col == 0) + break @intFromPtr(buf.ptr) - @intFromPtr(self.buf.ptr); + const bytes = egc_len(buf, &cols, abs_col); + buf = buf[bytes..]; + if (col < cols) + break @intFromPtr(buf.ptr) - @intFromPtr(self.buf.ptr); + col -= @intCast(cols); + abs_col += @intCast(cols); + } else error.BufferUnderrun; + } + + inline fn dump(self: *const Leaf, l: *ArrayList(u8), abs_col: usize) !void { + var buf: [16]u8 = undefined; + const wcwidth = try std.fmt.bufPrint(&buf, "{d}", .{self.width(abs_col)}); + if (self.bol) + try l.appendSlice("BOL "); + try l.appendSlice(wcwidth); + try l.append('"'); + try debug_render_chunk(self.buf, l); + try l.appendSlice("\" "); + if (self.eol) + try l.appendSlice("EOL "); + } + + fn debug_render_chunk(chunk: []const u8, l: *ArrayList(u8)) !void { + var cols: c_int = 0; + var buf = chunk; + while (buf.len > 0) { + switch (buf[0]) { + '\x00'...(' ' - 1) => { + const control = unicode.control_code_to_unicode(buf[0]); + try l.appendSlice(control); + buf = buf[1..]; + }, + else => { + const bytes = egc_len(buf, &cols, 0); + var buf_: [4096]u8 = undefined; + try l.appendSlice(try std.fmt.bufPrint(&buf_, "{s}", .{std.fmt.fmtSliceEscapeLower(buf[0..bytes])})); + buf = buf[bytes..]; + }, + } + } + } +}; + +const empty_leaf: Node = .{ .leaf = .{ .buf = "", .bol = false, .eol = false } }; +const empty_bol_leaf: Node = .{ .leaf = .{ .buf = "", .bol = true, .eol = false } }; +const empty_eol_leaf: Node = .{ .leaf = .{ .buf = "", .bol = false, .eol = true } }; +const empty_line_leaf: Node = .{ .leaf = .{ .buf = "", .bol = true, .eol = true } }; + +const Node = union(enum) { + node: Branch, + leaf: Leaf, + + const walker = *const fn (ctx: *anyopaque, node: *const Node) WalkerMut; + + fn new(a: Allocator, l: *const Node, r: *const Node) !*const Node { + const node = try a.create(Node); + const l_weights_sum = l.weights_sum(); + var weights_sum_ = Weights{}; + weights_sum_.add(l_weights_sum); + weights_sum_.add(r.weights_sum()); + weights_sum_.depth += 1; + node.* = .{ .node = .{ .left = l, .right = r, .weights = l_weights_sum, .weights_sum = weights_sum_ } }; + return node; + } + + fn weights_sum(self: *const Node) Weights { + return switch (self.*) { + .node => |*n| n.weights_sum, + .leaf => |*l| l.weights(), + }; + } + + fn depth(self: *const Node) usize { + return self.weights_sum().depth; + } + + pub fn lines(self: *const Node) usize { + return self.weights_sum().bols; + } + + pub fn length(self: *const Node) usize { + return self.weights_sum().len; + } + + pub fn is_balanced(self: *const Node) bool { + return switch (self.*) { + .node => |*n| n.is_balanced(), + .leaf => |_| true, + }; + } + + pub fn rebalance(self: *const Node, a: Allocator, tmp_a: Allocator) !Root { + return if (self.is_balanced()) self else bal: { + const leaves = try self.collect_leaves(tmp_a); + defer tmp_a.free(leaves); + break :bal self.merge(leaves, a); + }; + } + + fn merge(self: *const Node, leaves: []*const Node, a: Allocator) !Root { + const len = leaves.len; + if (len == 1) { + return leaves[0]; + } + if (len == 2) { + return Node.new(a, leaves[0], leaves[1]); + } + const mid = len / 2; + return Node.new(a, try self.merge(leaves[0..mid], a), try self.merge(leaves[mid..], a)); + } + + fn is_empty(self: *const Node) bool { + return switch (self.*) { + .node => |*n| n.left.is_empty() and n.right.is_empty(), + .leaf => |*l| if (self == &empty_leaf) true else l.is_empty(), + }; + } + + fn collect(self: *const Node, l: *ArrayList(*const Node)) !void { + switch (self.*) { + .node => |*node| { + try node.left.collect(l); + try node.right.collect(l); + }, + .leaf => (try l.addOne()).* = self, + } + } + + fn collect_leaves(self: *const Node, a: Allocator) ![]*const Node { + var leaves = ArrayList(*const Node).init(a); + try leaves.ensureTotalCapacity(self.lines()); + try self.collect(&leaves); + return leaves.toOwnedSlice(); + } + + fn walk_const(self: *const Node, f: Walker.F, ctx: *anyopaque) Walker { + switch (self.*) { + .node => |*node| { + const left = node.left.walk_const(f, ctx); + if (!left.keep_walking) { + var result = Walker{}; + result.err = left.err; + result.found = left.found; + return result; + } + const right = node.right.walk_const(f, ctx); + return node.merge_results_const(left, right); + }, + .leaf => |*l| return f(ctx, l), + } + } + + fn walk(self: *const Node, a: Allocator, f: WalkerMut.F, ctx: *anyopaque) WalkerMut { + switch (self.*) { + .node => |*node| { + const left = node.left.walk(a, f, ctx); + if (!left.keep_walking) { + var result = WalkerMut{}; + result.err = left.err; + result.found = left.found; + if (left.replace) |p| { + result.replace = Node.new(a, p, node.right) catch |e| return .{ .err = e }; + } + return result; + } + const right = node.right.walk(a, f, ctx); + return node.merge_results(a, left, right); + }, + .leaf => |*l| return f(ctx, l), + } + } + + fn walk_from_line_begin_const_internal(self: *const Node, line: usize, f: Walker.F, ctx: *anyopaque) Walker { + switch (self.*) { + .node => |*node| { + const left_bols = node.weights.bols; + if (line >= left_bols) + return node.right.walk_from_line_begin_const_internal(line - left_bols, f, ctx); + const left_result = node.left.walk_from_line_begin_const_internal(line, f, ctx); + const right_result = if (left_result.found and left_result.keep_walking) node.right.walk_const(f, ctx) else Walker{}; + return node.merge_results_const(left_result, right_result); + }, + .leaf => |*l| { + if (line == 0) { + var result = f(ctx, l); + if (result.err) |_| return result; + result.found = true; + return result; + } + return Walker.keep_walking; + }, + } + } + + pub fn walk_from_line_begin_const(self: *const Node, line: usize, f: Walker.F, ctx: *anyopaque) !bool { + const result = self.walk_from_line_begin_const_internal(line, f, ctx); + if (result.err) |e| return e; + return result.found; + } + + fn walk_from_line_begin_internal(self: *const Node, a: Allocator, line: usize, f: WalkerMut.F, ctx: *anyopaque) WalkerMut { + switch (self.*) { + .node => |*node| { + const left_bols = node.weights.bols; + if (line >= left_bols) { + const right_result = node.right.walk_from_line_begin_internal(a, line - left_bols, f, ctx); + if (right_result.replace) |p| { + var result = WalkerMut{}; + result.err = right_result.err; + result.found = right_result.found; + result.keep_walking = right_result.keep_walking; + result.replace = if (p.is_empty()) + node.left + else + Node.new(a, node.left, p) catch |e| return .{ .err = e }; + return result; + } else { + return right_result; + } + } + const left_result = node.left.walk_from_line_begin_internal(a, line, f, ctx); + const right_result = if (left_result.found and left_result.keep_walking) node.right.walk(a, f, ctx) else WalkerMut{}; + return node.merge_results(a, left_result, right_result); + }, + .leaf => |*l| { + if (line == 0) { + var result = f(ctx, l); + if (result.err) |_| { + result.replace = null; + return result; + } + result.found = true; + return result; + } + return WalkerMut.keep_walking; + }, + } + } + + pub fn walk_from_line_begin(self: *const Node, a: Allocator, line: usize, f: WalkerMut.F, ctx: *anyopaque) !struct { bool, ?Root } { + const result = self.walk_from_line_begin_internal(a, line, f, ctx); + if (result.err) |e| return e; + return .{ result.found, result.replace }; + } + + fn find_line_node(self: *const Node, line: usize) ?*const Node { + switch (self.*) { + .node => |*node| { + if (node.weights_sum.bols == 1) + return self; + const left_bols = node.weights.bols; + if (line >= left_bols) + return node.right.find_line_node(line - left_bols); + return node.left.find_line_node(line); + }, + .leaf => |*l| { + return if (l.bol) self else null; + }, + } + } + + fn debug_render_tree(self: *const Node, l: *ArrayList(u8), d: usize) void { + switch (self.*) { + .node => |*node| { + l.append('(') catch {}; + node.left.debug_render_tree(l, d + 1); + l.append(' ') catch {}; + node.right.debug_render_tree(l, d + 1); + l.append(')') catch {}; + }, + .leaf => |*leaf| { + l.append('"') catch {}; + l.appendSlice(leaf.buf) catch {}; + if (leaf.eol) + l.appendSlice("\\n") catch {}; + l.append('"') catch {}; + }, + } + } + + const EgcF = *const fn (ctx: *anyopaque, egc: []const u8, wcwidth: usize) Walker; + + pub fn walk_egc_forward(self: *const Node, line: usize, walker_f: EgcF, walker_ctx: *anyopaque) !void { + const Ctx = struct { + walker_f: EgcF, + walker_ctx: @TypeOf(walker_ctx), + abs_col: usize = 0, + fn walker(ctx_: *anyopaque, leaf: *const Self.Leaf) Walker { + const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); + var buf: []const u8 = leaf.buf; + while (buf.len > 0) { + var cols: c_int = undefined; + const bytes = egc_len(buf, &cols, ctx.abs_col); + const ret = ctx.walker_f(ctx.walker_ctx, buf[0..bytes], @intCast(cols)); + if (ret.err) |e| return .{ .err = e }; + buf = buf[bytes..]; + ctx.abs_col += @intCast(cols); + if (!ret.keep_walking) return Walker.stop; + } + if (leaf.eol) { + const ret = ctx.walker_f(ctx.walker_ctx, "\n", 1); + if (ret.err) |e| return .{ .err = e }; + if (!ret.keep_walking) return Walker.stop; + ctx.abs_col = 0; + } + return Walker.keep_walking; + } + }; + var ctx: Ctx = .{ .walker_f = walker_f, .walker_ctx = walker_ctx }; + const found = try self.walk_from_line_begin_const(line, Ctx.walker, &ctx); + if (!found) return error.NotFound; + } + + pub fn ecg_at(self: *const Node, line: usize, col: usize) error{NotFound}!struct { []const u8, usize, usize } { + const ctx_ = struct { + col: usize, + at: ?[]const u8 = null, + wcwidth: usize = 0, + fn walker(ctx_: *anyopaque, egc: []const u8, wcwidth: usize) Walker { + const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); + ctx.at = egc; + ctx.wcwidth = wcwidth; + if (ctx.col == 0 or egc[0] == '\n' or ctx.col < wcwidth) + return Walker.stop; + ctx.col -= wcwidth; + return Walker.keep_walking; + } + }; + var ctx: ctx_ = .{ .col = col }; + self.walk_egc_forward(line, ctx_.walker, &ctx) catch return .{ "?", 1, 0 }; + return if (ctx.at) |at| .{ at, ctx.wcwidth, ctx.col } else error.NotFound; + } + + pub fn test_at(self: *const Node, pred: *const fn (c: []const u8) bool, line: usize, col: usize) bool { + const ecg, _, _ = self.ecg_at(line, col) catch return false; + return pred(ecg); + } + + pub fn get_line_width_map(self: *const Node, line: usize, map: *ArrayList(u16)) error{ Stop, NoSpaceLeft }!void { + const Ctx = struct { + map: *ArrayList(u16), + wcwidth: usize = 0, + fn walker(ctx_: *anyopaque, egc: []const u8, wcwidth: usize) Walker { + const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); + var n = egc.len; + while (n > 0) : (n -= 1) { + const p = ctx.map.addOne() catch |e| return .{ .err = e }; + p.* = @intCast(ctx.wcwidth); + } + ctx.wcwidth += wcwidth; + return if (egc[0] == '\n') Walker.stop else Walker.keep_walking; + } + }; + var ctx: Ctx = .{ .map = map }; + self.walk_egc_forward(line, Ctx.walker, &ctx) catch |e| return switch (e) { + error.NoSpaceLeft => error.NoSpaceLeft, + else => error.Stop, + }; + } + + pub fn get_range(self: *const Node, sel: Selection, copy_buf: ?[]u8, size: ?*usize, wcwidth_: ?*usize) error{ Stop, NoSpaceLeft }!?[]u8 { + const Ctx = struct { + col: usize = 0, + sel: Selection, + out: ?[]u8, + bytes: usize = 0, + wcwidth: usize = 0, + fn walker(ctx_: *anyopaque, egc: []const u8, wcwidth: usize) Walker { + const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); + if (ctx.col < ctx.sel.begin.col) { + ctx.col += wcwidth; + return Walker.keep_walking; + } + if (ctx.out) |out| { + if (egc.len > out.len) + return .{ .err = error.NoSpaceLeft }; + @memcpy(out[0..egc.len], egc); + ctx.out = out[egc.len..]; + } + ctx.bytes += egc.len; + ctx.wcwidth += wcwidth; + if (egc[0] == '\n') { + ctx.col = 0; + ctx.sel.begin.col = 0; + ctx.sel.begin.row += 1; + } else { + ctx.col += wcwidth; + ctx.sel.begin.col += wcwidth; + } + return if (ctx.sel.begin.eql(ctx.sel.end) or ctx.sel.begin.right_of(ctx.sel.end)) + Walker.stop + else + Walker.keep_walking; + } + }; + + var ctx: Ctx = .{ .sel = sel, .out = copy_buf }; + ctx.sel.normalize(); + if (sel.begin.eql(sel.end)) + return error.Stop; + self.walk_egc_forward(sel.begin.row, Ctx.walker, &ctx) catch |e| return switch (e) { + error.NoSpaceLeft => error.NoSpaceLeft, + else => error.Stop, + }; + if (size) |p| p.* = ctx.bytes; + if (wcwidth_) |p| p.* = ctx.wcwidth; + return if (copy_buf) |buf_| buf_[0..ctx.bytes] else null; + } + + pub fn delete_range(self: *const Node, sel: Selection, a: Allocator, size: ?*usize) error{Stop}!Root { + var wcwidth: usize = 0; + _ = self.get_range(sel, null, size, &wcwidth) catch return error.Stop; + return self.del_chars(sel.begin.row, sel.begin.col, wcwidth, a) catch return error.Stop; + } + + pub fn del_chars(self: *const Node, line: usize, col: usize, count: usize, a: Allocator) !Root { + const Ctx = struct { + a: Allocator, + col: usize, + abs_col: usize = 0, + count: usize, + delete_next_bol: bool = false, + fn walker(Ctx: *anyopaque, leaf: *const Leaf) WalkerMut { + const ctx = @as(*@This(), @ptrCast(@alignCast(Ctx))); + var result = WalkerMut.keep_walking; + if (ctx.delete_next_bol and ctx.count == 0) { + result.replace = Leaf.new(ctx.a, leaf.buf, false, leaf.eol) catch |e| return .{ .err = e }; + result.keep_walking = false; + ctx.delete_next_bol = false; + return result; + } + const leaf_wcwidth = leaf.width(ctx.abs_col); + const leaf_bol = leaf.bol and !ctx.delete_next_bol; + ctx.delete_next_bol = false; + const base_col = ctx.abs_col; + ctx.abs_col += leaf_wcwidth; + if (ctx.col > leaf_wcwidth) { + // next node + ctx.col -= leaf_wcwidth; + if (leaf.eol) + ctx.col -= 1; + } else { + // this node + if (ctx.col == 0) { + if (ctx.count > leaf_wcwidth) { + ctx.count -= leaf_wcwidth; + result.replace = Leaf.new(ctx.a, "", leaf_bol, false) catch |e| return .{ .err = e }; + if (leaf.eol) { + ctx.count -= 1; + ctx.delete_next_bol = true; + } + } else if (ctx.count == leaf_wcwidth) { + result.replace = Leaf.new(ctx.a, "", leaf_bol, leaf.eol) catch |e| return .{ .err = e }; + ctx.count = 0; + } else { + const pos = leaf.width_to_pos(ctx.count, base_col) catch |e| return .{ .err = e }; + result.replace = Leaf.new(ctx.a, leaf.buf[pos..], leaf_bol, leaf.eol) catch |e| return .{ .err = e }; + ctx.count = 0; + } + } else if (ctx.col == leaf_wcwidth) { + if (leaf.eol) { + ctx.count -= 1; + result.replace = Leaf.new(ctx.a, leaf.buf, leaf_bol, false) catch |e| return .{ .err = e }; + ctx.delete_next_bol = true; + } + ctx.col -= leaf_wcwidth; + } else { + if (ctx.col + ctx.count >= leaf_wcwidth) { + ctx.count -= leaf_wcwidth - ctx.col; + const pos = leaf.width_to_pos(ctx.col, base_col) catch |e| return .{ .err = e }; + const leaf_eol = if (leaf.eol and ctx.count > 0) leaf_eol: { + ctx.count -= 1; + ctx.delete_next_bol = true; + break :leaf_eol false; + } else leaf.eol; + result.replace = Leaf.new(ctx.a, leaf.buf[0..pos], leaf_bol, leaf_eol) catch |e| return .{ .err = e }; + ctx.col = 0; + } else { + const pos = leaf.width_to_pos(ctx.col, base_col) catch |e| return .{ .err = e }; + const pos_end = leaf.width_to_pos(ctx.col + ctx.count, base_col) catch |e| return .{ .err = e }; + const left = Leaf.new(ctx.a, leaf.buf[0..pos], leaf_bol, false) catch |e| return .{ .err = e }; + const right = Leaf.new(ctx.a, leaf.buf[pos_end..], false, leaf.eol) catch |e| return .{ .err = e }; + result.replace = Node.new(ctx.a, left, right) catch |e| return .{ .err = e }; + ctx.count = 0; + } + } + if (ctx.count == 0 and !ctx.delete_next_bol) + result.keep_walking = false; + } + return result; + } + }; + var ctx: Ctx = .{ .a = a, .col = col, .count = count }; + const found, const root = try self.walk_from_line_begin(a, line, Ctx.walker, &ctx); + return if (found) (if (root) |r| r else error.Stop) else error.NotFound; + } + + fn merge_in_place(leaves: []const Node, a: Allocator) !Root { + const len = leaves.len; + if (len == 1) { + return &leaves[0]; + } + if (len == 2) { + return Node.new(a, &leaves[0], &leaves[1]); + } + const mid = len / 2; + return Node.new(a, try merge_in_place(leaves[0..mid], a), try merge_in_place(leaves[mid..], a)); + } + + pub fn get_line(self: *const Node, line: usize, result: *ArrayList(u8)) !void { + const Ctx = struct { + line: *ArrayList(u8), + fn walker(ctx_: *anyopaque, leaf: *const Leaf) Walker { + const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); + ctx.line.appendSlice(leaf.buf) catch |e| return .{ .err = e }; + return if (!leaf.eol) Walker.keep_walking else Walker.stop; + } + }; + var ctx: Ctx = .{ .line = result }; + const found = self.walk_from_line_begin_const(line, Ctx.walker, &ctx) catch false; + return if (!found) error.NotFound; + } + + pub fn line_width(self: *const Node, line: usize) !usize { + const do = struct { + result: usize = 0, + fn walker(ctx: *anyopaque, leaf: *const Leaf) Walker { + const do = @as(*@This(), @ptrCast(@alignCast(ctx))); + do.result += leaf.width(do.result); + return if (!leaf.eol) Walker.keep_walking else Walker.stop; + } + }; + var ctx: do = .{}; + const found = self.walk_from_line_begin_const(line, do.walker, &ctx) catch true; + return if (found) ctx.result else error.NotFound; + } + + pub fn pos_to_width(self: *const Node, line: usize, pos: usize) !usize { + const do = struct { + result: usize = 0, + pos: usize, + fn walker(ctx: *anyopaque, leaf: *const Leaf) Walker { + const do = @as(*@This(), @ptrCast(@alignCast(ctx))); + do.result += leaf.pos_to_width(&do.pos, do.result); + return if (!(leaf.eol or do.pos == 0)) Walker.keep_walking else Walker.stop; + } + }; + var ctx: do = .{ .pos = pos }; + const found = self.walk_from_line_begin_const(line, do.walker, &ctx) catch true; + return if (found) ctx.result else error.NotFound; + } + + pub fn insert_chars(self_: *const Node, line_: usize, col_: usize, s: []const u8, a: Allocator) !struct { usize, usize, Root } { + var self = self_; + const Ctx = struct { + a: Allocator, + col: usize, + abs_col: usize = 0, + s: []const u8, + eol: bool, + + fn walker(ctx: *anyopaque, leaf: *const Leaf) WalkerMut { + const Ctx = @as(*@This(), @ptrCast(@alignCast(ctx))); + const leaf_wcwidth = leaf.width(Ctx.abs_col); + const base_col = Ctx.abs_col; + Ctx.abs_col += leaf_wcwidth; + + if (Ctx.col == 0) { + const left = Leaf.new(Ctx.a, Ctx.s, leaf.bol, Ctx.eol) catch |e| return .{ .err = e }; + const right = Leaf.new(Ctx.a, leaf.buf, Ctx.eol, leaf.eol) catch |e| return .{ .err = e }; + return .{ .replace = Node.new(Ctx.a, left, right) catch |e| return .{ .err = e } }; + } + + if (leaf_wcwidth == Ctx.col) { + if (leaf.eol and Ctx.eol and Ctx.s.len == 0) { + const left = Leaf.new(Ctx.a, leaf.buf, leaf.bol, true) catch |e| return .{ .err = e }; + const right = Leaf.new(Ctx.a, Ctx.s, true, true) catch |e| return .{ .err = e }; + return .{ .replace = Node.new(Ctx.a, left, right) catch |e| return .{ .err = e } }; + } + const left = Leaf.new(Ctx.a, leaf.buf, leaf.bol, false) catch |e| return .{ .err = e }; + if (Ctx.eol) { + const middle = Leaf.new(Ctx.a, Ctx.s, false, Ctx.eol) catch |e| return .{ .err = e }; + const right = Leaf.new(Ctx.a, "", Ctx.eol, leaf.eol) catch |e| return .{ .err = e }; + return .{ .replace = Node.new( + Ctx.a, + left, + Node.new(Ctx.a, middle, right) catch |e| return .{ .err = e }, + ) catch |e| return .{ .err = e } }; + } else { + const right = Leaf.new(Ctx.a, Ctx.s, false, leaf.eol) catch |e| return .{ .err = e }; + return .{ .replace = Node.new(Ctx.a, left, right) catch |e| return .{ .err = e } }; + } + } + + if (leaf_wcwidth > Ctx.col) { + const pos = leaf.width_to_pos(Ctx.col, base_col) catch |e| return .{ .err = e }; + if (Ctx.eol and Ctx.s.len == 0) { + const left = Leaf.new(Ctx.a, leaf.buf[0..pos], leaf.bol, Ctx.eol) catch |e| return .{ .err = e }; + const right = Leaf.new(Ctx.a, leaf.buf[pos..], Ctx.eol, leaf.eol) catch |e| return .{ .err = e }; + return .{ .replace = Node.new(Ctx.a, left, right) catch |e| return .{ .err = e } }; + } + const left = Leaf.new(Ctx.a, leaf.buf[0..pos], leaf.bol, false) catch |e| return .{ .err = e }; + const middle = Leaf.new(Ctx.a, Ctx.s, false, Ctx.eol) catch |e| return .{ .err = e }; + const right = Leaf.new(Ctx.a, leaf.buf[pos..], Ctx.eol, leaf.eol) catch |e| return .{ .err = e }; + return .{ .replace = Node.new( + Ctx.a, + left, + Node.new(Ctx.a, middle, right) catch |e| return .{ .err = e }, + ) catch |e| return .{ .err = e } }; + } + + Ctx.col -= leaf_wcwidth; + return if (leaf.eol) WalkerMut.stop else WalkerMut.keep_walking; + } + }; + if (s.len == 0) return error.Stop; + var rest = try a.dupe(u8, s); + var chunk = rest; + var line = line_; + var col = col_; + var need_eol = false; + while (rest.len > 0) { + if (std.mem.indexOfScalar(u8, rest, '\n')) |eol| { + chunk = rest[0..eol]; + rest = rest[eol + 1 ..]; + need_eol = true; + } else { + chunk = rest; + rest = &[_]u8{}; + need_eol = false; + } + var ctx: Ctx = .{ .a = a, .col = col, .s = chunk, .eol = need_eol }; + const found, const replace = try self.walk_from_line_begin(a, line, Ctx.walker, &ctx); + if (!found) return error.NotFound; + if (replace) |root| self = root; + if (need_eol) { + line += 1; + col = 0; + } else { + col += egc_chunk_width(chunk, col); + } + } + return .{ line, col, self }; + } + + pub fn store(self: *const Node, writer: anytype) !void { + switch (self.*) { + .node => |*node| { + try node.left.store(writer); + try node.right.store(writer); + }, + .leaf => |*leaf| { + _ = try writer.write(leaf.buf); + if (leaf.eol) + _ = try writer.write("\n"); + }, + } + } + + pub const FindAllCallback = fn (data: *anyopaque, begin_row: usize, begin_col: usize, end_row: usize, end_col: usize) error{Stop}!void; + pub fn find_all_ranges(self: *const Node, pattern: []const u8, data: *anyopaque, callback: *const FindAllCallback, a: Allocator) !void { + const Ctx = struct { + pattern: []const u8, + data: *anyopaque, + callback: *const FindAllCallback, + line: usize = 0, + pos: usize = 0, + buf: []u8, + rest: []u8 = "", + const Ctx = @This(); + const Writer = std.io.Writer(*Ctx, error{Stop}, write); + fn write(ctx: *Ctx, bytes: []const u8) error{Stop}!usize { + var input = bytes; + while (true) { + const input_consume_size = @min(ctx.buf.len - ctx.rest.len, input.len); + @memcpy(ctx.buf[ctx.rest.len .. ctx.rest.len + input_consume_size], input[0..input_consume_size]); + ctx.rest = ctx.buf[0 .. ctx.rest.len + input_consume_size]; + input = input[input_consume_size..]; + + if (ctx.rest.len < ctx.pattern.len) + return bytes.len - input.len; + + var i: usize = 0; + const end = ctx.rest.len - ctx.pattern.len; + + while (i <= end) { + if (std.mem.eql(u8, ctx.rest[i .. i + ctx.pattern.len], ctx.pattern)) { + const begin_row = ctx.line + 1; + const begin_pos = ctx.pos; + ctx.skip(&i, ctx.pattern.len); + const end_row = ctx.line + 1; + const end_pos = ctx.pos; + try ctx.callback(ctx.data, begin_row, begin_pos, end_row, end_pos); + } else { + ctx.skip(&i, 1); + } + } + std.mem.copyForwards(u8, ctx.buf, ctx.rest[i..]); + ctx.rest = ctx.buf[0 .. ctx.rest.len - i]; + if (input.len == 0) + break; + if (ctx.rest.len == ctx.buf.len) + unreachable; + } + return bytes.len - input.len; + } + fn skip(ctx: *Ctx, i: *usize, n_: usize) void { + var n = n_; + while (n > 0) : (n -= 1) { + if (ctx.rest[i.*] == '\n') { + ctx.line += 1; + ctx.pos = 0; + } else { + ctx.pos += 1; + } + i.* += 1; + } + } + fn writer(ctx: *Ctx) Writer { + return .{ .context = ctx }; + } + }; + var ctx: Ctx = .{ + .pattern = pattern, + .data = data, + .callback = callback, + .buf = try a.alloc(u8, pattern.len * 2), + }; + defer a.free(ctx.buf); + return self.store(ctx.writer()); + } + + pub fn debug_render_chunks(self: *const Node, line: usize, output: *ArrayList(u8)) !void { + const ctx_ = struct { + l: *ArrayList(u8), + wcwidth: usize = 0, + fn walker(ctx_: *anyopaque, leaf: *const Leaf) Walker { + const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); + leaf.dump(ctx.l, ctx.wcwidth) catch |e| return .{ .err = e }; + ctx.wcwidth += leaf.width(ctx.wcwidth); + return if (!leaf.eol) Walker.keep_walking else Walker.stop; + } + }; + var ctx: ctx_ = .{ .l = output }; + const found = self.walk_from_line_begin_const(line, ctx_.walker, &ctx) catch true; + if (!found) return error.NotFound; + + var buf: [16]u8 = undefined; + const wcwidth = try std.fmt.bufPrint(&buf, "{d}", .{ctx.wcwidth}); + try output.appendSlice(wcwidth); + } + + pub fn debug_line_render_tree(self: *const Node, line: usize, l: *ArrayList(u8)) !void { + return if (self.find_line_node(line)) |n| n.debug_render_tree(l, 0) else error.NotFound; + } +}; + +pub fn create(a: Allocator) !*Self { + const self = try a.create(Self); + const arena_a = if (builtin.is_test) a else std.heap.page_allocator; + self.* = .{ + .arena = std.heap.ArenaAllocator.init(arena_a), + .a = self.arena.allocator(), + .external_a = a, + .root = try Node.new(self.a, &empty_leaf, &empty_leaf), + }; + return self; +} + +pub fn deinit(self: *Self) void { + self.arena.deinit(); + self.external_a.destroy(self); +} + +fn new_file(self: *const Self, file_exists: *bool) !Root { + file_exists.* = false; + return Leaf.new(self.a, "", true, false); +} + +pub fn load(self: *const Self, reader: anytype, size: usize) !Root { + const eol = '\n'; + var buf = try self.a.alloc(u8, size); + const read_size = try reader.read(buf); + if (read_size != size) + return error.BufferUnderrun; + const final_read = try reader.read(buf); + if (final_read != 0) + unreachable; + + var leaf_count: usize = 1; + for (0..buf.len) |i| { + if (buf[i] == eol) leaf_count += 1; + } + + var leaves = try self.a.alloc(Node, leaf_count); + var cur_leaf: usize = 0; + var b: usize = 0; + for (0..buf.len) |i| { + if (buf[i] == eol) { + const line = buf[b..i]; + leaves[cur_leaf] = .{ .leaf = .{ .buf = line, .bol = true, .eol = true } }; + cur_leaf += 1; + b = i + 1; + } + } + const line = buf[b..]; + leaves[cur_leaf] = .{ .leaf = .{ .buf = line, .bol = true, .eol = false } }; + if (leaves.len != cur_leaf + 1) + return error.Unexpected; + return Node.merge_in_place(leaves, self.a); +} + +pub fn load_from_string(self: *const Self, s: []const u8) !Root { + var stream = std.io.fixedBufferStream(s); + return self.load(stream.reader(), s.len); +} + +pub fn load_from_file(self: *const Self, file_path: []const u8, file_exists: *bool) !Root { + const file = cwd().openFile(file_path, .{ .mode = .read_only }) catch |e| switch (e) { + error.FileNotFound => return self.new_file(file_exists), + else => return e, + }; + + file_exists.* = true; + defer file.close(); + const stat = try file.stat(); + return self.load(file.reader(), stat.size); +} + +pub fn load_from_file_and_update(self: *Self, file_path: []const u8) !void { + var file_exists: bool = false; + self.root = try self.load_from_file(file_path, &file_exists); + self.file_path = try self.a.dupe(u8, file_path); + self.last_save = self.root; + self.file_exists = file_exists; +} + +pub fn store_to_string(self: *const Self, a: Allocator) ![]u8 { + var s = try ArrayList(u8).initCapacity(a, self.root.weights_sum().len); + try self.root.store(s.writer()); + return s.toOwnedSlice(); +} + +fn store_to_file_const(self: *const Self, file: anytype) !void { + const buffer_size = 4096 * 16; // 64KB + const BufferedWriter = std.io.BufferedWriter(buffer_size, std.fs.File.Writer); + const Writer = std.io.Writer(*BufferedWriter, BufferedWriter.Error, BufferedWriter.write); + + const file_writer: std.fs.File.Writer = file.writer(); + var buffered_writer: BufferedWriter = .{ .unbuffered_writer = file_writer }; + + try self.root.store(Writer{ .context = &buffered_writer }); + try buffered_writer.flush(); +} + +pub fn store_to_existing_file_const(self: *const Self, file_path: []const u8) !void { + const stat = try cwd().statFile(file_path); + var atomic = try cwd().atomicFile(file_path, .{ .mode = stat.mode }); + defer atomic.deinit(); + try self.store_to_file_const(atomic.file); + try atomic.finish(); +} + +pub fn store_to_new_file_const(self: *const Self, file_path: []const u8) !void { + const file = try cwd().createFile(file_path, .{ .read = true, .truncate = true }); + defer file.close(); + try self.store_to_file_const(file); +} + +pub fn store_to_file_and_clean(self: *Self, file_path: []const u8) !void { + self.store_to_existing_file_const(file_path) catch |e| switch (e) { + error.FileNotFound => try self.store_to_new_file_const(file_path), + else => return e, + }; + self.last_save = self.root; + self.file_exists = true; +} + +pub fn is_dirty(self: *const Self) bool { + return if (!self.file_exists) true else if (self.last_save) |p| self.root != p else true; +} + +pub fn version(self: *const Self) usize { + return @intFromPtr(self.root); +} + +pub fn update(self: *Self, root: Root) void { + self.root = root; +} + +pub fn store_undo(self: *Self, meta: []const u8) !void { + self.push_undo(try self.create_undo(self.root, meta)); + self.curr_history = null; + try self.push_redo_branch(); +} + +fn create_undo(self: *const Self, root: Root, meta_: []const u8) !*UndoNode { + const h = try self.a.create(UndoNode); + const meta = try self.a.dupe(u8, meta_); + h.* = UndoNode{ + .root = root, + .meta = meta, + }; + return h; +} + +fn push_undo(self: *Self, h: *UndoNode) void { + const next = self.undo_history; + self.undo_history = h; + h.next = next; +} + +fn push_redo(self: *Self, h: *UndoNode) void { + const next = self.redo_history; + self.redo_history = h; + h.next = next; +} + +fn push_redo_branch(self: *Self) !void { + const r = self.redo_history orelse return; + const u = self.undo_history orelse return; + const next = u.branches; + const b = try self.a.create(UndoBranch); + b.* = .{ + .redo = r, + .next = next, + }; + u.branches = b; + self.redo_history = null; +} + +pub fn undo(self: *Self, meta: []const u8) error{Stop}![]const u8 { + const r = self.curr_history orelse self.create_undo(self.root, meta) catch return error.Stop; + const h = self.undo_history orelse return error.Stop; + self.undo_history = h.next; + self.curr_history = h; + self.root = h.root; + self.push_redo(r); + return h.meta; +} + +pub fn redo(self: *Self) error{Stop}![]const u8 { + const u = self.curr_history orelse return error.Stop; + const h = self.redo_history orelse return error.Stop; + if (u.root != self.root) return error.Stop; + self.redo_history = h.next; + self.curr_history = h; + self.root = h.root; + self.push_undo(u); + return h.meta; +} + +fn egc_len(egcs: []const u8, colcount: *c_int, abs_col: usize) usize { + if (egcs[0] == '\t') { + colcount.* = @intCast(8 - abs_col % 8); + return 1; + } + return nc.ncegc_len(egcs, colcount) catch ret: { + colcount.* = 1; + break :ret 1; + }; +} + +fn egc_chunk_width(chunk_: []const u8, abs_col_: usize) usize { + var abs_col = abs_col_; + var chunk = chunk_; + var colcount: usize = 0; + var cols: c_int = 0; + while (chunk.len > 0) { + const bytes = egc_len(chunk, &cols, abs_col); + colcount += @intCast(cols); + abs_col += @intCast(cols); + if (chunk.len < bytes) break; + chunk = chunk[bytes..]; + } + return colcount; +} diff --git a/src/Cursor.zig b/src/Cursor.zig new file mode 100644 index 0000000..0906240 --- /dev/null +++ b/src/Cursor.zig @@ -0,0 +1,220 @@ +const std = @import("std"); +const cbor = @import("cbor"); +const Buffer = @import("Buffer.zig"); +const View = @import("View.zig"); +const Selection = @import("Selection.zig"); + +row: usize = 0, +col: usize = 0, +target: usize = 0, + +const Self = @This(); + +pub inline fn invalid() Self { + return .{ + .row = std.math.maxInt(u32), + .col = std.math.maxInt(u32), + .target = std.math.maxInt(u32), + }; +} + +pub inline fn eql(self: Self, other: Self) bool { + return self.row == other.row and self.col == other.col; +} + +pub inline fn right_of(self: Self, other: Self) bool { + return if (self.row > other.row) true else if (self.row == other.row and self.col > other.col) true else false; +} + +pub fn clamp_to_buffer(self: *Self, root: Buffer.Root) void { + self.row = @min(self.row, root.lines() - 1); + self.col = @min(self.col, root.line_width(self.row) catch 0); +} + +fn follow_target(self: *Self, root: Buffer.Root) void { + self.col = @min(self.target, root.line_width(self.row) catch 0); +} + +fn move_right_no_target(self: *Self, root: Buffer.Root) !void { + const lines = root.lines(); + if (lines <= self.row) return error.Stop; + if (self.col < root.line_width(self.row) catch 0) { + _, const wcwidth, const offset = root.ecg_at(self.row, self.col) catch return error.Stop; + self.col += wcwidth - offset; + } else if (self.row < lines - 1) { + self.col = 0; + self.row += 1; + } else return error.Stop; +} + +pub fn move_right(self: *Self, root: Buffer.Root) !void { + try self.move_right_no_target(root); + self.target = self.col; +} + +fn move_left_no_target(self: *Self, root: Buffer.Root) !void { + if (self.col == 0) { + if (self.row == 0) return error.Stop; + self.row -= 1; + self.col = root.line_width(self.row) catch 0; + } else { + _, const wcwidth, _ = root.ecg_at(self.row, self.col - 1) catch return error.Stop; + if (self.col > wcwidth) self.col -= wcwidth else self.col = 0; + } +} + +pub fn move_left(self: *Self, root: Buffer.Root) !void { + try self.move_left_no_target(root); + self.target = self.col; +} + +pub fn move_up(self: *Self, root: Buffer.Root) !void { + if (self.row > 0) { + self.row -= 1; + self.follow_target(root); + self.move_left_no_target(root) catch return; + try self.move_right_no_target(root); + } else return error.Stop; +} + +pub fn move_down(self: *Self, root: Buffer.Root) !void { + if (self.row < root.lines() - 1) { + self.row += 1; + self.follow_target(root); + self.move_left_no_target(root) catch return; + try self.move_right_no_target(root); + } else return error.Stop; +} + +pub fn move_page_up(self: *Self, root: Buffer.Root, view: *const View) void { + self.row = if (self.row > view.rows) self.row - view.rows else 0; + self.follow_target(root); + self.move_left_no_target(root) catch return; + self.move_right_no_target(root) catch return; +} + +pub fn move_page_down(self: *Self, root: Buffer.Root, view: *const View) void { + if (root.lines() < view.rows) { + self.move_buffer_last(root); + } else if (self.row < root.lines() - view.rows - 1) { + self.row += view.rows; + } else self.row = root.lines() - 1; + self.follow_target(root); + self.move_left_no_target(root) catch return; + self.move_right_no_target(root) catch return; +} + +pub fn move_to(self: *Self, root: Buffer.Root, row: usize, col: usize) !void { + if (row < root.lines()) { + self.row = row; + self.col = @min(col, root.line_width(self.row) catch return error.Stop); + self.target = self.col; + } else return error.Stop; +} + +pub fn move_abs(self: *Self, root: Buffer.Root, v: *View, y: usize, x: usize) !void { + self.row = v.row + y; + self.col = v.col + x; + self.clamp_to_buffer(root); + self.target = self.col; +} + +pub fn move_begin(self: *Self) void { + self.col = 0; + self.target = self.col; +} + +pub fn move_end(self: *Self, root: Buffer.Root) void { + if (self.row < root.lines()) self.col = root.line_width(self.row) catch 0; + self.target = std.math.maxInt(u32); +} + +pub fn move_buffer_begin(self: *Self) void { + self.row = 0; + self.col = 0; + self.target = 0; +} + +pub fn move_buffer_end(self: *Self, root: Buffer.Root) void { + self.row = root.lines() - 1; + self.move_end(root); + if (self.col == 0) self.target = 0; +} + +fn move_buffer_first(self: *Self, root: Buffer.Root) void { + self.row = 0; + self.follow_target(root); +} + +fn move_buffer_last(self: *Self, root: Buffer.Root) void { + self.row = root.lines() - 1; + self.follow_target(root); +} + +fn is_at_begin(self: *const Self) bool { + return self.col == 0; +} + +fn is_at_end(self: *const Self, root: Buffer.Root) bool { + return if (self.row < root.lines()) self.col == root.line_width(self.row) catch 0 else true; +} + +pub fn test_at(self: *const Self, root: Buffer.Root, pred: *const fn (c: []const u8) bool) bool { + return root.test_at(pred, self.row, self.col); +} + +pub fn write(self: *const Self, writer: Buffer.MetaWriter) !void { + try cbor.writeValue(writer, .{ + self.row, + self.col, + self.target, + }); +} + +pub fn extract(self: *Self, iter: *[]const u8) !bool { + return cbor.matchValue(iter, .{ + cbor.extract(&self.row), + cbor.extract(&self.col), + cbor.extract(&self.target), + }); +} + +pub fn nudge_insert(self: *Self, nudge: Selection) void { + if (self.row < nudge.begin.row or (self.row == nudge.begin.row and self.col < nudge.begin.col)) return; + + const rows = nudge.end.row - nudge.begin.row; + if (self.row == nudge.begin.row) { + if (nudge.begin.row < nudge.end.row) { + self.row += rows; + self.col = self.col - nudge.begin.col + nudge.end.col; + } else { + self.col += nudge.end.col - nudge.begin.col; + } + } else { + self.row += rows; + } +} + +pub fn nudge_delete(self: *Self, nudge: Selection) bool { + if (self.row < nudge.begin.row or (self.row == nudge.begin.row and self.col < nudge.begin.col)) return true; + if (self.row == nudge.begin.row) { + if (nudge.begin.row < nudge.end.row) { + return false; + } else { + if (self.col < nudge.end.col) { + return false; + } + self.col -= nudge.end.col - nudge.begin.col; + return true; + } + } + if (self.row < nudge.end.row) return false; + if (self.row == nudge.end.row) { + if (self.col < nudge.end.col) return false; + self.row -= nudge.end.row - nudge.begin.row; + self.col -= nudge.end.col; + return true; + } + self.row -= nudge.end.row - nudge.begin.row; + return true; +} diff --git a/src/Selection.zig b/src/Selection.zig new file mode 100644 index 0000000..dfd3cd4 --- /dev/null +++ b/src/Selection.zig @@ -0,0 +1,61 @@ +const std = @import("std"); +const Buffer = @import("Buffer.zig"); +const Cursor = @import("Cursor.zig"); + +begin: Cursor = Cursor{}, +end: Cursor = Cursor{}, + +const Self = @This(); + +pub inline fn eql(self: Self, other: Self) bool { + return self.begin.eql(other.begin) and self.end.eql(other.end); +} + +pub fn from_cursor(cursor: *const Cursor) Self { + return .{ .begin = cursor.*, .end = cursor.* }; +} + +pub fn line_from_cursor(cursor: Cursor, root: Buffer.Root) Self { + var begin = cursor; + var end = cursor; + begin.move_begin(); + end.move_end(root); + end.move_right(root) catch {}; + return .{ .begin = begin, .end = end }; +} + +pub fn empty(self: *const Self) bool { + return self.begin.eql(self.end); +} + +pub fn reverse(self: *Self) void { + const tmp = self.begin; + self.begin = self.end; + self.end = tmp; +} + +pub fn normalize(self: *Self) void { + if (self.begin.right_of(self.end)) + self.reverse(); +} + +pub fn write(self: *const Self, writer: Buffer.MetaWriter) !void { + try self.begin.write(writer); + try self.end.write(writer); +} + +pub fn extract(self: *Self, iter: *[]const u8) !bool { + if (!try self.begin.extract(iter)) return false; + return self.end.extract(iter); +} + +pub fn nudge_insert(self: *Self, nudge: Self) void { + self.begin.nudge_insert(nudge); + self.end.nudge_insert(nudge); +} + +pub fn nudge_delete(self: *Self, nudge: Self) bool { + if (!self.begin.nudge_delete(nudge)) + return false; + return self.end.nudge_delete(nudge); +} diff --git a/src/View.zig b/src/View.zig new file mode 100644 index 0000000..b3909ca --- /dev/null +++ b/src/View.zig @@ -0,0 +1,142 @@ +const std = @import("std"); +const cbor = @import("cbor"); +const Buffer = @import("Buffer.zig"); +const Cursor = @import("Cursor.zig"); +const Selection = @import("Selection.zig"); + +row: usize = 0, +col: usize = 0, +rows: usize = 0, +cols: usize = 0, + +const scroll_cursor_min_border_distance = 5; +const scroll_cursor_min_border_distance_mouse = 1; + +const Self = @This(); + +pub inline fn invalid() Self { + return .{ + .row = std.math.maxInt(u32), + .col = std.math.maxInt(u32), + }; +} + +inline fn reset(self: *Self) void { + self.* = .{}; +} + +pub inline fn eql(self: Self, other: Self) bool { + return self.row == other.row and self.col == other.col and self.rows == other.rows and self.cols == other.cols; +} + +pub fn move_left(self: *Self) !void { + if (self.col > 0) { + self.col -= 1; + } else return error.Stop; +} + +pub fn move_right(self: *Self) !void { + self.col += 1; +} + +pub fn move_up(self: *Self) !void { + if (!self.is_at_top()) { + self.row -= 1; + } else return error.Stop; +} + +pub fn move_down(self: *Self, root: Buffer.Root) !void { + if (!self.is_at_bottom(root)) { + self.row += 1; + } else return error.Stop; +} + +pub fn move_to(self: *Self, root: Buffer.Root, row: usize) !void { + if (row < root.lines() - self.rows - 1) { + self.row = row; + } else return error.Stop; +} + +inline fn is_at_top(self: *const Self) bool { + return self.row == 0; +} + +inline fn is_at_bottom(self: *const Self, root: Buffer.Root) bool { + if (root.lines() < self.rows) return true; + return self.row >= root.lines() - scroll_cursor_min_border_distance; +} + +pub inline fn is_visible(self: *const Self, cursor: *const Cursor) bool { + const row_min = self.row; + const row_max = row_min + self.rows; + const col_min = self.col; + const col_max = col_min + self.cols; + return row_min <= cursor.row and cursor.row <= row_max and + col_min <= cursor.col and cursor.col < col_max; +} + +inline fn is_visible_selection(self: *const Self, sel: *const Selection) bool { + const row_min = self.row; + const row_max = row_min + self.rows; + return self.is_visible(sel.begin) or is_visible(sel.end) or + (sel.begin.row < row_min and sel.end.row > row_max); +} + +inline fn to_cursor_top(self: *const Self) Cursor { + return .{ .row = self.row, .col = 0 }; +} + +inline fn to_cursor_bottom(self: *const Self, root: Buffer.Root) Cursor { + const bottom = @min(root.lines(), self.row + self.rows + 1); + return .{ .row = bottom, .col = 0 }; +} + +fn clamp_row(self: *Self, cursor: *const Cursor, abs: bool) void { + const min_border_distance: usize = if (abs) scroll_cursor_min_border_distance_mouse else scroll_cursor_min_border_distance; + if (cursor.row < min_border_distance) { + self.row = 0; + return; + } + if (self.row > 0 and cursor.row >= min_border_distance) { + if (cursor.row < self.row + min_border_distance) { + self.row = cursor.row - min_border_distance; + return; + } + } + if (cursor.row < self.row) { + self.row = 0; + } else if (cursor.row > self.row + self.rows - min_border_distance) { + self.row = cursor.row + min_border_distance - self.rows; + } +} + +fn clamp_col(self: *Self, cursor: *const Cursor, _: bool) void { + if (cursor.col < self.col) { + self.col = cursor.col; + } else if (cursor.col > self.col + self.cols - 1) { + self.col = cursor.col - self.cols + 1; + } +} + +pub fn clamp(self: *Self, cursor: *const Cursor, abs: bool) void { + self.clamp_row(cursor, abs); + self.clamp_col(cursor, abs); +} + +pub fn write(self: *const Self, writer: Buffer.MetaWriter) !void { + try cbor.writeValue(writer, .{ + self.row, + self.col, + self.rows, + self.cols, + }); +} + +pub fn extract(self: *Self, iter: *[]const u8) !bool { + return cbor.matchValue(iter, .{ + cbor.extract(&self.row), + cbor.extract(&self.col), + cbor.extract(&self.rows), + cbor.extract(&self.cols), + }); +} diff --git a/src/color.zig b/src/color.zig new file mode 100644 index 0000000..dec4117 --- /dev/null +++ b/src/color.zig @@ -0,0 +1,62 @@ +const pow = @import("std").math.pow; + +pub const RGB = struct { + r: u8, + g: u8, + b: u8, + + pub inline fn from_u24(v: u24) RGB { + const r = @as(u8, @intCast(v >> 16 & 0xFF)); + const g = @as(u8, @intCast(v >> 8 & 0xFF)); + const b = @as(u8, @intCast(v & 0xFF)); + return .{ .r = r, .g = g, .b = b }; + } + + pub inline fn to_u24(v: RGB) u24 { + const r = @as(u24, @intCast(v.r)) << 16; + const g = @as(u24, @intCast(v.g)) << 8; + const b = @as(u24, @intCast(v.b)); + return r | b | g; + } + + pub fn contrast(a_: RGB, b_: RGB) f32 { + const a = RGBf.from_RGB(a_).luminance(); + const b = RGBf.from_RGB(b_).luminance(); + return (@max(a, b) + 0.05) / (@min(a, b) + 0.05); + } + + pub fn max_contrast(v: RGB, a: RGB, b: RGB) RGB { + return if (contrast(v, a) > contrast(v, b)) a else b; + } +}; + +pub const RGBf = struct { + r: f32, + g: f32, + b: f32, + + pub inline fn from_RGB(v: RGB) RGBf { + return .{ .r = tof(v.r), .g = tof(v.g), .b = tof(v.b) }; + } + + pub fn luminance(v: RGBf) f32 { + return linear(v.r) * RED + linear(v.g) * GREEN + linear(v.b) * BLUE; + } + + inline fn tof(c: u8) f32 { + return @as(f32, @floatFromInt(c)) / 255.0; + } + + inline fn linear(v: f32) f32 { + return if (v <= 0.03928) v / 12.92 else pow(f32, (v + 0.055) / 1.055, GAMMA); + } + + const RED = 0.2126; + const GREEN = 0.7152; + const BLUE = 0.0722; + const GAMMA = 2.4; +}; + +pub fn max_contrast(v: u24, a: u24, b: u24) u24 { + return RGB.max_contrast(RGB.from_u24(v), RGB.from_u24(a), RGB.from_u24(b)).to_u24(); +} diff --git a/src/config.zig b/src/config.zig new file mode 100644 index 0000000..a91290f --- /dev/null +++ b/src/config.zig @@ -0,0 +1,15 @@ +frame_rate: usize = 60, +theme: []const u8 = "default", +input_mode: []const u8 = "flow", +modestate_show: bool = true, +selectionstate_show: bool = true, +modstate_show: bool = false, +keystate_show: bool = false, +gutter_line_numbers: bool = true, +gutter_line_numbers_relative: bool = false, +enable_terminal_cursor: bool = false, +highlight_current_line: bool = true, +highlight_current_line_gutter: bool = true, +show_whitespace: bool = true, +animation_min_lag: usize = 0, //milliseconds +animation_max_lag: usize = 150, //milliseconds diff --git a/src/diff.zig b/src/diff.zig new file mode 100644 index 0000000..f35f20d --- /dev/null +++ b/src/diff.zig @@ -0,0 +1,165 @@ +const std = @import("std"); +const tp = @import("thespian"); +const dizzy = @import("dizzy"); +const Buffer = @import("Buffer"); +const tracy = @import("tracy"); +const cbor = @import("cbor"); + +const Self = @This(); +const module_name = @typeName(Self); +pub const Error = error{ OutOfMemory, Exit }; + +pub const Kind = enum { insert, delete }; +pub const Edit = struct { + kind: Kind, + line: usize, + offset: usize, + bytes: []const u8, +}; + +pid: ?tp.pid, + +pub fn create() Error!Self { + return .{ .pid = try Process.create() }; +} + +pub fn deinit(self: *Self) void { + if (self.pid) |pid| { + pid.send(.{"shutdown"}) catch {}; + pid.deinit(); + self.pid = null; + } +} + +const Process = struct { + arena: std.heap.ArenaAllocator, + a: std.mem.Allocator, + receiver: Receiver, + + const Receiver = tp.Receiver(*Process); + const outer_a = std.heap.page_allocator; + + pub fn create() Error!tp.pid { + const self = try outer_a.create(Process); + self.* = .{ + .arena = std.heap.ArenaAllocator.init(outer_a), + .a = self.arena.allocator(), + .receiver = Receiver.init(Process.receive, self), + }; + return tp.spawn_link(self.a, self, Process.start, module_name) catch |e| tp.exit_error(e); + } + + fn start(self: *Process) tp.result { + errdefer self.deinit(); + tp.receive(&self.receiver); + } + + fn deinit(self: *Process) void { + self.arena.deinit(); + outer_a.destroy(self); + } + + fn receive(self: *Process, from: tp.pid_ref, m: tp.message) tp.result { + errdefer self.deinit(); + + var cb: usize = 0; + var root_dst: usize = 0; + var root_src: usize = 0; + + return if (try m.match(.{ "D", tp.extract(&cb), tp.extract(&root_dst), tp.extract(&root_src) })) + self.diff(from, cb, root_dst, root_src) catch |e| tp.exit_error(e) + else if (try m.match(.{"shutdown"})) + tp.exit_normal(); + } + + fn diff(self: *Process, from: tp.pid_ref, cb_addr: usize, root_new_addr: usize, root_old_addr: usize) !void { + const frame = tracy.initZone(@src(), .{ .name = "diff" }); + defer frame.deinit(); + const cb: *CallBack = if (cb_addr == 0) return else @ptrFromInt(cb_addr); + const root_dst: Buffer.Root = if (root_new_addr == 0) return else @ptrFromInt(root_new_addr); + const root_src: Buffer.Root = if (root_old_addr == 0) return else @ptrFromInt(root_old_addr); + + var dizzy_edits = std.ArrayListUnmanaged(dizzy.Edit){}; + var dst = std.ArrayList(u8).init(self.a); + var src = std.ArrayList(u8).init(self.a); + var scratch = std.ArrayListUnmanaged(u32){}; + var edits = std.ArrayList(Edit).init(self.a); + + defer { + dst.deinit(); + src.deinit(); + scratch.deinit(self.a); + dizzy_edits.deinit(self.a); + } + + try root_dst.store(dst.writer()); + try root_src.store(src.writer()); + + const scratch_len = 4 * (dst.items.len + src.items.len) + 2; + try scratch.ensureTotalCapacity(self.a, scratch_len); + scratch.items.len = scratch_len; + + try dizzy.PrimitiveSliceDiffer(u8).diff(self.a, &dizzy_edits, src.items, dst.items, scratch.items); + + if (dizzy_edits.items.len > 2) + try edits.ensureTotalCapacity((dizzy_edits.items.len - 1) / 2); + + var lines_dst: usize = 0; + var pos_src: usize = 0; + var pos_dst: usize = 0; + var last_offset: usize = 0; + + for (dizzy_edits.items) |dizzy_edit| { + switch (dizzy_edit.kind) { + .equal => { + const dist = dizzy_edit.range.end - dizzy_edit.range.start; + pos_src += dist; + pos_dst += dist; + scan_char(src.items[dizzy_edit.range.start..dizzy_edit.range.end], &lines_dst, '\n', &last_offset); + }, + .insert => { + const dist = dizzy_edit.range.end - dizzy_edit.range.start; + pos_src += 0; + pos_dst += dist; + const line_start_dst: usize = lines_dst; + scan_char(dst.items[dizzy_edit.range.start..dizzy_edit.range.end], &lines_dst, '\n', null); + (try edits.addOne()).* = .{ + .kind = .insert, + .line = line_start_dst, + .offset = last_offset, + .bytes = dst.items[dizzy_edit.range.start..dizzy_edit.range.end], + }; + }, + .delete => { + const dist = dizzy_edit.range.end - dizzy_edit.range.start; + pos_src += dist; + pos_dst += 0; + (try edits.addOne()).* = .{ + .kind = .delete, + .line = lines_dst, + .offset = last_offset, + .bytes = src.items[dizzy_edit.range.start..dizzy_edit.range.end], + }; + }, + } + } + cb(from, edits.items); + } + + fn scan_char(chars: []const u8, lines: *usize, char: u8, last_offset: ?*usize) void { + var pos = chars; + while (pos.len > 0) { + if (pos[0] == char) { + if (last_offset) |off| off.* = pos.len - 1; + lines.* += 1; + } + pos = pos[1..]; + } + } +}; + +pub const CallBack = fn (from: tp.pid_ref, edits: []Edit) void; + +pub fn diff(self: Self, cb: *const CallBack, root_dst: Buffer.Root, root_src: Buffer.Root) tp.result { + if (self.pid) |pid| try pid.send(.{ "D", @intFromPtr(cb), @intFromPtr(root_dst), @intFromPtr(root_src) }); +} diff --git a/src/file_type.zig b/src/file_type.zig new file mode 100644 index 0000000..bba91e4 --- /dev/null +++ b/src/file_type.zig @@ -0,0 +1,127 @@ +const std = @import("std"); +const treez = @import("treez"); +pub const FileType = @This(); + +color: u24, +icon: []const u8, +name: []const u8, +lang_fn: LangFn, +extensions: []const []const u8, +highlights: [:0]const u8, +injections: ?[:0]const u8, +first_line_matches: ?FirstLineMatch = null, +comment: []const u8, + +pub fn get_by_name(name: []const u8) ?*const FileType { + for (file_types) |*file_type| + if (std.mem.eql(u8, file_type.name, name)) + return file_type; + return null; +} + +pub fn guess(file_path: ?[]const u8, content: []const u8) ?*const FileType { + if (guess_first_line(content)) |ft| return ft; + for (file_types) |*file_type| + if (file_path) |fp| if (match_file_type(file_type, fp)) + return file_type; + return null; +} + +fn guess_first_line(content: []const u8) ?*const FileType { + const first_line = if (std.mem.indexOf(u8, content, "\n")) |pos| content[0..pos] else content; + for (file_types) |*file_type| + if (file_type.first_line_matches) |match| + if (match_first_line(match, first_line)) + return file_type; + return null; +} + +fn match_first_line(match: FirstLineMatch, first_line: []const u8) bool { + if (match.prefix) |prefix| + if (prefix.len > first_line.len or !std.mem.eql(u8, first_line[0..prefix.len], prefix)) + return false; + if (match.content) |content| + if (std.mem.indexOf(u8, first_line, content)) |_| {} else return false; + return true; +} + +fn match_file_type(file_type: *const FileType, file_path: []const u8) bool { + const basename = std.fs.path.basename(file_path); + const extension = std.fs.path.extension(file_path); + return for (file_type.extensions) |ext| { + if (ext.len == basename.len and std.mem.eql(u8, ext, basename)) + return true; + if (extension.len > 0 and ext.len == extension.len - 1 and std.mem.eql(u8, ext, extension[1..])) + return true; + } else false; +} + +pub fn Parser(comptime lang: []const u8) LangFn { + return get_parser(lang); +} + +fn get_parser(comptime lang: []const u8) LangFn { + const language_name = ft_func_name(lang); + return @extern(?LangFn, .{ .name = "tree_sitter_" ++ language_name }) orelse @compileError(std.fmt.comptimePrint("Cannot find extern tree_sitter_{s}", .{language_name})); +} + +fn ft_func_name(comptime lang: []const u8) []const u8 { + var func_name: [lang.len]u8 = undefined; + for (lang, 0..) |c, i| + func_name[i] = if (c == '-') '_' else c; + return &func_name; +} + +const LangFn = *const fn () callconv(.C) ?*const treez.Language; + +const FirstLineMatch = struct { + prefix: ?[]const u8 = null, + content: ?[]const u8 = null, +}; + +const FileTypeOptions = struct { + extensions: []const []const u8 = &[_][]const u8{}, + comment: []const u8, + icon: ?[]const u8 = null, + color: ?u24 = null, + highlights: ?[:0]const u8 = null, + injections: ?[:0]const u8 = null, + first_line_matches: ?FirstLineMatch = null, + parser: ?LangFn = null, +}; + +fn DeclLang(comptime lang: []const u8, comptime args: FileTypeOptions) FileType { + return .{ + .color = args.color orelse 0xffffff, + .icon = args.icon orelse "󱀫", + .name = lang, + .lang_fn = if (args.parser) |p| p else get_parser(lang), + .extensions = args.extensions, + .comment = args.comment, + .highlights = if (args.highlights) |h| h else @embedFile("tree-sitter-" ++ lang ++ "/queries/highlights.scm"), + .injections = args.injections, + .first_line_matches = args.first_line_matches, + }; +} + +pub const file_types = load_file_types(@import("file_types.zig")); + +fn load_file_types(comptime Namespace: type) []FileType { + comptime switch (@typeInfo(Namespace)) { + .Struct => |info| { + var count = 0; + for (info.decls) |_| { + // @compileLog(decl.name, @TypeOf(@field(Namespace, decl.name))); + count += 1; + } + var cmds: [count]FileType = undefined; + var i = 0; + for (info.decls) |decl| { + cmds[i] = DeclLang(decl.name, @field(Namespace, decl.name)); + i += 1; + } + return &cmds; + }, + else => @compileError("expected tuple or struct type"), + }; +} diff --git a/src/file_types.zig b/src/file_types.zig new file mode 100644 index 0000000..b718b8a --- /dev/null +++ b/src/file_types.zig @@ -0,0 +1,294 @@ +pub const agda = .{ + .extensions = &[_][]const u8{"agda"}, + .comment = "--", +}; + +pub const bash = .{ + .color = 0x3e474a, + .icon = "󱆃", + .extensions = &[_][]const u8{ "sh", "bash" }, + .comment = "#", + .first_line_matches = .{ .prefix = "#!", .content = "sh" }, +}; + +pub const c = .{ + .icon = "󰙱", + .extensions = &[_][]const u8{ "c", "h" }, + .comment = "//", +}; + +pub const @"c-sharp" = .{ + .color = 0x68217a, + .icon = "󰌛", + .extensions = &[_][]const u8{"cs"}, + .comment = "//", +}; + +pub const conf = .{ + .color = 0x000000, + .icon = "", + .extensions = &[_][]const u8{ "conf", "config", ".gitconfig" }, + .highlights = fish.highlights, + .comment = "#", + .parser = fish.parser, +}; + +pub const cpp = .{ + .color = 0x9c033a, + .icon = "", + .extensions = &[_][]const u8{ "cc", "cpp", "cxx", "hpp", "hxx", "h", "ipp", "ixx" }, + .comment = "//", + .injections = @embedFile("tree-sitter-cpp/queries/injections.scm"), +}; + +pub const css = .{ + .color = 0x3d8fc6, + .icon = "󰌜", + .extensions = &[_][]const u8{"css"}, + .comment = "//", +}; + +pub const diff = .{ + .extensions = &[_][]const u8{ "diff", "patch" }, + .comment = "#", +}; + +pub const dockerfile = .{ + .color = 0x019bc6, + .icon = "", + .extensions = &[_][]const u8{ "Dockerfile", "dockerfile", "docker", "Containerfile", "container" }, + .comment = "#", +}; + +pub const dtd = .{ + .icon = "󰗀", + .extensions = &[_][]const u8{"dtd"}, + .comment = "