From 2897d8d7450814a62e8c1cec365fd06fb3221149 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 3 Jul 2025 16:28:55 +0200 Subject: [PATCH] feat: add command to edit file type configuration files --- build.zig | 8 + src/file_type_config.zig | 27 +++ src/tui/mainview.zig | 61 ++++++ src/tui/mode/overlay/file_type_palette.zig | 234 +++++++++++---------- src/tui/tui.zig | 9 +- 5 files changed, 219 insertions(+), 120 deletions(-) create mode 100644 src/file_type_config.zig diff --git a/build.zig b/build.zig index c508451..cfc5fc5 100644 --- a/build.zig +++ b/build.zig @@ -317,6 +317,13 @@ pub fn build_exe( }, }); + const file_type_config_mod = b.createModule(.{ + .root_source_file = b.path("src/file_type_config.zig"), + .imports = &.{ + .{ .name = "syntax", .module = syntax_mod }, + }, + }); + const log_mod = b.createModule(.{ .root_source_file = b.path("src/log.zig"), .imports = &.{ @@ -531,6 +538,7 @@ pub fn build_exe( .{ .name = "cbor", .module = cbor_mod }, .{ .name = "config", .module = config_mod }, .{ .name = "gui_config", .module = gui_config_mod }, + .{ .name = "file_type_config", .module = file_type_config_mod }, .{ .name = "log", .module = log_mod }, .{ .name = "command", .module = command_mod }, .{ .name = "EventHandler", .module = EventHandler_mod }, diff --git a/src/file_type_config.zig b/src/file_type_config.zig new file mode 100644 index 0000000..b588f9a --- /dev/null +++ b/src/file_type_config.zig @@ -0,0 +1,27 @@ +description: ?[]const u8 = null, +extensions: ?[]const []const u8 = null, +icon: ?[]const u8 = null, +color: ?u24 = null, +comment: ?[]const u8 = null, +formatter: ?[]const []const u8 = null, +language_server: ?[]const []const u8 = null, +first_line_matches_prefix: ?[]const u8 = null, +first_line_matches_content: ?[]const u8 = null, + +include_files: []const u8 = "", + +pub fn from_file_type(file_type: *const FileType) @This() { + return .{ + .color = file_type.color, + .icon = file_type.icon, + .description = file_type.description, + .extensions = file_type.extensions, + .first_line_matches_prefix = if (file_type.first_line_matches) |flm| flm.prefix else null, + .first_line_matches_content = if (file_type.first_line_matches) |flm| flm.content else null, + .comment = file_type.comment, + .formatter = file_type.formatter, + .language_server = file_type.language_server, + }; +} + +const FileType = @import("syntax").FileType; diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index b94e64c..87bb11d 100644 --- a/src/tui/mainview.zig +++ b/src/tui/mainview.zig @@ -8,6 +8,8 @@ const location_history = @import("location_history"); const project_manager = @import("project_manager"); const log = @import("log"); const shell = @import("shell"); +const syntax = @import("syntax"); +const file_type_config = @import("file_type_config"); const builtin = @import("builtin"); const build_options = @import("build_options"); @@ -263,6 +265,22 @@ fn open_style_config(self: *Self, Style: type) command.Result { if (self.get_active_buffer()) |buffer| buffer.mark_not_ephemeral(); } +fn get_file_type_config_file_path(allocator: std.mem.Allocator, file_type: []const u8) ![]const u8 { + var stream = std.ArrayList(u8).init(allocator); + const writer = stream.writer(); + _ = try writer.writeAll(try root.get_config_dir()); + _ = try writer.writeByte(std.fs.path.sep); + _ = try writer.writeAll("file_type"); + _ = try writer.writeByte(std.fs.path.sep); + std.fs.makeDirAbsolute(stream.items) catch |e| switch (e) { + error.PathAlreadyExists => {}, + else => return e, + }; + _ = try writer.writeAll(file_type); + _ = try writer.writeAll(".conf"); + return stream.toOwnedSlice(); +} + const cmds = struct { pub const Target = Self; const Ctx = command.Context; @@ -492,6 +510,49 @@ const cmds = struct { } pub const open_home_style_config_meta: Meta = .{ .description = "Edit home screen" }; + pub fn change_file_type(_: *Self, _: Ctx) Result { + return tui.open_overlay( + @import("mode/overlay/file_type_palette.zig").Variant("set_file_type", "Select file type").Type, + ); + } + pub const change_file_type_meta: Meta = .{ .description = "Change file type" }; + + pub fn open_file_type_config(self: *Self, ctx: Ctx) Result { + var file_type_name: []const u8 = undefined; + if (!(ctx.args.match(.{tp.extract(&file_type_name)}) catch false)) + return tui.open_overlay( + @import("mode/overlay/file_type_palette.zig").Variant("open_file_type_config", "Edit file type").Type, + ); + + const file_name = try get_file_type_config_file_path(self.allocator, file_type_name); + defer self.allocator.free(file_name); + + const file: ?std.fs.File = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch null; + if (file) |f| { + f.close(); + return tp.self_pid().send(.{ "cmd", "navigate", .{ .file = file_name } }); + } + + const file_type = syntax.FileType.get_by_name(file_type_name) orelse return error.UnknownFileType; + const config = file_type_config.from_file_type(file_type); + + var conf = std.ArrayListUnmanaged(u8).empty; + defer conf.deinit(self.allocator); + root.write_config_to_writer(file_type_config, config, conf.writer(self.allocator)) catch {}; + tui.reset_drag_context(); + try self.create_editor(); + try command.executeName("open_scratch_buffer", command.fmt(.{ + file_name, + conf.items, + "conf", + })); + if (self.get_active_buffer()) |buffer| buffer.mark_not_ephemeral(); + } + pub const open_file_type_config_meta: Meta = .{ + .arguments = &.{.string}, + .description = "Edit file type configuration", + }; + pub fn create_scratch_buffer(self: *Self, ctx: Ctx) Result { const args = try ctx.args.clone(self.allocator); defer self.allocator.free(args.buf); diff --git a/src/tui/mode/overlay/file_type_palette.zig b/src/tui/mode/overlay/file_type_palette.zig index 4c439cf..56f3269 100644 --- a/src/tui/mode/overlay/file_type_palette.zig +++ b/src/tui/mode/overlay/file_type_palette.zig @@ -6,125 +6,129 @@ const syntax = @import("syntax"); const Widget = @import("../../Widget.zig"); const tui = @import("../../tui.zig"); -pub const Type = @import("palette.zig").Create(@This()); +pub fn Variant(comptime command: []const u8, comptime label_: []const u8) type { + return struct { + 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 label = label_; + 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 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, -}; + pub const Match = struct { + name: []const u8, + score: i32, + matches: []const usize, + }; -var previous_file_type: ?[]const u8 = null; + 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.get_active_editor()) |editor| - if (editor.syntax) |editor_syntax| - break :blk editor_syntax.file_type.name; - break :blk null; + pub fn load_entries(palette: *Type) !usize { + var longest_hint: usize = 0; + var idx: usize = 0; + previous_file_type = blk: { + if (tui.get_active_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(" "); + 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"); + if (tui.config().show_fileicons) { + tui.render_file_icon(&button.plane, icon, color); + _ = button.plane.print(" ", .{}) catch {}; + } + 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) { + tui.render_match_cell(&button.plane, 0, index + 4, theme) catch break; + } else break; + } + return false; + } + + 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", command, .{name_} }) catch |e| menu.*.opts.ctx.logger.err("file_type_palette", e); + } }; - - 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(" "); - 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"); - if (tui.config().show_fileicons) { - tui.render_file_icon(&button.plane, icon, color); - _ = button.plane.print(" ", .{}) catch {}; - } - 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) { - tui.render_match_cell(&button.plane, 0, index + 4, theme) catch break; - } else break; - } - return false; -} - -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/tui.zig b/src/tui/tui.zig index c9c462a..23633c8 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -917,11 +917,6 @@ const cmds = struct { } pub const change_theme_meta: Meta = .{ .description = "Change 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: Meta = .{ .description = "Change file type" }; - pub fn change_fontface(self: *Self, _: Ctx) Result { if (build_options.gui) self.rdr_.get_fontfaces(); @@ -1117,6 +1112,10 @@ pub fn mini_mode() ?*MiniMode { return if (current().mini_mode_) |*p| p else null; } +pub fn open_overlay(mode: type) command.Result { + return current().enter_overlay_mode(mode); +} + pub fn query_cache() *syntax.QueryCache { return current().query_cache_; }