diff --git a/contrib/make_nightly_build b/contrib/make_nightly_build new file mode 100755 index 0000000..65949c5 --- /dev/null +++ b/contrib/make_nightly_build @@ -0,0 +1,175 @@ +#!/bin/bash +set -e + +for arg in "$@"; do + case "$arg" in + --no-github) NO_GITHUB=1 ;; + --no-codeberg) NO_CODEBERG=1 ;; + --no-flowcontrol) NO_FLOWCONTROL=1 ;; + --allow-dirty) ALLOW_DIRTY=1 ;; + esac +done + +builddir="nightly-build" + +DESTDIR="$(pwd)/$builddir" +BASEDIR="$(cd "$(dirname "$0")/.." && pwd)" +APPNAME="$(basename "$BASEDIR")" +title="$APPNAME nightly build" +repo="neurocyte/$APPNAME-nightly" + +release_notes="$BASEDIR/$builddir-release-notes" + +cd "$BASEDIR" + +if [ -e "$DESTDIR" ]; then + echo directory \"$builddir\" already exists + exit 1 +fi + +if [ -e "$release_notes" ]; then + echo file \""$release_notes"\" already exists + exit 1 +fi + +DIFF="$(git diff --stat --patch HEAD)" + +if [ -z "$ALLOW_DIRTY" ]; then + if [ -n "$DIFF" ]; then + echo there are outstanding changes: + echo "$DIFF" + exit 1 + fi + + UNPUSHED="$(git log --pretty=oneline '@{u}...')" + + if [ -n "$UNPUSHED" ]; then + echo there are unpushed commits: + echo "$UNPUSHED" + exit 1 + fi +fi + +# get latest version tag + +if [ -z "$NO_FLOWCONTROL" ]; then + last_nightly_version=$(curl -s https://git.flow-control.dev/api/v1/repos/neurocyte/flow-nightly/releases/latest | jq -r .tag_name) +elif [ -z "$NO_GITHUB" ]; then + last_nightly_version=$(curl -s "https://api.github.com/repos/$repo/releases/latest" | jq -r .tag_name) +elif [ -z "$NO_CODEBERG" ]; then + last_nightly_version=$(curl -s https://codeberg.org/api/v1/repos/neurocyte/flow-nightly/releases/latest | jq -r .tag_name) +fi +[ -z "$last_nightly_version" ] && { + echo "failed to fetch $title latest version" + exit 1 +} + +local_version="$(git --git-dir "$BASEDIR/.git" describe)" +if [ "$1" != "--no-github" ]; then + if [ "$local_version" == "$last_nightly_version" ]; then + echo "$title is already at version $last_nightly_version" + exit 1 + fi +fi + +echo +echo "building $title version $local_version... (previous $last_nightly_version)" +echo +git log "${last_nightly_version}..HEAD" --pretty="format:neurocyte/$APPNAME@%h %s" +echo +echo running tests... + +./zig build test + +echo building... + +./zig build -Dpackage_release --prefix "$DESTDIR/build" + +VERSION=$(/bin/cat "$DESTDIR/build/version") + +git archive --format=tar.gz --output="$DESTDIR/flow-$VERSION-source.tar.gz" HEAD +git archive --format=zip --output="$DESTDIR/flow-$VERSION-source.zip" HEAD + +cd "$DESTDIR/build" + +TARGETS=$(/bin/ls) + +for target in $TARGETS; do + if [ -d "$target" ]; then + cd "$target" + if [ "${target:0:8}" == "windows-" ]; then + echo packing zip "$target"... + zip -r "../../${APPNAME}-${VERSION}-${target}.zip" ./* + cd .. + else + echo packing tar "$target"... + tar -czf "../../${APPNAME}-${VERSION}-${target}.tar.gz" -- * + cd .. + fi + fi +done + +cd .. +rm -r build + +TARFILES=$(/bin/ls) + +for tarfile in $TARFILES; do + echo signing "$tarfile"... + gpg --local-user 4E6CF7234FFC4E14531074F98EB1E1BB660E3FB9 --detach-sig "$tarfile" + sha256sum -b "$tarfile" >"${tarfile}.sha256" +done + +echo "done making $title $VERSION @ $DESTDIR" +echo + +/bin/ls -lah + +cd .. + +{ + echo "## commits in this build" + echo + git log "${last_nightly_version}..HEAD" --pretty="format:neurocyte/$APPNAME@%h %s" + echo + echo + + echo "## contributors" + git shortlog -s -n "${last_nightly_version}..HEAD" | cut -b 8- + echo + + echo "## downloads" + echo "[flow-control.dev](https://git.flow-control.dev/neurocyte/flow-nightly/releases/tag/$VERSION) (source only)" + echo "[github.com](https://github.com/neurocyte/flow-nightly/releases/tag/$VERSION) (binaries & source)" + echo "[codeberg.org](https://codeberg.org/neurocyte/flow-nightly/releases/tag/$VERSION) (binaries & source)" +} >"$release_notes" + +cat "$release_notes" + +ASSETS="" + +if [ -z "$NO_FLOWCONTROL" ]; then + ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.tar.gz" + ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.tar.gz.sig" + ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.tar.gz.sha256" + ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.zip" + ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.zip.sig" + ASSETS="$ASSETS --asset $DESTDIR/flow-${VERSION}-source.zip.sha256" + echo uploading to git.flow-control.dev + tea releases create --login flow-control --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" \ + $ASSETS +fi + +if [ -z "$NO_CODEBERG" ]; then + for a in $DESTDIR/*; do + ASSETS="$ASSETS --asset $a" + done + echo uploading to codeberg.org + tea releases create --login codeberg --repo "$repo" --tag "$VERSION" --title "$title $VERSION" --note-file "$release_notes" \ + $ASSETS +fi + +if [ -z "$NO_GITHUB" ]; then + echo uploading to github.com + gh release create "$VERSION" --repo "$repo" --title "$title $VERSION" --notes-file "$release_notes" $DESTDIR/* +fi diff --git a/src/Project.zig b/src/Project.zig index f78db3a..92ba4d4 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -1005,10 +1005,17 @@ fn send_completion_items(to: tp.pid_ref, file_path: []const u8, row: usize, col: var item: []const u8 = ""; while (len > 0) : (len -= 1) { if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&item)))) return error.InvalidMessageField; - send_completion_item(to, file_path, row, col, item, if (len > 1) true else is_incomplete) catch return error.ClientFailed; + try send_completion_item(to, file_path, row, col, item, if (len > 1) true else is_incomplete); } } +fn invalid_field(field: []const u8) error{InvalidMessage} { + const logger = log.logger("lsp"); + defer logger.deinit(); + logger.print("invalid completion field '{s}'", .{field}); + return error.InvalidMessage; +} + fn send_completion_item(to: tp.pid_ref, file_path: []const u8, row: usize, col: usize, item: []const u8, is_incomplete: bool) (ClientError || InvalidMessageError || cbor.Error)!void { var label: []const u8 = ""; var label_detail: []const u8 = ""; @@ -1029,53 +1036,53 @@ fn send_completion_item(to: tp.pid_ref, file_path: []const u8, row: usize, col: var field_name: []const u8 = undefined; if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage; if (std.mem.eql(u8, field_name, "label")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&label)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&label)))) return invalid_field("label"); } else if (std.mem.eql(u8, field_name, "labelDetails")) { var len_ = cbor.decodeMapHeader(&iter) catch return; while (len_ > 0) : (len_ -= 1) { - if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage; + if (!(try cbor.matchString(&iter, &field_name))) return invalid_field("labelDetails"); if (std.mem.eql(u8, field_name, "detail")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&label_detail)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&label_detail)))) return invalid_field("labelDetails.detail"); } else if (std.mem.eql(u8, field_name, "description")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&label_description)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&label_description)))) return invalid_field("labelDetails.description"); } else { try cbor.skipValue(&iter); } } } else if (std.mem.eql(u8, field_name, "kind")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&kind)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&kind)))) return invalid_field("kind"); } else if (std.mem.eql(u8, field_name, "detail")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&detail)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&detail)))) return invalid_field("detail"); } else if (std.mem.eql(u8, field_name, "documentation")) { var len_ = cbor.decodeMapHeader(&iter) catch return; while (len_ > 0) : (len_ -= 1) { - if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage; + if (!(try cbor.matchString(&iter, &field_name))) return invalid_field("documentation"); if (std.mem.eql(u8, field_name, "kind")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&documentation_kind)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&documentation_kind)))) return invalid_field("documentation.kind"); } else if (std.mem.eql(u8, field_name, "value")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&documentation)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&documentation)))) return invalid_field("documentation.value"); } else { try cbor.skipValue(&iter); } } } else if (std.mem.eql(u8, field_name, "sortText")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&sortText)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&sortText)))) return invalid_field("sortText"); } else if (std.mem.eql(u8, field_name, "insertTextFormat")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&insertTextFormat)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&insertTextFormat)))) return invalid_field("insertTextFormat"); } else if (std.mem.eql(u8, field_name, "textEdit")) { // var textEdit: []const u8 = ""; // { "newText": "wait_expired(${1:timeout_ns: isize})", "insert": Range, "replace": Range }, var len_ = cbor.decodeMapHeader(&iter) catch return; while (len_ > 0) : (len_ -= 1) { - if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage; + if (!(try cbor.matchString(&iter, &field_name))) return invalid_field("textEdit"); if (std.mem.eql(u8, field_name, "newText")) { - if (!(try cbor.matchValue(&iter, cbor.extract(&textEdit_newText)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract(&textEdit_newText)))) return invalid_field("textEdit.newText"); } else if (std.mem.eql(u8, field_name, "insert")) { var range_: []const u8 = undefined; - if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range_)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range_)))) return invalid_field("textEdit.insert"); textEdit_insert = try read_range(range_); } else if (std.mem.eql(u8, field_name, "replace")) { var range_: []const u8 = undefined; - if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range_)))) return error.InvalidMessageField; + if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range_)))) return invalid_field("textEdit.replace"); textEdit_replace = try read_range(range_); } else { try cbor.skipValue(&iter); @@ -1085,8 +1092,8 @@ fn send_completion_item(to: tp.pid_ref, file_path: []const u8, row: usize, col: try cbor.skipValue(&iter); } } - const insert = textEdit_insert orelse return error.InvalidMessageField; - const replace = textEdit_replace orelse return error.InvalidMessageField; + const insert = textEdit_insert orelse Range{ .start = .{ .line = 0, .character = 0 }, .end = .{ .line = 0, .character = 0 } }; + const replace = textEdit_replace orelse Range{ .start = .{ .line = 0, .character = 0 }, .end = .{ .line = 0, .character = 0 } }; return to.send(.{ "cmd", "add_completion", .{ file_path, diff --git a/src/buffer/Buffer.zig b/src/buffer/Buffer.zig index d6d102b..ce03307 100644 --- a/src/buffer/Buffer.zig +++ b/src/buffer/Buffer.zig @@ -794,6 +794,35 @@ const Node = union(enum) { return if (found) ctx.result else error.NotFound; } + pub fn byte_offset_to_line_and_col(self: *const Node, pos: usize, metrics: Metrics, eol_mode: EolMode) Cursor { + const ctx_ = struct { + pos: usize, + line: usize = 0, + col: usize = 0, + eol_mode: EolMode, + fn walker(ctx_: *anyopaque, egc: []const u8, wcwidth: usize, _: Metrics) Walker { + const ctx = @as(*@This(), @ptrCast(@alignCast(ctx_))); + if (egc[0] == '\n') { + ctx.pos -= switch (ctx.eol_mode) { + .lf => 1, + .crlf => @min(2, ctx.pos), + }; + if (ctx.pos == 0) return Walker.stop; + ctx.line += 1; + ctx.col = 0; + } else { + ctx.pos -= @min(egc.len, ctx.pos); + if (ctx.pos == 0) return Walker.stop; + ctx.col += wcwidth; + } + return Walker.keep_walking; + } + }; + var ctx: ctx_ = .{ .pos = pos + 1, .eol_mode = eol_mode }; + self.walk_egc_forward(0, ctx_.walker, &ctx, metrics) catch {}; + return .{ .row = ctx.line, .col = ctx.col }; + } + pub fn insert_chars( self_: *const Node, line_: usize, diff --git a/src/file_link.zig b/src/file_link.zig index ec53f39..cc3b817 100644 --- a/src/file_link.zig +++ b/src/file_link.zig @@ -13,6 +13,7 @@ pub const FileDest = struct { column: ?usize = null, end_column: ?usize = null, exists: bool = false, + offset: ?usize = null, }; pub const DirDest = struct { @@ -37,11 +38,17 @@ pub fn parse(link: []const u8) error{InvalidFileLink}!Dest { .{ .file = .{ .path = it.first() } }; switch (dest) { .file => |*file| { - if (it.next()) |line_| + if (it.next()) |line_| if (line_.len > 0 and line_[0] == 'b') { + file.offset = std.fmt.parseInt(usize, line_[1..], 10) catch blk: { + file.path = link; + break :blk null; + }; + } else { file.line = std.fmt.parseInt(usize, line_, 10) catch blk: { file.path = link; break :blk null; }; + }; if (file.line) |_| if (it.next()) |col_| { file.column = std.fmt.parseInt(usize, col_, 10) catch null; }; @@ -88,6 +95,9 @@ pub fn parse_bracket_link(link: []const u8) error{InvalidFileLink}!Dest { pub fn navigate(to: tp.pid_ref, link: *const Dest) anyerror!void { switch (link.*) { .file => |file| { + if (file.offset) |offset| { + return to.send(.{ "cmd", "navigate", .{ .file = file.path, .offset = offset } }); + } if (file.line) |l| { if (file.column) |col| { try to.send(.{ "cmd", "navigate", .{ .file = file.path, .line = l, .column = col } }); diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index ca9a7c4..dbed511 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -77,12 +77,20 @@ ["ctrl+enter", "smart_insert_line_after"], ["ctrl+end", "move_buffer_end"], ["ctrl+home", "move_buffer_begin"], + ["ctrl+kp_end", "move_buffer_end"], + ["ctrl+kp_home", "move_buffer_begin"], ["ctrl+up", "move_scroll_up"], ["ctrl+down", "move_scroll_down"], + ["ctrl+kp_up", "move_scroll_up"], + ["ctrl+kp_down", "move_scroll_down"], ["ctrl+page_up", "move_scroll_page_up"], ["ctrl+page_down", "move_scroll_page_down"], + ["ctrl+kp_page_up", "move_scroll_page_up"], + ["ctrl+kp_page_down", "move_scroll_page_down"], ["ctrl+left", "move_word_left"], ["ctrl+right", "move_word_right"], + ["ctrl+kp_left", "move_word_left"], + ["ctrl+kp_right", "move_word_right"], ["ctrl+backspace", "delete_word_left"], ["ctrl+delete", "delete_word_right"], ["ctrl+f5", "toggle_inspector_view"], @@ -98,10 +106,16 @@ ["ctrl+shift+enter", "smart_insert_line_before"], ["ctrl+shift+end", "select_buffer_end"], ["ctrl+shift+home", "select_buffer_begin"], + ["ctrl+shift+kp_end", "select_buffer_end"], + ["ctrl+shift+kp_home", "select_buffer_begin"], ["ctrl+shift+up", "select_scroll_up"], ["ctrl+shift+down", "select_scroll_down"], + ["ctrl+shift+kp_up", "select_scroll_up"], + ["ctrl+shift+kp_down", "select_scroll_down"], ["ctrl+shift+left", "select_word_left"], ["ctrl+shift+right", "select_word_right"], + ["ctrl+shift+kp_left", "select_word_left"], + ["ctrl+shift+kp_right", "select_word_right"], ["ctrl+shift+space", "selections_reverse"], ["alt+o", "open_previous_file"], ["alt+j", "join_next_line"], @@ -117,8 +131,12 @@ ["alt+R", ["shell_execute_insert", "openssl", "rand", "-hex", "4"]], ["alt+left", "jump_back"], ["alt+right", "jump_forward"], + ["alt+kp_left", "jump_back"], + ["alt+kp_right", "jump_forward"], ["alt+up", "pull_up"], ["alt+down", "pull_down"], + ["alt+kp_up", "pull_up"], + ["alt+kp_down", "pull_down"], ["alt+enter", "insert_line"], ["alt+f10", "gutter_mode_next"], ["alt+shift+f10", "gutter_style_next"], @@ -128,29 +146,49 @@ ["alt+shift+s", "filter", "sort", "-u"], ["alt+shift+v", "paste"], ["alt+shift+i", "add_cursors_to_line_ends"], - ["alt+shift+left", "shrink_selection"], - ["alt+shift+right", "expand_selection"], + ["alt+shift+left", "expand_selection"], + ["alt+shift+right", "shrink_selection"], + ["alt+shift+kp_left", "expand_selection"], + ["alt+shift+kp_right", "shrink_selection"], ["alt+home", "select_prev_sibling"], ["alt+end", "select_next_sibling"], + ["alt+kp_home", "select_prev_sibling"], + ["alt+kp_end", "select_next_sibling"], + ["alt+{", "expand_selection"], + ["alt+}", "shrink_selection", true], + ["alt+[", "select_prev_sibling", true], + ["alt+]", "select_next_sibling", true], ["alt+shift+e", "move_parent_node_end"], ["alt+shift+b", "move_parent_node_start"], ["alt+a", "select_all_siblings"], ["alt+shift+home", "move_scroll_left"], ["alt+shift+end", "move_scroll_right"], + ["alt+shift+kp_home", "move_scroll_left"], + ["alt+shift+kp_end", "move_scroll_right"], ["alt+shift+up", "add_cursor_up"], ["alt+shift+down", "add_cursor_down"], + ["alt+shift+kp_up", "add_cursor_up"], + ["alt+shift+kp_down", "add_cursor_down"], ["alt+shift+f12", "goto_type_definition"], ["shift+f3", "goto_prev_match"], ["shift+f10", "toggle_syntax_highlighting"], ["shift+f12", "references"], ["shift+left", "select_left"], ["shift+right", "select_right"], + ["shift+kp_left", "select_left"], + ["shift+kp_right", "select_right"], ["shift+up", "select_up"], ["shift+down", "select_down"], + ["shift+kp_up", "select_up"], + ["shift+kp_down", "select_down"], ["shift+home", "smart_select_begin"], ["shift+end", "select_end"], + ["shift+kp_home", "smart_select_begin"], + ["shift+kp_end", "select_end"], ["shift+page_up", "select_page_up"], ["shift+page_down", "select_page_down"], + ["shift+kp_page_up", "select_page_up"], + ["shift+kp_page_down", "select_page_down"], ["shift+enter", "smart_insert_line_before"], ["shift+backspace", "delete_backward"], ["shift+tab", "unindent"], @@ -173,12 +211,20 @@ ["backspace", "smart_delete_backward"], ["left", "move_left"], ["right", "move_right"], + ["kp_left", "move_left"], + ["kp_right", "move_right"], ["up", "move_up"], ["down", "move_down"], + ["kp_up", "move_up"], + ["kp_down", "move_down"], ["home", "smart_move_begin"], ["end", "move_end"], + ["kp_home", "smart_move_begin"], + ["kp_end", "move_end"], ["page_up", "move_page_up"], ["page_down", "move_page_down"], + ["kp_page_up", "move_page_up"], + ["kp_page_down", "move_page_down"], ["tab", "indent"], ["ctrl+space", "enter_mode", "select"], @@ -231,16 +277,30 @@ ["right", "select_right"], ["ctrl+left", "select_word_left"], ["ctrl+right", "select_word_right"], + ["kp_left", "select_left"], + ["kp_right", "select_right"], + ["ctrl+kp_left", "select_word_left"], + ["ctrl+kp_right", "select_word_right"], ["up", "select_up"], ["down", "select_down"], + ["kp_up", "select_up"], + ["kp_down", "select_down"], ["home", "select_begin"], ["end", "select_end"], + ["kp_home", "select_begin"], + ["kp_end", "select_end"], ["ctrl+home", "select_buffer_begin"], ["ctrl+end", "select_buffer_end"], + ["ctrl+kp_home", "select_buffer_begin"], + ["ctrl+kp_end", "select_buffer_end"], ["page_up", "select_page_up"], ["page_down", "select_page_down"], ["ctrl+page_up", "select_scroll_page_up"], ["ctrl+page_down", "select_scroll_page_down"], + ["kp_page_up", "select_page_up"], + ["kp_page_down", "select_page_down"], + ["ctrl+kp_page_up", "select_scroll_page_up"], + ["ctrl+kp_page_down", "select_scroll_page_down"], ["ctrl+b", "move_to_char", "select_to_char_left"], ["ctrl+t", "move_to_char", "select_to_char_right"], ["ctrl+space", "enter_mode", "normal"], @@ -282,6 +342,8 @@ ["q", "quit"], ["up", "home_menu_up"], ["down", "home_menu_down"], + ["kp_up", "home_menu_up"], + ["kp_down", "home_menu_down"], ["enter", "home_menu_activate"] ] }, @@ -304,8 +366,12 @@ ["ctrl+escape", "palette_menu_cancel"], ["ctrl+up", "palette_menu_up"], ["ctrl+down", "palette_menu_down"], + ["ctrl+kp_up", "palette_menu_up"], + ["ctrl+kp_down", "palette_menu_down"], ["ctrl+page_up", "palette_menu_pageup"], ["ctrl+page_down", "palette_menu_pagedown"], + ["ctrl+kp_page_up", "palette_menu_pageup"], + ["ctrl+kp_page_down", "palette_menu_pagedown"], ["ctrl+enter", "palette_menu_activate"], ["ctrl+backspace", "overlay_delete_word_left"], ["ctrl+shift+e", "palette_menu_up"], @@ -326,10 +392,16 @@ ["escape", "palette_menu_cancel"], ["up", "palette_menu_up"], ["down", "palette_menu_down"], + ["kp_up", "palette_menu_up"], + ["kp_down", "palette_menu_down"], ["page_up", "palette_menu_pageup"], ["page_down", "palette_menu_pagedown"], + ["kp_page_up", "palette_menu_pageup"], + ["kp_page_down", "palette_menu_pagedown"], ["home", "palette_menu_top"], ["end", "palette_menu_bottom"], + ["kp_home", "palette_menu_top"], + ["kp_end", "palette_menu_bottom"], ["enter", "palette_menu_activate"], ["delete", "palette_menu_delete_item"], ["backspace", "overlay_delete_backwards"] @@ -341,6 +413,7 @@ }, "mini/numeric": { "press": [ + ["b", "goto_offset"], ["ctrl+q", "quit"], ["ctrl+v", "system_paste"], ["ctrl+u", "mini_mode_reset"], @@ -399,8 +472,12 @@ ["shift+tab", "mini_mode_reverse_complete_file"], ["up", "mini_mode_reverse_complete_file"], ["down", "mini_mode_try_complete_file"], - ["right", "mini_mode_try_complete_file_forward"], + ["kp_up", "mini_mode_reverse_complete_file"], + ["kp_down", "mini_mode_try_complete_file"], ["left", "mini_mode_delete_to_previous_path_segment"], + ["right", "mini_mode_try_complete_file_forward"], + ["kp_left", "mini_mode_delete_to_previous_path_segment"], + ["kp_right", "mini_mode_try_complete_file_forward"], ["tab", "mini_mode_try_complete_file"], ["escape", "mini_mode_cancel"], ["enter", "mini_mode_select"], @@ -430,6 +507,8 @@ ["shift+f3", "goto_prev_match"], ["up", "select_prev_file"], ["down", "select_next_file"], + ["kp_up", "select_prev_file"], + ["kp_down", "select_next_file"], ["f3", "goto_next_match"], ["f15", "goto_prev_match"], ["f9", "theme_prev"], @@ -462,6 +541,8 @@ ["shift+f3", "goto_prev_match"], ["up", "mini_mode_history_prev"], ["down", "mini_mode_history_next"], + ["kp_up", "mini_mode_history_prev"], + ["kp_down", "mini_mode_history_next"], ["f3", "goto_next_match"], ["f15", "goto_prev_match"], ["f9", "theme_prev"], diff --git a/src/keybind/builtin/helix.json b/src/keybind/builtin/helix.json index 2129459..c9226cd 100644 --- a/src/keybind/builtin/helix.json +++ b/src/keybind/builtin/helix.json @@ -111,6 +111,8 @@ ["home", "move_begin"], ["end", "move_end"], + ["kp_home", "move_begin"], + ["kp_end", "move_end"], ["w","move_next_word_start"], ["b","move_prev_word_start"], @@ -201,6 +203,8 @@ ["page_up", "move_scroll_page_up"], ["page_down", "move_scroll_page_down"], + ["kp_page_up", "move_scroll_page_up"], + ["kp_page_down", "move_scroll_page_down"], ["space F", "find_file"], ["space S", "workspace_symbol_picker"], @@ -293,6 +297,10 @@ ["alt+down", "shrink_selection"], ["alt+left", "select_prev_sibling"], ["alt+right", "select_next_sibling"], + ["alt+kp_up", "expand_selection"], + ["alt+kp_down", "shrink_selection"], + ["alt+kp_left", "select_prev_sibling"], + ["alt+kp_right", "select_next_sibling"], ["alt+e", "extend_parent_node_end"], ["alt+b", "extend_parent_node_start"], @@ -378,6 +386,10 @@ ["down", "select_down"], ["up", "select_up"], ["right", "select_right"], + ["kp_left", "select_left"], + ["kp_down", "select_down"], + ["kp_up", "select_up"], + ["kp_right", "select_right"], ["t", "extend_till_char"], ["f", "move_to_char", "select_to_char_right_helix"], @@ -386,6 +398,8 @@ ["home", "extend_to_line_start"], ["end", "extend_to_line_end"], + ["kp_home", "extend_to_line_start"], + ["kp_end", "extend_to_line_end"], ["w", "extend_next_word_start"], ["b", "extend_pre_word_start"], diff --git a/src/main.zig b/src/main.zig index d102022..6eac2e8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -255,19 +255,34 @@ pub fn main() anyerror!void { defer links.deinit(); var prev: ?*file_link.Dest = null; var line_next: ?usize = null; + var offset_next: ?usize = null; for (args.positional.trailing.items) |arg| { if (arg.len == 0) continue; if (!args.literal and arg[0] == '+') { - const line = try std.fmt.parseInt(usize, arg[1..], 10); - if (prev) |p| switch (p.*) { - .file => |*file| { - file.line = line; - continue; - }, - else => {}, - }; - line_next = line; + if (arg.len > 2 and arg[1] == 'b') { + const offset = try std.fmt.parseInt(usize, arg[2..], 10); + if (prev) |p| switch (p.*) { + .file => |*file| { + file.offset = offset; + continue; + }, + else => {}, + }; + offset_next = offset; + line_next = null; + } else { + const line = try std.fmt.parseInt(usize, arg[1..], 10); + if (prev) |p| switch (p.*) { + .file => |*file| { + file.line = line; + continue; + }, + else => {}, + }; + line_next = line; + offset_next = null; + } continue; } @@ -284,6 +299,15 @@ pub fn main() anyerror!void { else => {}, } } + if (offset_next) |offset| { + switch (curr.*) { + .file => |*file| { + file.offset = offset; + offset_next = null; + }, + else => {}, + } + } } var have_project = false; diff --git a/src/renderer/vaxis/input.zig b/src/renderer/vaxis/input.zig index c8bdcf2..c21b7ba 100644 --- a/src/renderer/vaxis/input.zig +++ b/src/renderer/vaxis/input.zig @@ -231,6 +231,25 @@ pub const utils = struct { vaxis.Key.f33 => "f33", vaxis.Key.f34 => "f34", vaxis.Key.f35 => "f35", + vaxis.Key.kp_decimal => "kp_decimal", + vaxis.Key.kp_divide => "kp_divide", + vaxis.Key.kp_multiply => "kp_multiply", + vaxis.Key.kp_subtract => "kp_subtract", + vaxis.Key.kp_add => "kp_add", + vaxis.Key.kp_enter => "kp_enter", + vaxis.Key.kp_equal => "kp_equal", + vaxis.Key.kp_separator => "kp_separator", + vaxis.Key.kp_left => "kp_left", + vaxis.Key.kp_right => "kp_right", + vaxis.Key.kp_up => "kp_up", + vaxis.Key.kp_down => "kp_down", + vaxis.Key.kp_page_up => "kp_page_up", + vaxis.Key.kp_page_down => "kp_page_down", + vaxis.Key.kp_home => "kp_home", + vaxis.Key.kp_end => "kp_end", + vaxis.Key.kp_insert => "kp_insert", + vaxis.Key.kp_delete => "kp_delete", + vaxis.Key.kp_begin => "kp_begin", vaxis.Key.media_play => "media_play", vaxis.Key.media_pause => "media_pause", vaxis.Key.media_play_pause => "media_play_pause", diff --git a/src/tui/editor.zig b/src/tui/editor.zig index e64168b..8252578 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -102,37 +102,41 @@ pub const CurSel = struct { } pub fn enable_selection(self: *Self, root: Buffer.Root, metrics: Buffer.Metrics) !*Selection { - return switch (tui.get_selection_style()) { - .normal => self.enable_selection_normal(), - .inclusive => try self.enable_selection_inclusive(root, metrics), - }; + self.selection = try self.to_selection(root, metrics); + return if (self.selection) |*sel| sel else unreachable; } pub fn enable_selection_normal(self: *Self) *Selection { - return if (self.selection) |*sel| - sel - else cod: { - self.selection = Selection.from_cursor(&self.cursor); - break :cod &self.selection.?; + self.selection = self.to_selection_normal(); + return if (self.selection) |*sel| sel else unreachable; + } + + pub fn to_selection(self: *const Self, root: Buffer.Root, metrics: Buffer.Metrics) !Selection { + return switch (tui.get_selection_style()) { + .normal => self.to_selection_normal(), + .inclusive => try self.to_selection_inclusive(root, metrics), }; } - fn enable_selection_inclusive(self: *Self, root: Buffer.Root, metrics: Buffer.Metrics) !*Selection { - return if (self.selection) |*sel| + fn to_selection_normal(self: *const Self) Selection { + return if (self.selection) |sel| sel else Selection.from_cursor(&self.cursor); + } + + fn to_selection_inclusive(self: *const Self, root: Buffer.Root, metrics: Buffer.Metrics) !Selection { + return if (self.selection) |sel| sel else cod: { - self.selection = Selection.from_cursor(&self.cursor); - try self.selection.?.end.move_right(root, metrics); - try self.cursor.move_right(root, metrics); - break :cod &self.selection.?; + var sel = Selection.from_cursor(&self.cursor); + try sel.end.move_right(root, metrics); + break :cod sel; }; } - fn to_inclusive_cursor(self: *const Self, root: Buffer.Root, metrics: Buffer.Metrics) !Cursor { - var res = self.cursor; + fn to_cursor_inclusive(self: *const Self, root: Buffer.Root, metrics: Buffer.Metrics) !Cursor { + var cursor = self.cursor; if (self.selection) |sel| if (!sel.is_reversed()) - try res.move_left(root, metrics); - return res; + try cursor.move_left(root, metrics); + return cursor; } pub fn disable_selection(self: *Self, root: Buffer.Root, metrics: Buffer.Metrics) void { @@ -170,9 +174,8 @@ pub const CurSel = struct { return sel; } - fn select_node(self: *Self, node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!void { - const range = node.getRange(); - self.selection = .{ + fn selection_from_range(range: syntax.Range, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!Selection { + return .{ .begin = .{ .row = range.start_point.row, .col = try root.pos_to_width(range.start_point.row, range.start_point.column, metrics), @@ -182,7 +185,23 @@ pub const CurSel = struct { .col = try root.pos_to_width(range.end_point.row, range.end_point.column, metrics), }, }; - self.cursor = self.selection.?.end; + } + + fn selection_from_node(node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!Selection { + return selection_from_range(node.getRange(), root, metrics); + } + + fn select_node(self: *Self, node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!void { + const sel = try selection_from_node(node, root, metrics); + self.selection = sel; + self.cursor = sel.end; + } + + fn select_parent_node(self: *Self, node: syntax.Node, root: Buffer.Root, metrics: Buffer.Metrics) error{NotFound}!syntax.Node { + const parent = node.getParent(); + if (parent.isNull()) return error.NotFound; + try self.select_node(parent, root, metrics); + return parent; } fn write(self: *const Self, writer: Buffer.MetaWriter) !void { @@ -1192,7 +1211,7 @@ pub const Editor = struct { fn get_rendered_cursor(self: *Self, style: anytype, cursel: anytype) !Cursor { return switch (style) { .normal => cursel.cursor, - .inclusive => try cursel.to_inclusive_cursor(try self.buf_root(), self.metrics), + .inclusive => try cursel.to_cursor_inclusive(try self.buf_root(), self.metrics), }; } @@ -4279,7 +4298,7 @@ pub const Editor = struct { } pub const selections_reverse_meta: Meta = .{ .description = "Reverse selection" }; - fn node_at_selection(self: *Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { + fn node_at_selection(self: *const Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { const syn = self.syntax orelse return error.Stop; const node = try syn.node_at_point_range(.{ .start_point = .{ @@ -4297,19 +4316,35 @@ pub const Editor = struct { return node; } - fn select_node_at_cursor(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - cursel.disable_selection(root, self.metrics); - const sel = (try cursel.enable_selection(root, self.metrics)).*; - return cursel.select_node(try self.node_at_selection(sel, root, metrics), root, metrics); + fn top_node_at_selection(self: *const Self, sel: Selection, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { + var node = try self.node_at_selection(sel, root, metrics); + if (node.isNull()) return node; + var parent = node.getParent(); + if (parent.isNull()) return node; + const node_sel = CurSel.selection_from_node(node, root, metrics) catch return node; + var parent_sel = CurSel.selection_from_node(parent, root, metrics) catch return node; + while (parent_sel.eql(node_sel)) { + node = parent; + parent = parent.getParent(); + parent_sel = CurSel.selection_from_node(parent, root, metrics) catch return node; + } + return node; + } + + fn top_node_at_cursel(self: *const Self, cursel: *const CurSel, root: Buffer.Root, metrics: Buffer.Metrics) error{Stop}!syntax.Node { + const sel = try cursel.to_selection(root, metrics); + return try self.top_node_at_selection(sel, root, metrics); } fn expand_selection_to_parent_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + var node = try self.top_node_at_selection(sel, root, metrics); if (node.isNull()) return error.Stop; - const parent = node.getParent(); - if (parent.isNull()) return error.Stop; - return cursel.select_node(parent, root, metrics); + var node_sel = try CurSel.selection_from_node(node, root, metrics); + if (!node_sel.eql(sel)) return cursel.select_node(node, root, metrics); + node = try cursel.select_parent_node(node, root, metrics); + while (cursel.selection.?.eql(sel)) + node = try cursel.select_parent_node(node, root, metrics); } pub fn expand_selection(self: *Self, _: Context) Result { @@ -4320,7 +4355,7 @@ pub const Editor = struct { try if (cursel.selection) |_| self.expand_selection_to_parent_node(root, cursel, self.metrics) else - self.select_node_at_cursor(root, cursel, self.metrics); + cursel.select_node(try self.top_node_at_cursel(cursel, root, self.metrics), root, self.metrics); self.clamp(); try self.send_editor_jump_destination(); } @@ -4362,8 +4397,7 @@ pub const Editor = struct { pub const shrink_selection_meta: Meta = .{ .description = "Shrink selection to first AST child node" }; fn select_next_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + const node = try self.top_node_at_cursel(cursel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_next_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4371,8 +4405,7 @@ pub const Editor = struct { } fn select_next_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + const node = try self.top_node_at_cursel(cursel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_next_named_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4386,19 +4419,17 @@ pub const Editor = struct { const root = try self.buf_root(); const cursel = self.get_primary(); cursel.check_selection(root, self.metrics); - if (cursel.selection) |_| - try if (unnamed) - self.select_next_sibling_node(root, cursel, self.metrics) - else - self.select_next_named_sibling_node(root, cursel, self.metrics); + try if (unnamed) + self.select_next_sibling_node(root, cursel, self.metrics) + else + self.select_next_named_sibling_node(root, cursel, self.metrics); self.clamp(); try self.send_editor_jump_destination(); } pub const select_next_sibling_meta: Meta = .{ .description = "Move selection to next AST sibling node" }; fn select_prev_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + const node = try self.top_node_at_cursel(cursel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_prev_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4406,8 +4437,7 @@ pub const Editor = struct { } fn select_prev_named_sibling_node(self: *Self, root: Buffer.Root, cursel: *CurSel, metrics: Buffer.Metrics) !void { - const sel = (try cursel.enable_selection(root, metrics)).*; - const node = try self.node_at_selection(sel, root, metrics); + const node = try self.top_node_at_cursel(cursel, root, metrics); if (node.isNull()) return error.Stop; const sibling = syntax.Node.externs.ts_node_prev_named_sibling(node); if (sibling.isNull()) return error.Stop; @@ -4421,11 +4451,10 @@ pub const Editor = struct { const root = try self.buf_root(); const cursel = self.get_primary(); cursel.check_selection(root, self.metrics); - if (cursel.selection) |_| - try if (unnamed) - self.select_prev_sibling_node(root, cursel, self.metrics) - else - self.select_prev_named_sibling_node(root, cursel, self.metrics); + try if (unnamed) + self.select_prev_sibling_node(root, cursel, self.metrics) + else + self.select_prev_named_sibling_node(root, cursel, self.metrics); self.clamp(); try self.send_editor_jump_destination(); } @@ -5477,6 +5506,28 @@ pub const Editor = struct { } pub const goto_line_and_column_meta: Meta = .{ .arguments = &.{ .integer, .integer } }; + pub fn goto_byte_offset(self: *Self, ctx: Context) Result { + try self.send_editor_jump_source(); + var offset: usize = 0; + if (try ctx.args.match(.{ + tp.extract(&offset), + })) { + // self.logger.print("goto: byte offset:{d}", .{ offset }); + } else return error.InvalidGotoByteOffsetArgument; + self.cancel_all_selections(); + const root = self.buf_root() catch return; + const eol_mode = self.buf_eol_mode() catch return; + const primary = self.get_primary(); + primary.cursor = root.byte_offset_to_line_and_col(offset, self.metrics, eol_mode); + if (self.view.is_visible(&primary.cursor)) + self.clamp() + else + try self.scroll_view_center(.{}); + try self.send_editor_jump_destination(); + self.need_render(); + } + pub const goto_byte_offset_meta: Meta = .{ .arguments = &.{.integer} }; + pub fn goto_definition(self: *Self, _: Context) Result { const file_path = self.file_path orelse return; const primary = self.get_primary(); diff --git a/src/tui/fonts.zig b/src/tui/fonts.zig index 35ef2d6..1c78176 100644 --- a/src/tui/fonts.zig +++ b/src/tui/fonts.zig @@ -224,6 +224,17 @@ pub const font_test_text: []const u8 = \\🙂‍↔ \\ \\ + \\你好世界 "Hello World" + \\一二三四五六七八九十 "123456789" + \\龍鳳麟龜 (dragon, phoenix, qilin, turtle) + \\Fullwidth numbers: 1234567890 + \\Fullwidth letters: ABCDEFG abcdefg + \\Fullwidth punctuation: !@#$%^&*() + \\Half-width (normal): ABC 123 + \\Full-width (double): ABC 123 + \\Punctuation: 。,、;:「」『』 + \\Symbols: ○●□■△▲☆★◇◆ + \\ \\ recommended fonts for terminals with no nerdfont fallback support (e.g. flow-gui): \\ \\ "IosevkaTerm Nerd Font" => https://github.com/ryanoasis/nerd-fonts/releases/download/v3.3.0/IosevkaTerm.zip diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index 6166b29..b8fc38b 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -150,10 +150,10 @@ pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool { }); return true; } else if (try m.match(.{ "navigate_complete", tp.extract(&same_file), tp.extract(&path), tp.extract(&goto_args), tp.extract(&line), tp.extract(&column) })) { - cmds.navigate_complete(self, same_file, path, goto_args, line, column) catch |e| return tp.exit_error(e, @errorReturnTrace()); + cmds.navigate_complete(self, same_file, path, goto_args, line, column, null) catch |e| return tp.exit_error(e, @errorReturnTrace()); return true; } else if (try m.match(.{ "navigate_complete", tp.extract(&same_file), tp.extract(&path), tp.extract(&goto_args), tp.null_, tp.null_ })) { - cmds.navigate_complete(self, same_file, path, goto_args, null, null) catch |e| return tp.exit_error(e, @errorReturnTrace()); + cmds.navigate_complete(self, same_file, path, goto_args, null, null, null) catch |e| return tp.exit_error(e, @errorReturnTrace()); return true; } return if (try self.floating_views.send(from_, m)) true else self.widgets.send(from_, m); @@ -349,6 +349,7 @@ const cmds = struct { var file_name: []const u8 = undefined; var line: ?i64 = null; var column: ?i64 = null; + var offset: ?i64 = null; var goto_args: []const u8 = &.{}; var iter = ctx.args.buf; @@ -370,6 +371,9 @@ const cmds = struct { } else if (std.mem.eql(u8, field_name, "goto")) { if (!try cbor.matchValue(&iter, cbor.extract_cbor(&goto_args))) return error.InvalidNavigateGotoArgument; + } else if (std.mem.eql(u8, field_name, "offset")) { + if (!try cbor.matchValue(&iter, cbor.extract(&offset))) + return error.InvalidNavigateOffsetArgument; } else { try cbor.skipValue(&iter); } @@ -392,7 +396,8 @@ const cmds = struct { if (tui.config().restore_last_cursor_position and !same_file and !have_editor_metadata and - line == null) + line == null and + offset == null) { const ctx_: struct { allocator: std.mem.Allocator, @@ -424,11 +429,11 @@ const cmds = struct { return; } - return cmds.navigate_complete(self, same_file, f, goto_args, line, column); + return cmds.navigate_complete(self, same_file, f, goto_args, line, column, offset); } pub const navigate_meta: Meta = .{ .arguments = &.{.object} }; - fn navigate_complete(self: *Self, same_file: bool, f: []const u8, goto_args: []const u8, line: ?i64, column: ?i64) Result { + fn navigate_complete(self: *Self, same_file: bool, f: []const u8, goto_args: []const u8, line: ?i64, column: ?i64, offset: ?i64) Result { if (!same_file) { if (self.get_active_editor()) |editor| { editor.send_editor_jump_source() catch {}; @@ -444,6 +449,10 @@ const cmds = struct { try command.executeName("scroll_view_center", .{}); if (column) |col| try command.executeName("goto_column", command.fmt(.{col})); + } else if (offset) |o| { + try command.executeName("goto_byte_offset", command.fmt(.{o})); + if (!same_file) + try command.executeName("scroll_view_center", .{}); } tui.need_render(); } @@ -608,8 +617,10 @@ const cmds = struct { pub fn delete_buffer(self: *Self, ctx: Ctx) Result { var file_path: []const u8 = undefined; - if (!(ctx.args.match(.{tp.extract(&file_path)}) catch false)) - return error.InvalidDeleteBufferArgument; + if (!(ctx.args.match(.{tp.extract(&file_path)}) catch false)) { + const editor = self.get_active_editor() orelse return error.InvalidDeleteBufferArgument; + file_path = editor.file_path orelse return error.InvalidDeleteBufferArgument; + } const buffer = self.buffer_manager.get_buffer_for_file(file_path) orelse return; if (buffer.is_dirty()) return tp.exit("unsaved changes"); diff --git a/src/tui/mode/mini/goto.zig b/src/tui/mode/mini/goto.zig index 6607dc6..963ec7f 100644 --- a/src/tui/mode/mini/goto.zig +++ b/src/tui/mode/mini/goto.zig @@ -1,17 +1,82 @@ +const fmt = @import("std").fmt; const command = @import("command"); const tui = @import("../../tui.zig"); +const Cursor = @import("../../editor.zig").Cursor; pub const Type = @import("numeric_input.zig").Create(@This()); pub const create = Type.create; +pub const ValueType = struct { + cursor: Cursor = .{}, + part: enum { row, col } = .row, +}; +pub const Separator = ':'; + pub fn name(_: *Type) []const u8 { return "#goto"; } -pub fn start(_: *Type) usize { - const editor = tui.get_active_editor() orelse return 1; - return editor.get_primary().cursor.row + 1; +pub fn start(_: *Type) ValueType { + const editor = tui.get_active_editor() orelse return .{}; + return .{ .cursor = editor.get_primary().cursor }; +} + +pub fn process_digit(self: *Type, digit: u8) void { + const part = if (self.input) |input| input.part else .row; + switch (part) { + .row => switch (digit) { + 0 => { + if (self.input) |*input| input.cursor.row = input.cursor.row * 10; + }, + 1...9 => { + if (self.input) |*input| { + input.cursor.row = input.cursor.row * 10 + digit; + } else { + self.input = .{ .cursor = .{ .row = digit } }; + } + }, + else => unreachable, + }, + .col => if (self.input) |*input| { + input.cursor.col = input.cursor.col * 10 + digit; + }, + } +} + +pub fn process_separator(self: *Type) void { + if (self.input) |*input| switch (input.part) { + .row => input.part = .col, + else => {}, + }; +} + +pub fn delete(self: *Type, input: *ValueType) void { + switch (input.part) { + .row => { + const newval = if (input.cursor.row < 10) 0 else input.cursor.row / 10; + if (newval == 0) self.input = null else input.cursor.row = newval; + }, + .col => { + const newval = if (input.cursor.col < 10) 0 else input.cursor.col / 10; + if (newval == 0) { + input.part = .row; + input.cursor.col = 0; + } else input.cursor.col = newval; + }, + } +} + +pub fn format_value(_: *Type, input: ?ValueType, buf: []u8) []const u8 { + return if (input) |value| blk: { + switch (value.part) { + .row => break :blk fmt.bufPrint(buf, "{d}", .{value.cursor.row}) catch "", + .col => if (value.cursor.col == 0) + break :blk fmt.bufPrint(buf, "{d}:", .{value.cursor.row}) catch "" + else + break :blk fmt.bufPrint(buf, "{d}:{d}", .{ value.cursor.row, value.cursor.col }) catch "", + } + } else ""; } pub const preview = goto; @@ -19,5 +84,9 @@ pub const apply = goto; pub const cancel = goto; fn goto(self: *Type, _: command.Context) void { - command.executeName("goto_line", command.fmt(.{self.input orelse self.start})) catch {}; + send_goto(if (self.input) |input| input.cursor else self.start.cursor); +} + +fn send_goto(cursor: Cursor) void { + command.executeName("goto_line_and_column", command.fmt(.{ cursor.row, cursor.col })) catch {}; } diff --git a/src/tui/mode/mini/goto_offset.zig b/src/tui/mode/mini/goto_offset.zig new file mode 100644 index 0000000..221f8a1 --- /dev/null +++ b/src/tui/mode/mini/goto_offset.zig @@ -0,0 +1,58 @@ +const fmt = @import("std").fmt; +const command = @import("command"); + +const tui = @import("../../tui.zig"); +const Cursor = @import("../../editor.zig").Cursor; + +pub const Type = @import("numeric_input.zig").Create(@This()); +pub const create = Type.create; + +pub const ValueType = struct { + cursor: Cursor = .{}, + offset: usize = 0, +}; + +pub fn name(_: *Type) []const u8 { + return "#goto byte"; +} + +pub fn start(_: *Type) ValueType { + const editor = tui.get_active_editor() orelse return .{}; + return .{ .cursor = editor.get_primary().cursor }; +} + +pub fn process_digit(self: *Type, digit: u8) void { + switch (digit) { + 0...9 => { + if (self.input) |*input| { + input.offset = input.offset * 10 + digit; + } else { + self.input = .{ .offset = digit }; + } + }, + else => unreachable, + } +} + +pub fn delete(self: *Type, input: *ValueType) void { + const newval = if (input.offset < 10) 0 else input.offset / 10; + if (newval == 0) self.input = null else input.offset = newval; +} + +pub fn format_value(_: *Type, input_: ?ValueType, buf: []u8) []const u8 { + return if (input_) |input| + fmt.bufPrint(buf, "{d}", .{input.offset}) catch "" + else + ""; +} + +pub const preview = goto; +pub const apply = goto; +pub const cancel = goto; + +fn goto(self: *Type, _: command.Context) void { + if (self.input) |input| + command.executeName("goto_byte_offset", command.fmt(.{input.offset})) catch {} + else + command.executeName("goto_line_and_column", command.fmt(.{ self.start.cursor.row, self.start.cursor.col })) catch {}; +} diff --git a/src/tui/mode/mini/numeric_input.zig b/src/tui/mode/mini/numeric_input.zig index 5b6be0f..7b6c30f 100644 --- a/src/tui/mode/mini/numeric_input.zig +++ b/src/tui/mode/mini/numeric_input.zig @@ -18,10 +18,12 @@ pub fn Create(options: type) type { const Commands = command.Collection(cmds); + const ValueType = if (@hasDecl(options, "ValueType")) options.ValueType else usize; + allocator: Allocator, buf: [30]u8 = undefined, - input: ?usize = null, - start: usize, + input: ?ValueType = null, + start: ValueType, ctx: command.Context, commands: Commands = undefined, @@ -31,7 +33,7 @@ pub fn Create(options: type) type { self.* = .{ .allocator = allocator, .ctx = .{ .args = try ctx.args.clone(allocator) }, - .start = 0, + .start = if (@hasDecl(options, "ValueType")) ValueType{} else 0, }; self.start = options.start(self); try self.commands.init(self); @@ -55,27 +57,42 @@ pub fn Create(options: type) type { fn update_mini_mode_text(self: *Self) void { if (tui.mini_mode()) |mini_mode| { - mini_mode.text = if (self.input) |linenum| - (fmt.bufPrint(&self.buf, "{d}", .{linenum}) catch "") - else - ""; + if (@hasDecl(options, "format_value")) { + mini_mode.text = options.format_value(self, self.input, &self.buf); + } else { + mini_mode.text = if (self.input) |linenum| + (fmt.bufPrint(&self.buf, "{d}", .{linenum}) catch "") + else + ""; + } mini_mode.cursor = tui.egc_chunk_width(mini_mode.text, 0, 1); } } fn insert_char(self: *Self, char: u8) void { - switch (char) { - '0' => { - if (self.input) |linenum| self.input = linenum * 10; - }, - '1'...'9' => { - const digit: usize = @intCast(char - '0'); - self.input = if (self.input) |x| x * 10 + digit else digit; - }, - else => {}, + const process_digit_ = if (@hasDecl(options, "process_digit")) options.process_digit else process_digit; + if (@hasDecl(options, "Separator")) { + switch (char) { + '0'...'9' => process_digit_(self, @intCast(char - '0')), + options.Separator => options.process_separator(self), + else => {}, + } + } else { + switch (char) { + '0'...'9' => process_digit_(self, @intCast(char - '0')), + else => {}, + } } } + fn process_digit(self: *Self, digit: u8) void { + self.input = switch (digit) { + 0 => if (self.input) |value| value * 10 else 0, + 1...9 => if (self.input) |x| x * 10 + digit else digit, + else => unreachable, + }; + } + fn insert_bytes(self: *Self, bytes: []const u8) void { for (bytes) |c| self.insert_char(c); } @@ -101,9 +118,13 @@ pub fn Create(options: type) type { pub const mini_mode_cancel_meta: Meta = .{ .description = "Cancel input" }; pub fn mini_mode_delete_backwards(self: *Self, _: Ctx) Result { - if (self.input) |linenum| { - const newval = if (linenum < 10) 0 else linenum / 10; - self.input = if (newval == 0) null else newval; + if (self.input) |*input| { + if (@hasDecl(options, "delete")) { + options.delete(self, input); + } else { + const newval = if (input.* < 10) 0 else input.* / 10; + self.input = if (newval == 0) null else newval; + } self.update_mini_mode_text(); options.preview(self, self.ctx); } diff --git a/src/tui/mode/mini/open_file.zig b/src/tui/mode/mini/open_file.zig index 07f70e4..fc13d55 100644 --- a/src/tui/mode/mini/open_file.zig +++ b/src/tui/mode/mini/open_file.zig @@ -37,9 +37,9 @@ pub fn select(self: *Type) void { var buf = std.ArrayList(u8).init(self.allocator); defer buf.deinit(); const file_path = project_manager.expand_home(&buf, self.file_path.items); - command.executeName("exit_mini_mode", .{}) catch {}; if (root.is_directory(file_path)) tp.self_pid().send(.{ "cmd", "change_project", .{file_path} }) catch {} else if (file_path.len > 0) tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_path } }) catch {}; + command.executeName("exit_mini_mode", .{}) catch {}; } diff --git a/src/tui/mode/overlay/completion_palette.zig b/src/tui/mode/overlay/completion_palette.zig index eac3856..defc212 100644 --- a/src/tui/mode/overlay/completion_palette.zig +++ b/src/tui/mode/overlay/completion_palette.zig @@ -117,6 +117,7 @@ fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { } const CompletionItemKind = enum(u8) { + None = 0, Text = 1, Method = 2, Function = 3, @@ -146,6 +147,7 @@ const CompletionItemKind = enum(u8) { fn kind_icon(kind: CompletionItemKind) []const u8 { return switch (kind) { + .None => " ", .Text => "󰊄", .Method => "", .Function => "󰊕", diff --git a/src/tui/mode/vim.zig b/src/tui/mode/vim.zig index e88cd7d..81839b9 100644 --- a/src/tui/mode/vim.zig +++ b/src/tui/mode/vim.zig @@ -51,6 +51,31 @@ const cmds_ = struct { } pub const @"e!_meta": Meta = .{ .description = "e! (force reload current file)" }; + pub fn bd(_: *void, _: Ctx) Result { + try cmd("close_file", .{}); + } + pub const bd_meta: Meta = .{ .description = "bd (Close file)" }; + + pub fn bw(_: *void, _: Ctx) Result { + try cmd("delete_buffer", .{}); + } + pub const bw_meta: Meta = .{ .description = "bw (Delete buffer)" }; + + pub fn bnext(_: *void, _: Ctx) Result { + try cmd("next_tab", .{}); + } + pub const bnext_meta: Meta = .{ .description = "bnext (Next buffer/tab)" }; + + pub fn bprevious(_: *void, _: Ctx) Result { + try cmd("next_tab", .{}); + } + pub const bprevious_meta: Meta = .{ .description = "bprevious (Previous buffer/tab)" }; + + pub fn ls(_: *void, _: Ctx) Result { + try cmd("switch_buffers", .{}); + } + pub const ls_meta: Meta = .{ .description = "ls (List/switch buffers)" }; + pub fn move_begin_or_add_integer_argument_zero(_: *void, _: Ctx) Result { return if (@import("keybind").current_integer_argument()) |_| command.executeName("add_integer_argument_digit", command.fmt(.{0})) diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 0577836..9955208 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -1034,9 +1034,31 @@ const cmds = struct { pub const find_in_files_meta: Meta = .{ .description = "Find in files" }; pub fn goto(self: *Self, ctx: Ctx) Result { - return enter_mini_mode(self, @import("mode/mini/goto.zig"), ctx); + var line: usize = undefined; + var column: usize = undefined; + return if (try ctx.args.match(.{tp.extract(&line)})) + command.executeName("goto_line", command.fmt(.{line})) + else if (try ctx.args.match(.{ tp.extract(&line), tp.extract(&column) })) + command.executeName("goto_line_and_column", command.fmt(.{ line, column })) + else + enter_mini_mode(self, @import("mode/mini/goto.zig"), ctx); } - pub const goto_meta: Meta = .{ .description = "Goto line" }; + pub const goto_meta: Meta = .{ + .description = "Goto line", + .arguments = &.{ .integer, .integer }, + }; + + pub fn goto_offset(self: *Self, ctx: Ctx) Result { + var offset: usize = undefined; + return if (try ctx.args.match(.{tp.extract(&offset)})) + command.executeName("goto_byte_offset", command.fmt(.{offset})) + else + enter_mini_mode(self, @import("mode/mini/goto_offset.zig"), ctx); + } + pub const goto_offset_meta: Meta = .{ + .description = "Goto byte offset", + .arguments = &.{.integer}, + }; pub fn move_to_char(self: *Self, ctx: Ctx) Result { return enter_mini_mode(self, @import("mode/mini/move_to_char.zig"), ctx); diff --git a/src/win32/DwriteRenderer.zig b/src/win32/DwriteRenderer.zig index 8ad34a0..0a9b20d 100644 --- a/src/win32/DwriteRenderer.zig +++ b/src/win32/DwriteRenderer.zig @@ -75,6 +75,7 @@ pub fn render( self: *const DwriteRenderer, font: Font, utf8: []const u8, + double_width: bool, ) void { var utf16_buf: [10]u16 = undefined; const utf16_len = std.unicode.utf8ToUtf16Le(&utf16_buf, utf8) catch unreachable; @@ -85,7 +86,10 @@ pub fn render( const rect: win32.D2D_RECT_F = .{ .left = 0, .top = 0, - .right = @floatFromInt(font.cell_size.x), + .right = if (double_width) + @as(f32, @floatFromInt(font.cell_size.x)) * 2 + else + @as(f32, @floatFromInt(font.cell_size.x)), .bottom = @floatFromInt(font.cell_size.y), }; self.render_target.BeginDraw(); @@ -96,7 +100,7 @@ pub fn render( self.render_target.DrawText( @ptrCast(utf16.ptr), @intCast(utf16.len), - font.text_format, + if (double_width) font.text_format_double else font.text_format_single, &rect, &self.white_brush.ID2D1Brush, .{}, diff --git a/src/win32/GlyphIndexCache.zig b/src/win32/GlyphIndexCache.zig index 02bc281..85fb686 100644 --- a/src/win32/GlyphIndexCache.zig +++ b/src/win32/GlyphIndexCache.zig @@ -5,9 +5,15 @@ const Node = struct { prev: ?u32, next: ?u32, codepoint: ?u21, + right_half: ?bool, }; -map: std.AutoHashMapUnmanaged(u21, u32) = .{}, +const MapKey = struct { + codepoint: u21, + right_half: bool, +}; + +map: std.AutoHashMapUnmanaged(MapKey, u32) = .{}, nodes: []Node, front: u32, back: u32, @@ -25,13 +31,14 @@ pub fn init(allocator: std.mem.Allocator, capacity: u32) error{OutOfMemory}!Glyp pub fn clearRetainingCapacity(self: *GlyphIndexCache) void { self.map.clearRetainingCapacity(); - self.nodes[0] = .{ .prev = null, .next = 1, .codepoint = null }; - self.nodes[self.nodes.len - 1] = .{ .prev = @intCast(self.nodes.len - 2), .next = null, .codepoint = null }; + self.nodes[0] = .{ .prev = null, .next = 1, .codepoint = null, .right_half = null }; + self.nodes[self.nodes.len - 1] = .{ .prev = @intCast(self.nodes.len - 2), .next = null, .codepoint = null, .right_half = null }; for (self.nodes[1 .. self.nodes.len - 1], 1..) |*node, index| { node.* = .{ .prev = @intCast(index - 1), .next = @intCast(index + 1), .codepoint = null, + .right_half = null, }; } self.front = 0; @@ -51,12 +58,12 @@ const Reserved = struct { index: u32, replaced: ?u21, }; -pub fn reserve(self: *GlyphIndexCache, allocator: std.mem.Allocator, codepoint: u21) error{OutOfMemory}!union(enum) { +pub fn reserve(self: *GlyphIndexCache, allocator: std.mem.Allocator, codepoint: u21, right_half: bool) error{OutOfMemory}!union(enum) { newly_reserved: Reserved, already_reserved: u32, } { { - const entry = try self.map.getOrPut(allocator, codepoint); + const entry = try self.map.getOrPut(allocator, .{ .codepoint = codepoint, .right_half = right_half }); if (entry.found_existing) { self.moveToBack(entry.value_ptr.*); return .{ .already_reserved = entry.value_ptr.* }; @@ -69,7 +76,7 @@ pub fn reserve(self: *GlyphIndexCache, allocator: std.mem.Allocator, codepoint: const replaced = self.nodes[self.front].codepoint; self.nodes[self.front].codepoint = codepoint; if (replaced) |r| { - const removed = self.map.remove(r); + const removed = self.map.remove(.{ .codepoint = r, .right_half = self.nodes[self.front].right_half orelse false }); std.debug.assert(removed); } const save_front = self.front; diff --git a/src/win32/d3d11.zig b/src/win32/d3d11.zig index 8e63b05..b919d3e 100644 --- a/src/win32/d3d11.zig +++ b/src/win32/d3d11.zig @@ -149,7 +149,7 @@ pub const WindowState = struct { } // TODO: this should take a utf8 graphme instead - pub fn generateGlyph(state: *WindowState, font: Font, codepoint: u21) u32 { + pub fn generateGlyph(state: *WindowState, font: Font, codepoint: u21, kind: enum { single, left, right }) u32 { // for now we'll just use 1 texture and leverage the entire thing const texture_cell_count: XY(u16) = getD3d11TextureMaxCellCount(font.cell_size); const texture_cell_count_total: u32 = @@ -184,16 +184,27 @@ pub const WindowState = struct { break :blk &(state.glyph_index_cache.?); }; + const right_half: bool = switch (kind) { + .single, .left => false, + .right => true, + }; + switch (glyph_index_cache.reserve( global.glyph_cache_arena.allocator(), codepoint, + right_half, ) catch |e| oom(e)) { .newly_reserved => |reserved| { // var render_success = false; // defer if (!render_success) state.glyph_index_cache.remove(reserved.index); const pos: XY(u16) = cellPosFromIndex(reserved.index, texture_cell_count.x); const coord = coordFromCellPos(font.cell_size, pos); - const staging = global.staging_texture.update(font.cell_size); + const staging_size: XY(u16) = .{ + // twice the width to handle double-wide glyphs + .x = font.cell_size.x * 2, + .y = font.cell_size.y, + }; + const staging = global.staging_texture.update(staging_size); var utf8_buf: [7]u8 = undefined; const utf8_len: u3 = std.unicode.utf8Encode(codepoint, &utf8_buf) catch |e| std.debug.panic( "todo: handle invalid codepoint {} (0x{0x}) ({s})", @@ -202,12 +213,16 @@ pub const WindowState = struct { staging.text_renderer.render( font, utf8_buf[0..utf8_len], + switch (kind) { + .single => false, + .left, .right => true, + }, ); const box: win32.D3D11_BOX = .{ - .left = 0, + .left = if (right_half) font.cell_size.x else 0, .top = 0, .front = 0, - .right = font.cell_size.x, + .right = if (right_half) font.cell_size.x * 2 else font.cell_size.x, .bottom = font.cell_size.y, .back = 1, }; @@ -289,7 +304,7 @@ pub fn paint( } const copy_col_count: u16 = @min(col_count, shader_col_count); - const blank_space_glyph_index = state.generateGlyph(font, ' '); + const blank_space_glyph_index = state.generateGlyph(font, ' ', .single); const cell_count: u32 = @as(u32, shader_col_count) * @as(u32, shader_row_count); state.shader_cells.updateCount(cell_count); diff --git a/src/win32/dwrite.zig b/src/win32/dwrite.zig index 0bd85f2..23129e7 100644 --- a/src/win32/dwrite.zig +++ b/src/win32/dwrite.zig @@ -23,11 +23,13 @@ pub fn init() void { } pub const Font = struct { - text_format: *win32.IDWriteTextFormat, + text_format_single: *win32.IDWriteTextFormat, + text_format_double: *win32.IDWriteTextFormat, cell_size: XY(u16), pub fn init(dpi: u32, size: f32, face: *const FontFace) Font { - var text_format: *win32.IDWriteTextFormat = undefined; + var text_format_single: *win32.IDWriteTextFormat = undefined; + { const hr = global.dwrite_factory.CreateTextFormat( face.ptr(), @@ -37,14 +39,43 @@ pub const Font = struct { .NORMAL, // stretch win32.scaleDpi(f32, size, dpi), win32.L(""), // locale - &text_format, + &text_format_single, ); if (hr < 0) std.debug.panic( "CreateTextFormat '{}' height {d} failed, hresult=0x{x}", .{ std.unicode.fmtUtf16Le(face.slice()), size, @as(u32, @bitCast(hr)) }, ); } - errdefer _ = text_format.IUnknown.Release(); + errdefer _ = text_format_single.IUnknown.Release(); + + var text_format_double: *win32.IDWriteTextFormat = undefined; + { + const hr = global.dwrite_factory.CreateTextFormat( + face.ptr(), + null, + .NORMAL, //weight + .NORMAL, // style + .NORMAL, // stretch + win32.scaleDpi(f32, size, dpi), + win32.L(""), // locale + &text_format_double, + ); + if (hr < 0) std.debug.panic( + "CreateTextFormat '{}' height {d} failed, hresult=0x{x}", + .{ std.unicode.fmtUtf16Le(face.slice()), size, @as(u32, @bitCast(hr)) }, + ); + } + errdefer _ = text_format_double.IUnknown.Release(); + + { + const hr = text_format_double.SetTextAlignment(win32.DWRITE_TEXT_ALIGNMENT_CENTER); + if (hr < 0) fatalHr("SetTextAlignment", hr); + } + + { + const hr = text_format_double.SetParagraphAlignment(win32.DWRITE_PARAGRAPH_ALIGNMENT_CENTER); + if (hr < 0) fatalHr("SetParagraphAlignment", hr); + } const cell_size: XY(u16) = blk: { var text_layout: *win32.IDWriteTextLayout = undefined; @@ -52,7 +83,7 @@ pub const Font = struct { const hr = global.dwrite_factory.CreateTextLayout( win32.L("█"), 1, - text_format, + text_format_single, std.math.floatMax(f32), std.math.floatMax(f32), &text_layout, @@ -73,13 +104,15 @@ pub const Font = struct { }; return .{ - .text_format = text_format, + .text_format_single = text_format_single, + .text_format_double = text_format_double, .cell_size = cell_size, }; } pub fn deinit(self: *Font) void { - _ = self.text_format.IUnknown.Release(); + _ = self.text_format_single.IUnknown.Release(); + _ = self.text_format_double.IUnknown.Release(); self.* = undefined; } diff --git a/src/win32/gui.zig b/src/win32/gui.zig index 6a6cad4..b553e0d 100644 --- a/src/win32/gui.zig +++ b/src/win32/gui.zig @@ -1081,19 +1081,31 @@ fn WndProc( global.render_cells_arena.allocator(), global.screen.buf.len, ) catch |e| oom(e); + var prev_width: usize = 1; + var prev_cell: render.Cell = undefined; + var prev_codepoint: u21 = undefined; for (global.screen.buf, global.render_cells.items) |*screen_cell, *render_cell| { - const codepoint = if (std.unicode.utf8ValidateSlice(screen_cell.char.grapheme)) + const width = screen_cell.char.width; + // temporary workaround, ignore multi-codepoint graphemes + const codepoint = if (screen_cell.char.grapheme.len > 4) + std.unicode.replacement_character + else if (std.unicode.utf8ValidateSlice(screen_cell.char.grapheme)) std.unicode.wtf8Decode(screen_cell.char.grapheme) catch std.unicode.replacement_character else std.unicode.replacement_character; - render_cell.* = .{ - .glyph_index = state.render_state.generateGlyph( - font, - codepoint, - ), - .background = renderColorFromVaxis(screen_cell.style.bg), - .foreground = renderColorFromVaxis(screen_cell.style.fg), - }; + if (prev_width > 1) { + render_cell.* = prev_cell; + render_cell.glyph_index = state.render_state.generateGlyph(font, prev_codepoint, .right); + } else { + render_cell.* = .{ + .glyph_index = state.render_state.generateGlyph(font, codepoint, if (width == 1) .single else .left), + .background = renderColorFromVaxis(screen_cell.style.bg), + .foreground = renderColorFromVaxis(screen_cell.style.fg), + }; + } + prev_width = width; + prev_cell = render_cell.*; + prev_codepoint = codepoint; } render.paint( &state.render_state, diff --git a/test/tests_buffer.zig b/test/tests_buffer.zig index 2ba6af4..95ab500 100644 --- a/test/tests_buffer.zig +++ b/test/tests_buffer.zig @@ -190,6 +190,14 @@ test "get_byte_pos" { try std.testing.expectEqual(33, try buffer.root.get_byte_pos(.{ .row = 4, .col = 0 }, metrics(), eol_mode)); try std.testing.expectEqual(66, try buffer.root.get_byte_pos(.{ .row = 8, .col = 0 }, metrics(), eol_mode)); try std.testing.expectEqual(97, try buffer.root.get_byte_pos(.{ .row = 11, .col = 2 }, metrics(), eol_mode)); + + eol_mode = .crlf; + try std.testing.expectEqual(0, try buffer.root.get_byte_pos(.{ .row = 0, .col = 0 }, metrics(), eol_mode)); + try std.testing.expectEqual(10, try buffer.root.get_byte_pos(.{ .row = 1, .col = 0 }, metrics(), eol_mode)); + try std.testing.expectEqual(12, try buffer.root.get_byte_pos(.{ .row = 1, .col = 2 }, metrics(), eol_mode)); + try std.testing.expectEqual(37, try buffer.root.get_byte_pos(.{ .row = 4, .col = 0 }, metrics(), eol_mode)); + try std.testing.expectEqual(74, try buffer.root.get_byte_pos(.{ .row = 8, .col = 0 }, metrics(), eol_mode)); + try std.testing.expectEqual(108, try buffer.root.get_byte_pos(.{ .row = 11, .col = 2 }, metrics(), eol_mode)); } test "delete_bytes" { @@ -406,3 +414,44 @@ test "get_from_pos" { const result3 = buffer.root.get_from_pos(.{ .row = 1, .col = 5 }, &result_buf, metrics()); try std.testing.expectEqualDeep(result3[0 .. line1.len - 4], line1[4..]); } + +test "byte_offset_to_line_and_col" { + const doc: []const u8 = + \\All your + \\ropes + \\are belong to + \\us! + \\All your + \\ropes + \\are belong to + \\us! + \\All your + \\ropes + \\are belong to + \\us! + ; + var eol_mode: Buffer.EolMode = .lf; + var sanitized: bool = false; + const buffer = try Buffer.create(a); + defer buffer.deinit(); + buffer.update(try buffer.load_from_string(doc, &eol_mode, &sanitized)); + + try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 0 }, buffer.root.byte_offset_to_line_and_col(0, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 8 }, buffer.root.byte_offset_to_line_and_col(8, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 1, .col = 0 }, buffer.root.byte_offset_to_line_and_col(9, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 1, .col = 2 }, buffer.root.byte_offset_to_line_and_col(11, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 4, .col = 0 }, buffer.root.byte_offset_to_line_and_col(33, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 8, .col = 0 }, buffer.root.byte_offset_to_line_and_col(66, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 11, .col = 2 }, buffer.root.byte_offset_to_line_and_col(97, metrics(), eol_mode)); + + eol_mode = .crlf; + + try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 0 }, buffer.root.byte_offset_to_line_and_col(0, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 8 }, buffer.root.byte_offset_to_line_and_col(8, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 0, .col = 8 }, buffer.root.byte_offset_to_line_and_col(9, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 1, .col = 0 }, buffer.root.byte_offset_to_line_and_col(10, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 1, .col = 2 }, buffer.root.byte_offset_to_line_and_col(12, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 4, .col = 0 }, buffer.root.byte_offset_to_line_and_col(37, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 8, .col = 0 }, buffer.root.byte_offset_to_line_and_col(74, metrics(), eol_mode)); + try std.testing.expectEqual(Buffer.Cursor{ .row = 11, .col = 2 }, buffer.root.byte_offset_to_line_and_col(108, metrics(), eol_mode)); +}