Merge branch 'master' into zig-0.15

This commit is contained in:
CJ van den Berg 2025-09-24 18:33:33 +02:00
commit 96e8100373
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
24 changed files with 894 additions and 145 deletions

175
contrib/make_nightly_build Executable file
View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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 } });

View file

@ -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"],

View file

@ -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"],

View file

@ -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;

View file

@ -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",

View file

@ -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();

View file

@ -224,6 +224,17 @@ pub const font_test_text: []const u8 =
\\🙂‍↔
\\
\\
\\你好世界 "Hello World"
\\一二三四五六七八九十 "123456789"
\\龍鳳麟龜 (dragon, phoenix, qilin, turtle)
\\Fullwidth numbers:
\\Fullwidth letters:  
\\Fullwidth punctuation:
\\Half-width (normal): ABC 123
\\Full-width (double):  
\\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

View file

@ -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");

View file

@ -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 {};
}

View file

@ -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 {};
}

View file

@ -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);
}

View file

@ -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 {};
}

View file

@ -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 => "󰊕",

View file

@ -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}))

View file

@ -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);

View file

@ -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,
.{},

View file

@ -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;

View file

@ -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);

View file

@ -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;
}

View file

@ -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,

View file

@ -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));
}