diff --git a/README.md b/README.md index 03ba453..cf7be9b 100644 --- a/README.md +++ b/README.md @@ -94,14 +94,6 @@ You may install it on another system by simply copying the binary. scp zig-out/bin/flow root@otherhost:/usr/local/bin ``` -Configuration is mostly dynamically maintained with various commands in the UI. -It is stored under the standard user configuration path. Usually `~/.config/flow` -on Linux. %APPDATA%\Roaming\flow on Windows. Somewhere magical on MacOS - -Logs, traces and per-project most recently used file lists are stored in the -standard user application state directory. Usually `~/.local/state/flow` on -Linux and %APPDATA%\Roaming\flow on Windows. - Files to load may be specifed on the command line: ```shell @@ -133,9 +125,26 @@ Show supported language names with `--list-languages`. See `flow --help` for the full list of command line options. +# Configuration + +Configuration is mostly dynamically maintained with various commands in the UI. +It is stored under the standard user configuration path. Usually `~/.config/flow` +on Linux. %APPDATA%\Roaming\flow on Windows. Somewhere magical on MacOS. + +There are commands to open the various configuration files, so you don't have to +manually find them. Look for commands starting with `Edit` in the command palette. + +File types may be configured with the `Edit file type configuration` command. You +can also create a new file type by adding a new `.conf` file to the `file_type` +directory. Have a look at an existing file type to see what options are available. + +Logs, traces and per-project most recently used file lists are stored in the +standard user application state directory. Usually `~/.local/state/flow` on +Linux and %APPDATA%\Roaming\flow on Windows. + # Key bindings and commands -Press `F2` to switch the current keybinding mode. (flow, vim, emacs, etc.) +Press `F4` to switch the current keybinding mode. (flow, vim, emacs, etc.) Press `ctrl+shift+p` or `alt+x` to show the command palette. Press `ctrl+F2` to see a full list of all current keybindings and commands. diff --git a/build.zig b/build.zig index c508451..313e84b 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 = &.{ @@ -498,7 +505,7 @@ pub fn build_exe( .{ .name = "thespian", .module = thespian_mod }, .{ .name = "Buffer", .module = Buffer_mod }, .{ .name = "tracy", .module = tracy_mod }, - .{ .name = "syntax", .module = syntax_mod }, + .{ .name = "file_type_config", .module = file_type_config_mod }, .{ .name = "dizzy", .module = dizzy_dep.module("dizzy") }, .{ .name = "fuzzig", .module = fuzzig_dep.module("fuzzig") }, .{ .name = "git", .module = git_mod }, @@ -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 }, @@ -583,6 +591,7 @@ pub fn build_exe( exe.root_module.addImport("renderer", renderer_mod); exe.root_module.addImport("input", input_mod); exe.root_module.addImport("syntax", syntax_mod); + exe.root_module.addImport("file_type_config", file_type_config_mod); exe.root_module.addImport("color", color_mod); exe.root_module.addImport("bin_path", bin_path_mod); exe.root_module.addImport("version", b.createModule(.{ .root_source_file = version_file })); @@ -627,6 +636,7 @@ pub fn build_exe( check_exe.root_module.addImport("renderer", renderer_mod); check_exe.root_module.addImport("input", input_mod); check_exe.root_module.addImport("syntax", syntax_mod); + check_exe.root_module.addImport("file_type_config", file_type_config_mod); check_exe.root_module.addImport("color", color_mod); check_exe.root_module.addImport("bin_path", bin_path_mod); check_exe.root_module.addImport("version", b.createModule(.{ .root_source_file = version_file })); diff --git a/build.zig.zon b/build.zig.zon index 0a6e1a8..000a830 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -15,8 +15,8 @@ .hash = "dizzy-1.0.0-AAAAAM1wAAAiDbx_6RwcVEOBk8p2XOu8t9WPNc3K7kBK", }, .thespian = .{ - .url = "https://github.com/neurocyte/thespian/archive/829a8d33e92988a51a8c51d204ec766a28c7903d.tar.gz", - .hash = "thespian-0.0.1-owFOjs0TBgAAed7EtHDPtpB7NBn-riNjb7Rkc7a_Voow", + .url = "https://github.com/neurocyte/thespian/archive/ccdcbbff09f945eec063ebf889581db3e1312107.tar.gz", + .hash = "thespian-0.0.1-owFOjlgaBgCqc3FCnB4Xyg8-9jyIDWgHSJMGx_nt5Kcc", }, .themes = .{ .url = "https://github.com/neurocyte/flow-themes/releases/download/master-952f9f630ea9544088fd30293666ee0650b7a690/flow-themes.tar.gz", diff --git a/help.md b/help.md index 676b5b3..5db2e7b 100644 --- a/help.md +++ b/help.md @@ -157,19 +157,17 @@ Configuration is stored in the standard location The default configuration will be written the first time Flow Control is started and looks similar to this: ``` -{ - "frame_rate": 60, - "theme": "default", - "input_mode": "flow", - "gutter_line_numbers": true, - "gutter_line_numbers_relative": false, - "enable_terminal_cursor": false, - "highlight_current_line": true, - "highlight_current_line_gutter": true, - "show_whitespace": false, - "animation_min_lag": 0, - "animation_max_lag": 150 -} +frame_rate 60 +theme "default" +input_mode "flow" +gutter_line_numbers true +gutter_line_numbers_relative false +enable_terminal_cursor false +highlight_current_line true +highlight_current_line_gutter true +show_whitespace false +animation_min_lag 0 +animation_max_lag 150 ``` Most of these options are fairly self explanitory. @@ -183,3 +181,7 @@ of frames rendered. `animation_max_lag` controls the maximum amount of time allowed for rendering scrolling animations. Set to 0 to disable scrolling animation altogether. + +File types may be configured with the `Edit file type configuration` command. You +can also create a new file type by adding a new `.conf` file to the `file_type` +directory. Have a look at an existing file type to see what options are available. diff --git a/src/Project.zig b/src/Project.zig index 489c328..19e4399 100644 --- a/src/Project.zig +++ b/src/Project.zig @@ -230,6 +230,7 @@ pub fn restore_state_v0(self: *Self, data: []const u8) error{ InvalidPIntType, JsonIncompatibleType, NotAnObject, + BadArrayAllocExtract, }!void { tp.trace(tp.channel.debug, .{"restore_state_v0"}); defer self.sort_files_by_mtime(); diff --git a/src/color.zig b/src/color.zig index c3bb124..47be2ec 100644 --- a/src/color.zig +++ b/src/color.zig @@ -23,10 +23,51 @@ pub const RGB = struct { return .{ .r = v[0], .g = v[1], .b = v[2] }; } + pub fn from_string(s: []const u8) ?RGB { + const nib = struct { + fn f(c: u8) ?u8 { + return switch (c) { + '0'...'9' => c - '0', + 'A'...'F' => c - 'A' + 10, + 'a'...'f' => c - 'a' + 10, + else => null, + }; + } + }.f; + + if (s.len != 7) return null; + if (s[0] != '#') return null; + const r = (nib(s[1]) orelse return null) << 4 | (nib(s[2]) orelse return null); + const g = (nib(s[3]) orelse return null) << 4 | (nib(s[4]) orelse return null); + const b = (nib(s[5]) orelse return null) << 4 | (nib(s[6]) orelse return null); + return .{ .r = r, .g = g, .b = b }; + } + pub fn to_u8s(v: RGB) [3]u8 { return [_]u8{ v.r, v.g, v.b }; } + pub fn to_string(v: RGB, s: *[7]u8) []u8 { + const nib = struct { + fn f(n: u8) u8 { + return switch (n) { + 0...9 => '0' + n, + 0xA...0xF => 'A' + n - 10, + else => unreachable, + }; + } + }.f; + + s[0] = '#'; + s[1] = nib(v.r >> 4); + s[2] = nib(v.r & 0b00001111); + s[3] = nib(v.g >> 4); + s[4] = nib(v.g & 0b00001111); + s[5] = nib(v.b >> 4); + s[6] = nib(v.b & 0b00001111); + return s; + } + pub fn contrast(a_: RGB, b_: RGB) f32 { const a = RGBf.from_RGB(a_).luminance(); const b = RGBf.from_RGB(b_).luminance(); diff --git a/src/file_type_config.zig b/src/file_type_config.zig new file mode 100644 index 0000000..008bb9d --- /dev/null +++ b/src/file_type_config.zig @@ -0,0 +1,202 @@ +name: []const u8 = default.name, +description: ?[]const u8 = null, +extensions: ?[]const []const u8 = null, +icon: ?[]const u8 = null, +color: ?u24 = null, +comment: ?[]const u8 = null, +parser: ?[]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, +first_line_matches: ?[]const u8 = null, + +include_files: []const u8 = "", + +pub const default = struct { + pub const name = "text"; + pub const description = "Plain Text"; + pub const icon = "🖹"; + pub const color = 0x000000; +}; + +fn from_file_type(file_type: syntax.FileType) @This() { + return .{ + .name = file_type.name, + .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, + .parser = file_type.name, + .comment = file_type.comment, + .formatter = file_type.formatter, + .language_server = file_type.language_server, + }; +} + +pub fn get_default(allocator: std.mem.Allocator, file_type_name: []const u8) ![]const u8 { + const file_type = syntax.FileType.get_by_name_static(file_type_name) orelse return error.UnknownFileType; + const config = from_file_type(file_type); + var content = std.ArrayListUnmanaged(u8).empty; + defer content.deinit(allocator); + root.write_config_to_writer(@This(), config, content.writer(allocator)) catch {}; + return content.toOwnedSlice(allocator); +} + +pub fn get_all_names() []const []const u8 { + cache_mutex.lock(); + defer cache_mutex.unlock(); + if (cache_list.len == 0) + cache_list = load_all(cache_allocator) catch &.{}; + return cache_list; +} + +const cache_allocator = std.heap.c_allocator; +var cache_mutex: std.Thread.Mutex = .{}; +var cache: CacheType = .empty; +const CacheType = std.StringHashMapUnmanaged(?@This()); +var cache_list: []const []const u8 = &.{}; + +pub fn get(file_type_name: []const u8) !?@This() { + cache_mutex.lock(); + defer cache_mutex.unlock(); + + return if (cache.get(file_type_name)) |self| self else self: { + const file_type = file_type: { + const file_name = try get_config_file_path(cache_allocator, file_type_name); + defer cache_allocator.free(file_name); + + const file: ?std.fs.File = std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }) catch null; + if (file) |f| { + defer f.close(); + const stat = try f.stat(); + const buf = try cache_allocator.alloc(u8, @intCast(stat.size)); + defer cache_allocator.free(buf); + const size = try f.readAll(buf); + std.debug.assert(size == stat.size); + var self: @This() = .{}; + var bufs_: [][]const u8 = &.{}; // cached, no need to free + try root.parse_text_config_file(@This(), cache_allocator, &self, &bufs_, file_name, buf); + break :file_type self; + } else { + break :file_type if (syntax.FileType.get_by_name_static(file_type_name)) |ft| from_file_type(ft) else null; + } + }; + try cache.put(cache_allocator, file_type_name, file_type); + break :self file_type; + }; +} + +pub fn get_config_file_path(allocator: std.mem.Allocator, file_type: []const u8) ![]u8 { + var stream = std.ArrayList(u8).fromOwnedSlice(allocator, try get_config_dir_path(allocator)); + const writer = stream.writer(); + _ = try writer.writeAll(file_type); + _ = try writer.writeAll(".conf"); + return stream.toOwnedSlice(); +} + +fn get_config_dir_path(allocator: std.mem.Allocator) ![]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, + }; + return stream.toOwnedSlice(); +} + +const extension = ".conf"; + +fn load_all(allocator: std.mem.Allocator) ![]const []const u8 { + const dir_path = try get_config_dir_path(allocator); + defer allocator.free(dir_path); + + var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true }); + defer dir.close(); + + var names: std.StringHashMapUnmanaged(void) = .empty; + defer names.deinit(allocator); + + var iter = dir.iterate(); + while (try iter.next()) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.name, extension)) continue; + const file_type_name = entry.name[0 .. entry.name.len - extension.len]; + if (!names.contains(file_type_name)) + try names.put(allocator, try allocator.dupe(u8, file_type_name), {}); + } + + for (syntax.FileType.get_all()) |file_type| { + if (!names.contains(file_type.name)) + try names.put(allocator, try allocator.dupe(u8, file_type.name), {}); + } + + var list: std.ArrayListUnmanaged([]const u8) = .empty; + defer list.deinit(allocator); + + var names_iter = names.keyIterator(); + while (names_iter.next()) |key| { + (try list.addOne(allocator)).* = key.*; + } + + const less_fn = struct { + fn less_fn(_: void, lhs: []const u8, rhs: []const u8) bool { + return std.mem.order(u8, lhs, rhs) == .lt; + } + }.less_fn; + std.mem.sort([]const u8, list.items, {}, less_fn); + + return list.toOwnedSlice(allocator); +} + +pub fn guess_file_type(file_path: ?[]const u8, content: []const u8) ?@This() { + return guess(file_path, content); +} + +fn guess(file_path: ?[]const u8, content: []const u8) ?@This() { + if (guess_first_line(content)) |ft| return ft; + for (get_all_names()) |file_type_name| { + const file_type = get(file_type_name) catch unreachable orelse unreachable; + if (file_path) |fp| if (syntax.FileType.match_file_type(file_type.extensions orelse continue, fp)) + return file_type; + } + return null; +} + +fn guess_first_line(content: []const u8) ?@This() { + const first_line = if (std.mem.indexOf(u8, content, "\n")) |pos| content[0..pos] else content; + for (get_all_names()) |file_type_name| { + const file_type = get(file_type_name) catch unreachable orelse unreachable; + if (syntax.FileType.match_first_line(file_type.first_line_matches_prefix, file_type.first_line_matches_content, first_line)) + return file_type; + } + return null; +} + +pub fn create_syntax(file_type_config: @This(), allocator: std.mem.Allocator, query_cache: *syntax.QueryCache) !?*syntax { + return syntax.create( + syntax.FileType.get_by_name_static(file_type_config.parser orelse file_type_config.name) orelse return null, + allocator, + query_cache, + ); +} + +pub fn create_syntax_guess_file_type( + allocator: std.mem.Allocator, + content: []const u8, + file_path: ?[]const u8, + query_cache: *syntax.QueryCache, +) !?*syntax { + const file_type = guess(file_path, content) orelse return error.NotFound; + return create_syntax(file_type, allocator, query_cache); +} + +const syntax = @import("syntax"); +const std = @import("std"); +const root = @import("root"); diff --git a/src/keybind/builtin/vim.json b/src/keybind/builtin/vim.json index 19f1685..98e9b90 100644 --- a/src/keybind/builtin/vim.json +++ b/src/keybind/builtin/vim.json @@ -249,6 +249,7 @@ "deinit_command": ["resume_undo_history"], "press": [ ["", ["move_left_vim"], ["enter_mode", "normal"]], + ["", ["move_left_vim"], ["enter_mode", "normal"]], ["", "delete_forward"], ["", "delete_backward"], ["", "smart_insert_line"], diff --git a/src/list_languages.zig b/src/list_languages.zig index 31de403..6efbd10 100644 --- a/src/list_languages.zig +++ b/src/list_languages.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const syntax = @import("syntax"); +const file_type_config = @import("file_type_config"); const builtin = @import("builtin"); const RGB = @import("color").RGB; @@ -16,7 +16,8 @@ pub fn list(allocator: std.mem.Allocator, writer: anytype, tty_config: std.io.tt var max_formatter_len: usize = 0; var max_extensions_len: usize = 0; - for (syntax.FileType.file_types) |file_type| { + for (file_type_config.get_all_names()) |file_type_name| { + const file_type = try file_type_config.get(file_type_name) orelse unreachable; max_language_len = @max(max_language_len, file_type.name.len); max_langserver_len = @max(max_langserver_len, args_string_length(file_type.language_server)); max_formatter_len = @max(max_formatter_len, args_string_length(file_type.formatter)); @@ -31,10 +32,11 @@ pub fn list(allocator: std.mem.Allocator, writer: anytype, tty_config: std.io.tt try tty_config.setColor(writer, .reset); try writer.writeAll("\n"); - for (syntax.FileType.file_types) |file_type| { + for (file_type_config.get_all_names()) |file_type_name| { + const file_type = try file_type_config.get(file_type_name) orelse unreachable; try writer.writeAll(" "); - try setColorRgb(writer, file_type.color); - try writer.writeAll(file_type.icon); + try setColorRgb(writer, file_type.color orelse file_type_config.default.color); + try writer.writeAll(file_type.icon orelse file_type_config.default.icon); try tty_config.setColor(writer, .reset); try writer.writeAll(" "); try write_string(writer, file_type.name, max_language_len + 1); diff --git a/src/main.zig b/src/main.zig index 0d6c119..01641d9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,6 +2,7 @@ const std = @import("std"); const tui = @import("tui"); const cbor = @import("cbor"); const thespian = @import("thespian"); +const color = @import("color"); const flags = @import("flags"); const builtin = @import("builtin"); const bin_path = @import("bin_path"); @@ -148,11 +149,10 @@ pub fn main() anyerror!void { return list_languages.list(a, stdout.writer(), tty_config); } - if (builtin.os.tag != .windows) - if (std.posix.getenv("JITDEBUG")) |_| - thespian.install_debugger() - else if (@hasDecl(renderer, "install_crash_handler")) - renderer.install_crash_handler(); + if (builtin.os.tag != .windows and @hasDecl(renderer, "install_crash_handler")) { + if (std.posix.getenv("JITDEBUG")) |_| renderer.jit_debugger_enabled = true; + renderer.install_crash_handler(); + } if (args.debug_wait) { std.debug.print("press return to start", .{}); @@ -445,13 +445,24 @@ pub fn exists_config(T: type) bool { return true; } +fn get_default(T: type) T { + return switch (@typeInfo(T)) { + .array => &.{}, + .pointer => |info| switch (info.size) { + .slice => &.{}, + else => @compileError("unsupported config type " ++ @typeName(T)), + }, + else => .{}, + }; +} + pub fn read_config(T: type, allocator: std.mem.Allocator) struct { T, [][]const u8 } { config_mutex.lock(); defer config_mutex.unlock(); var bufs: [][]const u8 = &[_][]const u8{}; - const json_file_name = get_app_config_file_name(application_name, @typeName(T)) catch return .{ .{}, bufs }; + const json_file_name = get_app_config_file_name(application_name, @typeName(T)) catch return .{ get_default(T), bufs }; const text_file_name = json_file_name[0 .. json_file_name.len - ".json".len]; - var conf: T = .{}; + var conf: T = get_default(T); if (!read_config_file(T, allocator, &conf, &bufs, text_file_name)) { _ = read_config_file(T, allocator, &conf, &bufs, json_file_name); } @@ -476,12 +487,16 @@ fn read_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs: *[][] fn read_text_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8) !void { var file = try std.fs.openFileAbsolute(file_name, .{ .mode = .read_only }); defer file.close(); - const text = try file.readToEndAlloc(allocator, 64 * 1024); - defer allocator.free(text); + const content = try file.readToEndAlloc(allocator, 64 * 1024); + defer allocator.free(content); + return parse_text_config_file(T, allocator, conf, bufs_, file_name, content); +} + +pub fn parse_text_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8, content: []const u8) !void { var cbor_buf = std.ArrayList(u8).init(allocator); defer cbor_buf.deinit(); const writer = cbor_buf.writer(); - var it = std.mem.splitScalar(u8, text, '\n'); + var it = std.mem.splitScalar(u8, content, '\n'); var lineno: u32 = 0; while (it.next()) |line| { lineno += 1; @@ -505,7 +520,7 @@ fn read_text_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: var bufs = std.ArrayListUnmanaged([]const u8).fromOwnedSlice(bufs_.*); bufs.append(allocator, cb) catch @panic("OOM:read_text_config_file"); bufs_.* = bufs.toOwnedSlice(allocator) catch @panic("OOM:read_text_config_file"); - return read_cbor_config(T, conf, file_name, cb); + return read_cbor_config(T, allocator, conf, file_name, cb); } fn read_json_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: *[][]const u8, file_name: []const u8) !void { @@ -520,11 +535,12 @@ fn read_json_config_file(T: type, allocator: std.mem.Allocator, conf: *T, bufs_: const cb = try cbor.fromJson(json, cbor_buf); var iter = cb; _ = try cbor.decodeMapHeader(&iter); - return read_cbor_config(T, conf, file_name, iter); + return read_cbor_config(T, allocator, conf, file_name, iter); } fn read_cbor_config( T: type, + allocator: std.mem.Allocator, conf: *T, file_name: []const u8, cb: []const u8, @@ -554,12 +570,29 @@ fn read_cbor_config( } } else if (std.mem.eql(u8, field_name, field_info.name)) { known = true; - var value: field_info.type = undefined; - if (try cbor.matchValue(&iter, cbor.extract(&value))) { - @field(conf, field_info.name) = value; - } else { - try cbor.skipValue(&iter); - std.log.err("invalid value for key '{s}'", .{field_name}); + switch (field_info.type) { + u24, ?u24 => { + var value: []const u8 = undefined; + if (try cbor.matchValue(&iter, cbor.extract(&value))) { + const color_ = color.RGB.from_string(value); + if (color_) |color__| + @field(conf, field_info.name) = color__.to_u24() + else + std.log.err("invalid value for key '{s}'", .{field_name}); + } else { + try cbor.skipValue(&iter); + std.log.err("invalid value for key '{s}'", .{field_name}); + } + }, + else => { + var value: field_info.type = undefined; + if (try cbor.matchValue(&iter, cbor.extractAlloc(&value, allocator))) { + @field(conf, field_info.name) = value; + } else { + try cbor.skipValue(&iter); + std.log.err("invalid value for key '{s}'", .{field_name}); + } + }, } }; if (!known) { @@ -613,15 +646,36 @@ pub fn write_config_to_writer(comptime T: type, data: T, writer: anytype) @TypeO } else { try writer.print("{s} ", .{field_info.name}); } - var s = std.json.writeStream(writer, .{ .whitespace = .indent_4 }); - try s.write(@field(data, field_info.name)); + switch (field_info.type) { + u24 => try write_color_value(@field(data, field_info.name), writer), + ?u24 => if (@field(data, field_info.name)) |value| + try write_color_value(value, writer) + else + try writer.writeAll("null"), + else => { + var s = std.json.writeStream(writer, .{ .whitespace = .minified }); + try s.write(@field(data, field_info.name)); + }, + } try writer.print("\n", .{}); } } +fn write_color_value(value: u24, writer: anytype) @TypeOf(writer).Error!void { + var hex: [7]u8 = undefined; + try writer.writeByte('"'); + try writer.writeAll(color.RGB.to_string(color.RGB.from_u24(value), &hex)); + try writer.writeByte('"'); +} + fn config_eql(comptime T: type, a: T, b: T) bool { switch (T) { []const u8 => return std.mem.eql(u8, a, b), + []const []const u8 => { + if (a.len != b.len) return false; + for (a, 0..) |x, i| if (!config_eql([]const u8, x, b[i])) return false; + return true; + }, else => {}, } switch (@typeInfo(T)) { @@ -707,7 +761,7 @@ pub fn list_themes(allocator: std.mem.Allocator) ![]const []const u8 { return result.toOwnedSlice(); } -pub fn get_config_dir() ![]const u8 { +pub fn get_config_dir() ConfigDirError![]const u8 { return get_app_config_dir(application_name); } diff --git a/src/project_manager.zig b/src/project_manager.zig index a9c5466..78dde81 100644 --- a/src/project_manager.zig +++ b/src/project_manager.zig @@ -3,7 +3,7 @@ const tp = @import("thespian"); const cbor = @import("cbor"); const log = @import("log"); const tracy = @import("tracy"); -const FileType = @import("syntax").FileType; +const file_type_config = @import("file_type_config"); const root = @import("root"); const Buffer = @import("Buffer"); const builtin = @import("builtin"); @@ -137,7 +137,7 @@ pub fn delete_task(task: []const u8) (ProjectManagerError || ProjectError)!void return send(.{ "delete_task", project, task }); } -pub fn did_open(file_path: []const u8, file_type: *const FileType, version: usize, text: []const u8, ephemeral: bool) (ProjectManagerError || ProjectError)!void { +pub fn did_open(file_path: []const u8, file_type: file_type_config, version: usize, text: []const u8, ephemeral: bool) (ProjectManagerError || ProjectError)!void { if (ephemeral) return; const project = tp.env.get().str("project"); if (project.len == 0) diff --git a/src/renderer/vaxis/Plane.zig b/src/renderer/vaxis/Plane.zig index 80df067..34f68a8 100644 --- a/src/renderer/vaxis/Plane.zig +++ b/src/renderer/vaxis/Plane.zig @@ -11,10 +11,12 @@ const RGB = @import("color").RGB; const Plane = @This(); +const name_buf_len = 128; + window: vaxis.Window, row: i32 = 0, col: i32 = 0, -name_buf: [128]u8, +name_buf: [name_buf_len]u8, name_len: usize, cache: GraphemeCache = .{}, style: vaxis.Cell.Style = .{}, @@ -27,7 +29,7 @@ pub const Options = struct { x: usize = 0, rows: usize = 0, cols: usize = 0, - name: [*:0]const u8, + name: []const u8, flags: option = .none, }; @@ -44,13 +46,14 @@ pub fn init(nopts: *const Options, parent_: Plane) !Plane { .height = @as(u16, @intCast(nopts.rows)), .border = .{}, }; + const len = @min(nopts.name.len, name_buf_len); var plane: Plane = .{ .window = parent_.window.child(opts), .name_buf = undefined, - .name_len = std.mem.span(nopts.name).len, + .name_len = len, .scrolling = nopts.flags == .VSCROLL, }; - @memcpy(plane.name_buf[0..plane.name_len], nopts.name); + @memcpy(plane.name_buf[0..len], nopts.name[0..len]); return plane; } diff --git a/src/renderer/vaxis/renderer.zig b/src/renderer/vaxis/renderer.zig index 08130db..0c5983d 100644 --- a/src/renderer/vaxis/renderer.zig +++ b/src/renderer/vaxis/renderer.zig @@ -57,6 +57,7 @@ pub const Error = error{ InvalidPIntType, JsonIncompatibleType, NotAnObject, + BadArrayAllocExtract, } || std.Thread.SpawnError; pub fn init(allocator: std.mem.Allocator, handler_ctx: *anyopaque, no_alternate: bool, _: *const fn (ctx: *anyopaque) void) Error!Self { @@ -135,15 +136,46 @@ pub fn install_crash_handler() void { std.posix.sigaction(std.posix.SIG.ILL, &act, null); } +pub var jit_debugger_enabled: bool = false; + fn handle_crash(sig: i32, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.c) noreturn { + const debug = @import("std/debug.zig"); + debug.lockStdErr(); + + if (panic_in_progress()) + std.posix.abort(); + in_panic.store(true, .release); const cleanup = panic_cleanup; panic_cleanup = null; + if (cleanup) |self| { self.vx.deinit(self.allocator, self.tty.anyWriter()); self.tty.deinit(); } - @import("std/debug.zig").handleSegfaultPosix(sig, info, ctx_ptr); + if (builtin.os.tag == .linux and jit_debugger_enabled) { + handleSegfaultPosixNoAbort(sig, info, ctx_ptr); + @import("thespian").sighdl_debugger(sig, @ptrCast(@constCast(info)), ctx_ptr); + std.posix.abort(); + } else { + debug.handleSegfaultPosix(sig, info, ctx_ptr); + } + unreachable; +} + +fn handleSegfaultPosixNoAbort(sig: i32, info: *const std.posix.siginfo_t, ctx_ptr: ?*anyopaque) void { + const debug = @import("std/debug.zig"); + debug.resetSegfaultHandler(); + const addr = switch (builtin.os.tag) { + .linux => @intFromPtr(info.fields.sigfault.addr), + .freebsd, .macos => @intFromPtr(info.addr), + .netbsd => @intFromPtr(info.info.reason.fault.addr), + .openbsd => @intFromPtr(info.data.fault.addr), + .solaris, .illumos => @intFromPtr(info.reason.fault.addr), + else => unreachable, + }; + const code = if (builtin.os.tag == .netbsd) info.info.code else info.code; + debug.dumpSegfaultInfoPosix(sig, code, addr, ctx_ptr); } pub fn run(self: *Self) Error!void { diff --git a/src/renderer/vaxis/std/debug.zig b/src/renderer/vaxis/std/debug.zig index 8608184..b5f8697 100644 --- a/src/renderer/vaxis/std/debug.zig +++ b/src/renderer/vaxis/std/debug.zig @@ -1384,7 +1384,7 @@ pub fn attachSegfaultHandler() void { updateSegfaultHandler(&act); } -fn resetSegfaultHandler() void { +pub fn resetSegfaultHandler() void { if (native_os == .windows) { if (windows_segfault_handle) |handle| { assert(windows.kernel32.RemoveVectoredExceptionHandler(handle) != 0); @@ -1442,7 +1442,7 @@ pub fn handleSegfaultPosix(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*an posix.abort(); } -fn dumpSegfaultInfoPosix(sig: i32, code: i32, addr: usize, ctx_ptr: ?*anyopaque) void { +pub fn dumpSegfaultInfoPosix(sig: i32, code: i32, addr: usize, ctx_ptr: ?*anyopaque) void { const stderr = io.getStdErr().writer(); _ = switch (sig) { posix.SIG.SEGV => if (native_arch == .x86_64 and native_os == .linux and code == 128) // SI_KERNEL diff --git a/src/renderer/win32/renderer.zig b/src/renderer/win32/renderer.zig index 293ce68..a56adb7 100644 --- a/src/renderer/win32/renderer.zig +++ b/src/renderer/win32/renderer.zig @@ -35,6 +35,7 @@ pub const Error = error{ InvalidPIntType, JsonIncompatibleType, NotAnObject, + BadArrayAllocExtract, } || std.Thread.SpawnError; pub const panic = messageBoxThenPanic(.{ .title = "Flow Panic" }); diff --git a/src/shell.zig b/src/shell.zig index 69746cb..cb27221 100644 --- a/src/shell.zig +++ b/src/shell.zig @@ -26,6 +26,7 @@ pub const Error = error{ InvalidPIntType, JsonIncompatibleType, NotAnObject, + BadArrayAllocExtract, }; pub const OutputHandler = fn (context: usize, parent: tp.pid_ref, arg0: []const u8, output: []const u8) void; diff --git a/src/syntax/src/QueryCache.zig b/src/syntax/src/QueryCache.zig index a9dad05..816d8bc 100644 --- a/src/syntax/src/QueryCache.zig +++ b/src/syntax/src/QueryCache.zig @@ -24,7 +24,8 @@ const CacheEntry = struct { query: ?*Query, query_arena: ?*std.heap.ArenaAllocator, query_type: QueryType, - file_type: *const FileType, + file_type_name: []const u8, + lang_fn: FileType.LangFn, fn destroy(self: *@This(), allocator: std.mem.Allocator) void { if (self.query_arena) |a| { @@ -101,7 +102,7 @@ fn release_cache_entry_hash_map(allocator: std.mem.Allocator, hash_map: *std.Str hash_map.deinit(allocator); } -fn get_cache_entry(self: *Self, file_type: *const FileType, comptime query_type: QueryType) CacheError!*CacheEntry { +fn get_cache_entry(self: *Self, file_type: FileType, comptime query_type: QueryType) CacheError!*CacheEntry { if (self.mutex) |*mtx| mtx.lock(); defer if (self.mutex) |*mtx| mtx.unlock(); @@ -119,7 +120,8 @@ fn get_cache_entry(self: *Self, file_type: *const FileType, comptime query_type: .query = null, .query_arena = null, .mutex = if (self.mutex) |_| .{} else null, - .file_type = file_type, + .lang_fn = file_type.lang_fn, + .file_type_name = file_type.name, .query_type = query_type, }; entry_.value_ptr.* = q; @@ -133,8 +135,8 @@ fn get_cached_query(self: *Self, entry: *CacheEntry) Error!?*Query { defer if (entry.mutex) |*mtx| mtx.unlock(); return if (entry.query) |query| query else blk: { - const lang = entry.file_type.lang_fn() orelse std.debug.panic("tree-sitter parser function failed for language: {s}", .{entry.file_type.name}); - const queries = FileType.queries.get(entry.file_type.name) orelse return null; + const lang = entry.lang_fn() orelse std.debug.panic("tree-sitter parser function failed for language: {s}", .{entry.file_type_name}); + const queries = FileType.queries.get(entry.file_type_name) orelse return null; const query_bin = switch (entry.query_type) { .highlights => queries.highlights_bin, .errors => queries.errors_bin, @@ -166,7 +168,7 @@ fn ReturnType(comptime query_type: QueryType) type { }; } -pub fn get(self: *Self, file_type: *const FileType, comptime query_type: QueryType) Error!ReturnType(query_type) { +pub fn get(self: *Self, file_type: FileType, comptime query_type: QueryType) Error!ReturnType(query_type) { const query = try self.get_cached_query(try self.get_cache_entry(file_type, query_type)); self.add_ref_locked(); return switch (@typeInfo(ReturnType(query_type))) { diff --git a/src/syntax/src/file_type.zig b/src/syntax/src/file_type.zig index 4c45c64..8030272 100644 --- a/src/syntax/src/file_type.zig +++ b/src/syntax/src/file_type.zig @@ -20,43 +20,45 @@ comment: []const u8, formatter: ?[]const []const u8, language_server: ?[]const []const u8, -pub fn get_by_name(name: []const u8) ?*const FileType { - for (file_types) |*file_type| - if (std.mem.eql(u8, file_type.name, name)) +pub fn get_by_name_static(name: []const u8) ?FileType { + return FileType.static_file_types.get(name); +} + +pub fn get_all() []const FileType { + return FileType.static_file_types.values(); +} + +pub fn guess_static(file_path: ?[]const u8, content: []const u8) ?FileType { + if (guess_first_line_static(content)) |ft| return ft; + for (static_file_types.values()) |*file_type| + if (file_path) |fp| if (match_file_type(file_type.extensions, fp)) return file_type; return null; } -pub fn guess(file_path: ?[]const u8, content: []const u8) ?*const FileType { - if (guess_first_line(content)) |ft| return ft; - for (file_types) |*file_type| - if (file_path) |fp| if (match_file_type(file_type, fp)) - return file_type; - return null; -} - -fn guess_first_line(content: []const u8) ?*const FileType { +fn guess_first_line_static(content: []const u8) ?FileType { const first_line = if (std.mem.indexOf(u8, content, "\n")) |pos| content[0..pos] else content; - for (file_types) |*file_type| + for (static_file_types) |*file_type| if (file_type.first_line_matches) |match| - if (match_first_line(match, first_line)) + if (match_first_line(match.prefix, match.content, first_line)) return file_type; return null; } -fn match_first_line(match: FirstLineMatch, first_line: []const u8) bool { - if (match.prefix) |prefix| +pub fn match_first_line(match_prefix: ?[]const u8, match_content: ?[]const u8, first_line: []const u8) bool { + if (match_prefix == null and match_content == null) return false; + if (match_prefix) |prefix| if (prefix.len > first_line.len or !std.mem.eql(u8, first_line[0..prefix.len], prefix)) return false; - if (match.content) |content| + if (match_content) |content| if (std.mem.indexOf(u8, first_line, content)) |_| {} else return false; return true; } -fn match_file_type(file_type: *const FileType, file_path: []const u8) bool { +pub fn match_file_type(extensions: []const []const u8, file_path: []const u8) bool { const basename = std.fs.path.basename(file_path); const extension = std.fs.path.extension(file_path); - return for (file_type.extensions) |ext| { + return for (extensions) |ext| { if (ext.len == basename.len and std.mem.eql(u8, ext, basename)) return true; if (extension.len > 0 and ext.len == extension.len - 1 and std.mem.eql(u8, ext, extension[1..])) @@ -85,14 +87,15 @@ fn ft_func_name(comptime lang: []const u8) []const u8 { return &func_name; } -const LangFn = *const fn () callconv(.C) ?*const treez.Language; +pub const LangFn = *const fn () callconv(.C) ?*const treez.Language; pub const FirstLineMatch = struct { prefix: ?[]const u8 = null, content: ?[]const u8 = null, }; -pub const file_types = load_file_types(@import("file_types.zig")); +const static_file_type_list = load_file_types(@import("file_types.zig")); +const static_file_types = std.static_string_map.StaticStringMap(FileType).initComptime(static_file_type_list); fn vec(comptime args: anytype) []const []const u8 { var cmd: []const []const u8 = &[_][]const u8{}; @@ -102,7 +105,9 @@ fn vec(comptime args: anytype) []const []const u8 { return cmd; } -fn load_file_types(comptime Namespace: type) []const FileType { +const ListEntry = struct { []const u8, FileType }; + +fn load_file_types(comptime Namespace: type) []const ListEntry { comptime switch (@typeInfo(Namespace)) { .@"struct" => |info| { var count = 0; @@ -110,12 +115,12 @@ fn load_file_types(comptime Namespace: type) []const FileType { // @compileLog(decl.name, @TypeOf(@field(Namespace, decl.name))); count += 1; } - var construct_types: [count]FileType = undefined; + var construct_types: [count]ListEntry = undefined; var i = 0; for (info.decls) |decl| { const lang = decl.name; const args = @field(Namespace, lang); - construct_types[i] = .{ + construct_types[i] = .{ lang, .{ .color = if (@hasField(@TypeOf(args), "color")) args.color else 0xffffff, .icon = if (@hasField(@TypeOf(args), "icon")) args.icon else "󱀫", .name = lang, @@ -126,7 +131,7 @@ fn load_file_types(comptime Namespace: type) []const FileType { .first_line_matches = if (@hasField(@TypeOf(args), "first_line_matches")) args.first_line_matches else null, .formatter = if (@hasField(@TypeOf(args), "formatter")) vec(args.formatter) else null, .language_server = if (@hasField(@TypeOf(args), "language_server")) vec(args.language_server) else null, - }; + } }; i += 1; } const types = construct_types; diff --git a/src/syntax/src/syntax.zig b/src/syntax/src/syntax.zig index 2e5096e..bd67ccf 100644 --- a/src/syntax/src/syntax.zig +++ b/src/syntax/src/syntax.zig @@ -21,14 +21,13 @@ pub const Node = treez.Node; allocator: std.mem.Allocator, lang: *const Language, -file_type: *const FileType, parser: *Parser, query: *Query, errors_query: *Query, injections: ?*Query, tree: ?*treez.Tree = null, -pub fn create(file_type: *const FileType, allocator: std.mem.Allocator, query_cache: *QueryCache) !*Self { +pub fn create(file_type: FileType, allocator: std.mem.Allocator, query_cache: *QueryCache) !*Self { const query = try query_cache.get(file_type, .highlights); const errors_query = try query_cache.get(file_type, .errors); const injections = try query_cache.get(file_type, .injections); @@ -36,7 +35,6 @@ pub fn create(file_type: *const FileType, allocator: std.mem.Allocator, query_ca self.* = .{ .allocator = allocator, .lang = file_type.lang_fn() orelse std.debug.panic("tree-sitter parser function failed for language: {s}", .{file_type.name}), - .file_type = file_type, .parser = try Parser.create(), .query = query, .errors_query = errors_query, @@ -47,13 +45,13 @@ pub fn create(file_type: *const FileType, allocator: std.mem.Allocator, query_ca return self; } -pub fn create_file_type(allocator: std.mem.Allocator, lang_name: []const u8, query_cache: *QueryCache) !*Self { - const file_type = FileType.get_by_name(lang_name) orelse return error.NotFound; +pub fn static_create_file_type(allocator: std.mem.Allocator, lang_name: []const u8, query_cache: *QueryCache) !*Self { + const file_type = FileType.get_by_name_static(lang_name) orelse return error.NotFound; return create(file_type, allocator, query_cache); } -pub fn create_guess_file_type(allocator: std.mem.Allocator, content: []const u8, file_path: ?[]const u8, query_cache: *QueryCache) !*Self { - const file_type = FileType.guess(file_path, content) orelse return error.NotFound; +pub fn static_create_guess_file_type_static(allocator: std.mem.Allocator, content: []const u8, file_path: ?[]const u8, query_cache: *QueryCache) !*Self { + const file_type = FileType.guess_static(file_path, content) orelse return error.NotFound; return create(file_type, allocator, query_cache); } diff --git a/src/syntax/src/ts_serializer.zig b/src/syntax/src/ts_serializer.zig index 973c1f7..90c5865 100644 --- a/src/syntax/src/ts_serializer.zig +++ b/src/syntax/src/ts_serializer.zig @@ -280,6 +280,7 @@ pub const DeserializeError = error{ JsonIncompatibleType, InvalidQueryCbor, NotAnObject, + BadArrayAllocExtract, }; pub fn fromCbor(cb: []const u8, allocator: std.mem.Allocator) DeserializeError!struct { *TSQuery, *std.heap.ArenaAllocator } { diff --git a/src/tui/editor.zig b/src/tui/editor.zig index 5b14afc..74fafd9 100644 --- a/src/tui/editor.zig +++ b/src/tui/editor.zig @@ -8,6 +8,7 @@ const ripgrep = @import("ripgrep"); const tracy = @import("tracy"); const text_manip = @import("text_manip"); const syntax = @import("syntax"); +const file_type_config = @import("file_type_config"); const project_manager = @import("project_manager"); const root_mod = @import("root"); @@ -330,6 +331,7 @@ pub const Editor = struct { utf8_sanitized: bool = false, } = .{}, + file_type: ?file_type_config = null, syntax: ?*syntax = null, syntax_no_render: bool = false, syntax_report_timing: bool = false, @@ -579,30 +581,30 @@ pub const Editor = struct { try new_buf.root.store(content.writer(std.heap.c_allocator), new_buf.file_eol_mode); } - const syn_file_type = blk: { + self.file_type = blk: { const frame_ = tracy.initZone(@src(), .{ .name = "guess" }); defer frame_.deinit(); break :blk if (lang_override.len > 0) - syntax.FileType.get_by_name(lang_override) + try file_type_config.get(lang_override) else - syntax.FileType.guess(self.file_path, content.items); + file_type_config.guess_file_type(self.file_path, content.items); }; const syn = blk: { const frame_ = tracy.initZone(@src(), .{ .name = "create" }); defer frame_.deinit(); - break :blk if (syn_file_type) |ft| - syntax.create(ft, self.allocator, tui.query_cache()) catch null + break :blk if (self.file_type) |ft| + ft.create_syntax(self.allocator, tui.query_cache()) catch null else null; }; - if (syn) |syn_| { + if (self.file_type) |ft| { const frame_ = tracy.initZone(@src(), .{ .name = "did_open" }); defer frame_.deinit(); project_manager.did_open( file_path, - syn_.file_type, + ft, self.lsp_version, try content.toOwnedSlice(std.heap.c_allocator), new_buf.is_ephemeral(), @@ -614,9 +616,9 @@ pub const Editor = struct { 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 ftn = if (self.file_type) |ft| ft.name else file_type_config.default.name; + const fti = if (self.file_type) |ft| ft.icon orelse file_type_config.default.icon else file_type_config.default.icon; + const ftc = if (self.file_type) |ft| ft.color orelse file_type_config.default.color else file_type_config.default.color; if (self.buffer) |buffer| { buffer.file_type_name = ftn; buffer.file_type_icon = fti; @@ -2946,30 +2948,46 @@ pub const Editor = struct { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { if (cursel.selection) |_| { + // just delete selection root = self.delete_selection(root, cursel, b.allocator) catch continue; all_stop = false; continue; } + + // detect indentation + const first = find_first_non_ws(root, cursel.cursor.row, self.metrics); + + // select char to the left with_selection_const(root, move_cursor_left, cursel, self.metrics) catch continue; + // if we don't have a selection after move_cursor_left there is nothing to delete if (cursel.selection) |*sel| { - const egc_left, _, _ = sel.end.egc_at(root, self.metrics) catch { - root = self.delete_selection(root, cursel, b.allocator) catch continue; - all_stop = false; - continue; - }; - const egc_right, _, _ = sel.begin.egc_at(root, self.metrics) catch { - root = self.delete_selection(root, cursel, b.allocator) catch continue; - all_stop = false; - continue; - }; + if (first > sel.end.col) { + // we are inside leading whitespace + // select to next indentation boundary + while (sel.end.col > 0 and sel.end.col % self.indent_size != 0) + with_selection_const(root, move_cursor_left, cursel, self.metrics) catch break; + } else { + // char being deleted + const egc_left, _, _ = sel.end.egc_at(root, self.metrics) catch { + root = self.delete_selection(root, cursel, b.allocator) catch continue; + all_stop = false; + continue; + }; + // char to the right of char being deleted + const egc_right, _, _ = sel.begin.egc_at(root, self.metrics) catch { + root = self.delete_selection(root, cursel, b.allocator) catch continue; + all_stop = false; + continue; + }; - for (Buffer.unicode.char_pairs) |pair| if (std.mem.eql(u8, egc_left, pair[0]) and std.mem.eql(u8, egc_right, pair[1])) { - sel.begin.move_right(root, self.metrics) catch {}; - break; - }; + // if left char is a smart pair left char, also delete smart pair right char + for (Buffer.unicode.char_pairs) |pair| if (std.mem.eql(u8, egc_left, pair[0]) and std.mem.eql(u8, egc_right, pair[1])) { + sel.begin.move_right(root, self.metrics) catch {}; + break; + }; + } } - root = self.delete_selection(root, cursel, b.allocator) catch continue; all_stop = false; }; @@ -3592,7 +3610,7 @@ pub const Editor = struct { pub const toggle_prefix_meta: Meta = .{ .arguments = &.{.string} }; pub fn toggle_comment(self: *Self, _: Context) Result { - const comment = if (self.syntax) |syn| syn.file_type.comment else "//"; + const comment = if (self.file_type) |file_type| file_type.comment else "#"; return self.toggle_prefix(command.fmt(.{comment})); } pub const toggle_comment_meta: Meta = .{ .description = "Toggle comment" }; @@ -3634,7 +3652,7 @@ pub const Editor = struct { var cursel: CurSel = .{}; cursel.cursor = cursor.*; const first = find_first_non_ws(root, cursel.cursor.row, self.metrics); - if (first == 0) return error.Stop; + if (first == 0) return root; const off = first % self.indent_size; const cols = if (off == 0) self.indent_size else off; const sel = cursel.enable_selection(root, self.metrics) catch return error.Stop; @@ -4631,7 +4649,7 @@ pub const Editor = struct { var content = std.ArrayListUnmanaged(u8).empty; defer content.deinit(self.allocator); try root.store(content.writer(self.allocator), eol_mode); - self.syntax = syntax.create_guess_file_type(self.allocator, content.items, self.file_path, tui.query_cache()) catch |e| switch (e) { + self.syntax = file_type_config.create_syntax_guess_file_type(self.allocator, content.items, self.file_path, tui.query_cache()) catch |e| switch (e) { error.NotFound => null, else => return e, }; @@ -5521,7 +5539,7 @@ pub const Editor = struct { pub const select_meta: Meta = .{ .arguments = &.{ .integer, .integer, .integer, .integer } }; fn get_formatter(self: *Self) ?[]const []const u8 { - if (self.syntax) |syn| if (syn.file_type.formatter) |fmtr| if (fmtr.len > 0) return fmtr; + if (self.file_type) |file_type| if (file_type.formatter) |fmtr| if (fmtr.len > 0) return fmtr; return null; } @@ -5727,11 +5745,11 @@ pub const Editor = struct { saved.cursor = sel.end; break :ret sel; }; - var result = std.ArrayListUnmanaged(u8).empty; - defer result.deinit(self.allocator); + var result = std.ArrayList(u8).init(self.allocator); + defer result.deinit(); const writer: struct { self_: *Self, - result: *std.ArrayListUnmanaged(u8), + result: *std.ArrayList(u8), allocator: std.mem.Allocator, const Error = @typeInfo(@typeInfo(@TypeOf(Buffer.unicode.LetterCasing.toUpperStr)).@"fn".return_type.?).error_union.error_set; @@ -5742,7 +5760,7 @@ pub const Editor = struct { else try letter_casing.toLowerStr(writer.self_.allocator, bytes); defer writer.self_.allocator.free(flipped); - return writer.result.appendSlice(writer.allocator, flipped); + return writer.result.appendSlice(flipped); } fn map_error(e: anyerror, _: ?*std.builtin.StackTrace) Error { return @errorCast(e); @@ -5817,29 +5835,38 @@ pub const Editor = struct { self.syntax_refresh_full = true; self.syntax_incremental_reparse = false; - self.syntax = syntax: { + const file_type_config_ = try file_type_config.get(file_type); + self.file_type = file_type_config_; + + self.syntax = blk: { + break :blk if (self.file_type) |ft| + ft.create_syntax(self.allocator, tui.query_cache()) catch null + else + null; + }; + + if (self.file_type) |ft| { var content = std.ArrayListUnmanaged(u8).empty; defer content.deinit(std.heap.c_allocator); const root = try self.buf_root(); try root.store(content.writer(std.heap.c_allocator), try self.buf_eol_mode()); - const syn = syntax.create_file_type(self.allocator, file_type, tui.query_cache()) catch null; - if (syn) |syn_| if (self.file_path) |file_path| + + if (self.file_path) |file_path| project_manager.did_open( file_path, - syn_.file_type, + ft, self.lsp_version, try content.toOwnedSlice(std.heap.c_allocator), if (self.buffer) |p| p.is_ephemeral() else true, ) 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 ftn = if (self.file_type) |ft| ft.name else "text"; + const fti = if (self.file_type) |ft| ft.icon orelse "🖹" else "🖹"; + const ftc = if (self.file_type) |ft| ft.color orelse 0x000000 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}); diff --git a/src/tui/mainview.zig b/src/tui/mainview.zig index b94e64c..7fafc9d 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"); @@ -492,6 +494,46 @@ 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", false).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", true).Type, + ); + + const file_name = try file_type_config.get_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 content = try file_type_config.get_default(self.allocator, file_type_name); + defer self.allocator.free(content); + + tui.reset_drag_context(); + try self.create_editor(); + try command.executeName("open_scratch_buffer", command.fmt(.{ + file_name, + content, + "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..90f1305 100644 --- a/src/tui/mode/overlay/file_type_palette.zig +++ b/src/tui/mode/overlay/file_type_palette.zig @@ -2,129 +2,135 @@ const std = @import("std"); const cbor = @import("cbor"); const tp = @import("thespian"); const syntax = @import("syntax"); +const file_type_config = @import("file_type_config"); 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, allow_previous: bool) 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.file_type) |editor_file_type| + break :blk editor_file_type.name; + break :blk null; + }; + + for (file_type_config.get_all_names()) |file_type_name| { + const file_type = try file_type_config.get(file_type_name) orelse unreachable; + idx += 1; + (try palette.entries.addOne()).* = .{ + .label = file_type.description orelse file_type_config.default.description, + .name = file_type.name, + .icon = file_type.icon orelse file_type_config.default.icon, + .color = file_type.color orelse file_type_config.default.color, + }; + if (previous_file_type) |previous_name| if (std.mem.eql(u8, file_type.name, previous_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 (!allow_previous) 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 b12c4ce..23633c8 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -105,8 +105,8 @@ fn init(allocator: Allocator) InitError!*Self { var conf, const conf_bufs = root.read_config(@import("config"), allocator); defer root.free_config(allocator, conf_bufs); - if (conf.start_debugger_on_crash) - tp.install_debugger(); + if (@hasDecl(renderer, "install_crash_handler") and conf.start_debugger_on_crash) + renderer.jit_debugger_enabled = true; const theme_, const parsed_theme = get_theme_by_name(allocator, conf.theme) orelse get_theme_by_name(allocator, "dark_modern") orelse return error.UnknownTheme; conf.theme = theme_.name; @@ -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_; }