diff --git a/src/keybind/builtin/flow.json b/src/keybind/builtin/flow.json index 9e1141a..69a8092 100644 --- a/src/keybind/builtin/flow.json +++ b/src/keybind/builtin/flow.json @@ -19,6 +19,7 @@ ["ctrl+c", "copy"], ["ctrl+v", "system_paste"], ["ctrl+u", "pop_cursor"], + ["ctrl+k>m", "change_file_type"], ["ctrl+k>ctrl+u", "delete_to_begin"], ["ctrl+k>ctrl+k", "delete_to_end"], ["ctrl+k>ctrl+d", "move_cursor_next_match"], diff --git a/src/tui/editor.zig b/src/tui/editor.zig index a1d0040..658cf96 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -4282,6 +4282,39 @@ pub const Editor = struct { self.syntax_report_timing = !self.syntax_report_timing; } pub const toggle_syntax_timing_meta = .{ .description = "Toggle tree-sitter timing reports" }; + + pub fn set_file_type(self: *Self, ctx: Context) Result { + var file_type: []const u8 = undefined; + if (!try ctx.args.match(.{tp.extract(&file_type)})) + return error.InvalidArgument; + + if (self.syntax) |syn| syn.destroy(); + self.syntax_last_rendered_root = null; + self.syntax_refresh_full = true; + self.syntax_incremental_reparse = false; + + self.syntax = syntax: { + var content = std.ArrayList(u8).init(self.allocator); + defer content.deinit(); + const root = try self.buf_root(); + try root.store(content.writer(), try self.buf_eol_mode()); + const syn = syntax.create_file_type(self.allocator, file_type) catch null; + if (syn) |syn_| if (self.file_path) |file_path| + project_manager.did_open(file_path, syn_.file_type, self.lsp_version, try content.toOwnedSlice()) catch |e| + self.logger.print("project_manager.did_open failed: {any}", .{e}); + break :syntax syn; + }; + self.syntax_no_render = tp.env.get().is("no-syntax"); + self.syntax_report_timing = tp.env.get().is("syntax-report-timing"); + + const ftn = if (self.syntax) |syn| syn.file_type.name else "text"; + const fti = if (self.syntax) |syn| syn.file_type.icon else "🖹"; + const ftc = if (self.syntax) |syn| syn.file_type.color else 0x000000; + const file_exists = if (self.buffer) |b| b.file_exists else false; + try self.send_editor_open(self.file_path orelse "", file_exists, ftn, fti, ftc); + self.logger.print("file type {s}", .{file_type}); + } + pub const set_file_type_meta = .{ .arguments = &.{.string} }; }; pub fn create(allocator: Allocator, parent: Widget) !Widget { diff --git a/src/tui/mode/overlay/file_type_palette.zig b/src/tui/mode/overlay/file_type_palette.zig new file mode 100644 index 0000000..3464cbd --- /dev/null +++ b/src/tui/mode/overlay/file_type_palette.zig @@ -0,0 +1,147 @@ +const std = @import("std"); +const cbor = @import("cbor"); +const tp = @import("thespian"); +const syntax = @import("syntax"); + +const Widget = @import("../../Widget.zig"); +const tui = @import("../../tui.zig"); +const mainview = @import("../../mainview.zig"); + +pub const Type = @import("palette.zig").Create(@This()); + +pub const label = "Select file type"; +pub const name = " file type"; +pub const description = "file type"; + +pub const Entry = struct { + label: []const u8, + name: []const u8, + icon: []const u8, + color: u24, +}; + +pub const Match = struct { + name: []const u8, + score: i32, + matches: []const usize, +}; + +var previous_file_type: ?[]const u8 = null; + +pub fn load_entries(palette: *Type) !usize { + var longest_hint: usize = 0; + var idx: usize = 0; + previous_file_type = blk: { + if (tui.current().mainview.dynamic_cast(mainview)) |mv_| if (mv_.get_editor()) |editor| { + if (editor.syntax) |editor_syntax| break :blk editor_syntax.file_type.name; + }; + break :blk null; + }; + + for (syntax.FileType.file_types) |file_type| { + idx += 1; + (try palette.entries.addOne()).* = .{ + .label = file_type.description, + .name = file_type.name, + .icon = file_type.icon, + .color = file_type.color, + }; + if (previous_file_type) |file_type_name| if (std.mem.eql(u8, file_type.name, file_type_name)) { + palette.initial_selected = idx; + }; + longest_hint = @max(longest_hint, file_type.name.len); + } + return longest_hint; +} + +pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { + var value = std.ArrayList(u8).init(palette.allocator); + defer value.deinit(); + const writer = value.writer(); + try cbor.writeValue(writer, entry.label); + try cbor.writeValue(writer, entry.icon); + try cbor.writeValue(writer, entry.color); + try cbor.writeValue(writer, entry.name); + try cbor.writeValue(writer, matches orelse &[_]usize{}); + try palette.menu.add_item_with_handler(value.items, select); + palette.items += 1; +} + +pub fn on_render_menu(_: *Type, button: *Type.ButtonState, theme: *const Widget.Theme, selected: bool) bool { + const style_base = theme.editor_widget; + const style_label = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor_widget; + const style_hint = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_label; + button.plane.set_base_style(style_base); + button.plane.erase(); + button.plane.home(); + button.plane.set_style(style_label); + if (button.active or button.hover or selected) { + _ = button.plane.fill_width(" ", .{}) catch {}; + button.plane.home(); + } + + button.plane.set_style(style_hint); + const pointer = if (selected) "⏵" else " "; + _ = button.plane.print("{s}", .{pointer}) catch {}; + + var iter = button.opts.label; + var description_: []const u8 = undefined; + var icon: []const u8 = undefined; + var color: u24 = undefined; + if (!(cbor.matchString(&iter, &description_) catch false)) @panic("invalid file_type description"); + if (!(cbor.matchString(&iter, &icon) catch false)) @panic("invalid file_type icon"); + if (!(cbor.matchInt(u24, &iter, &color) catch false)) @panic("invalid file_type color"); + render_file_icon(button, icon, color); + button.plane.set_style(style_label); + _ = button.plane.print(" {s} ", .{description_}) catch {}; + + var name_: []const u8 = undefined; + if (!(cbor.matchString(&iter, &name_) catch false)) + name_ = ""; + button.plane.set_style(style_hint); + _ = button.plane.print_aligned_right(0, "{s} ", .{name_}) catch {}; + + var index: usize = 0; + var len = cbor.decodeArrayHeader(&iter) catch return false; + while (len > 0) : (len -= 1) { + if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { + render_match_cell(button, 0, index + 4, theme) catch break; + } else break; + } + return false; +} + +fn render_match_cell(button: *Type.ButtonState, y: usize, x: usize, theme: *const Widget.Theme) !void { + button.plane.cursor_move_yx(@intCast(y), @intCast(x)) catch return; + var cell = button.plane.cell_init(); + _ = button.plane.at_cursor_cell(&cell) catch return; + cell.set_style(theme.editor_match); + _ = button.plane.putc(&cell) catch {}; +} + +fn render_file_icon(button: *Type.ButtonState, icon: []const u8, color: u24) void { + var cell = button.plane.cell_init(); + _ = button.plane.at_cursor_cell(&cell) catch return; + if (!(color == 0xFFFFFF or color == 0x000000 or color == 0x000001)) { + cell.set_fg_rgb(@intCast(color)) catch {}; + } + _ = button.plane.cell_load(&cell, icon) catch {}; + _ = button.plane.putc(&cell) catch {}; + button.plane.cursor_move_rel(0, 1) catch {}; +} + +fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { + var description_: []const u8 = undefined; + var icon: []const u8 = undefined; + var color: u24 = undefined; + var name_: []const u8 = undefined; + var iter = button.opts.label; + if (!(cbor.matchString(&iter, &description_) catch false)) return; + if (!(cbor.matchString(&iter, &icon) catch false)) return; + if (!(cbor.matchInt(u24, &iter, &color) catch false)) return; + if (!(cbor.matchString(&iter, &name_) catch false)) return; + if (previous_file_type) |prev| if (std.mem.eql(u8, prev, name_)) + return; + tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("file_type_palette", e); + tp.self_pid().send(.{ "cmd", "set_file_type", .{name_} }) catch |e| menu.*.opts.ctx.logger.err("file_type_palette", e); +} diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig index 09926c1..0934e0b 100644 --- a/src/tui/mode/overlay/palette.zig +++ b/src/tui/mode/overlay/palette.zig @@ -54,7 +54,7 @@ pub fn Create(options: type) type { .modal = try ModalBackground.create(*Self, allocator, tui.current().mainview, .{ .ctx = self }), .menu = try Menu.create(*Self, allocator, tui.current().mainview, .{ .ctx = self, - .on_render = on_render_menu, + .on_render = if (@hasDecl(options, "on_render_menu")) options.on_render_menu else on_render_menu, .on_resize = on_resize_menu, .on_scroll = EventHandler.bind(self, Self.on_scroll), .on_click4 = mouse_click_button4, diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 15d68b3..fac0dc9 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -759,6 +759,11 @@ const cmds = struct { } pub const change_theme_meta = .{ .description = "Select color theme" }; + pub fn change_file_type(self: *Self, _: Ctx) Result { + return self.enter_overlay_mode(@import("mode/overlay/file_type_palette.zig").Type); + } + pub const change_file_type_meta = .{ .description = "Change file type" }; + pub fn exit_overlay_mode(self: *Self, _: Ctx) Result { if (self.input_mode_outer == null) return; if (self.input_mode) |*mode| mode.deinit();