Merge branch 'master' into zig-0.15.0

This commit is contained in:
CJ van den Berg 2025-07-14 19:11:53 +02:00
commit 4e80bae8b8
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
24 changed files with 701 additions and 262 deletions

View file

@ -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 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: Files to load may be specifed on the command line:
```shell ```shell
@ -133,9 +125,26 @@ Show supported language names with `--list-languages`.
See `flow --help` for the full list of command line options. 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 # 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+shift+p` or `alt+x` to show the command palette.
Press `ctrl+F2` to see a full list of all current keybindings and commands. Press `ctrl+F2` to see a full list of all current keybindings and commands.

View file

@ -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(.{ const log_mod = b.createModule(.{
.root_source_file = b.path("src/log.zig"), .root_source_file = b.path("src/log.zig"),
.imports = &.{ .imports = &.{
@ -498,7 +505,7 @@ pub fn build_exe(
.{ .name = "thespian", .module = thespian_mod }, .{ .name = "thespian", .module = thespian_mod },
.{ .name = "Buffer", .module = Buffer_mod }, .{ .name = "Buffer", .module = Buffer_mod },
.{ .name = "tracy", .module = tracy_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 = "dizzy", .module = dizzy_dep.module("dizzy") },
.{ .name = "fuzzig", .module = fuzzig_dep.module("fuzzig") }, .{ .name = "fuzzig", .module = fuzzig_dep.module("fuzzig") },
.{ .name = "git", .module = git_mod }, .{ .name = "git", .module = git_mod },
@ -531,6 +538,7 @@ pub fn build_exe(
.{ .name = "cbor", .module = cbor_mod }, .{ .name = "cbor", .module = cbor_mod },
.{ .name = "config", .module = config_mod }, .{ .name = "config", .module = config_mod },
.{ .name = "gui_config", .module = gui_config_mod }, .{ .name = "gui_config", .module = gui_config_mod },
.{ .name = "file_type_config", .module = file_type_config_mod },
.{ .name = "log", .module = log_mod }, .{ .name = "log", .module = log_mod },
.{ .name = "command", .module = command_mod }, .{ .name = "command", .module = command_mod },
.{ .name = "EventHandler", .module = EventHandler_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("renderer", renderer_mod);
exe.root_module.addImport("input", input_mod); exe.root_module.addImport("input", input_mod);
exe.root_module.addImport("syntax", syntax_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("color", color_mod);
exe.root_module.addImport("bin_path", bin_path_mod); exe.root_module.addImport("bin_path", bin_path_mod);
exe.root_module.addImport("version", b.createModule(.{ .root_source_file = version_file })); 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("renderer", renderer_mod);
check_exe.root_module.addImport("input", input_mod); check_exe.root_module.addImport("input", input_mod);
check_exe.root_module.addImport("syntax", syntax_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("color", color_mod);
check_exe.root_module.addImport("bin_path", bin_path_mod); check_exe.root_module.addImport("bin_path", bin_path_mod);
check_exe.root_module.addImport("version", b.createModule(.{ .root_source_file = version_file })); check_exe.root_module.addImport("version", b.createModule(.{ .root_source_file = version_file }));

View file

@ -15,8 +15,8 @@
.hash = "dizzy-1.0.0-AAAAAM1wAAAiDbx_6RwcVEOBk8p2XOu8t9WPNc3K7kBK", .hash = "dizzy-1.0.0-AAAAAM1wAAAiDbx_6RwcVEOBk8p2XOu8t9WPNc3K7kBK",
}, },
.thespian = .{ .thespian = .{
.url = "https://github.com/neurocyte/thespian/archive/829a8d33e92988a51a8c51d204ec766a28c7903d.tar.gz", .url = "https://github.com/neurocyte/thespian/archive/ccdcbbff09f945eec063ebf889581db3e1312107.tar.gz",
.hash = "thespian-0.0.1-owFOjs0TBgAAed7EtHDPtpB7NBn-riNjb7Rkc7a_Voow", .hash = "thespian-0.0.1-owFOjlgaBgCqc3FCnB4Xyg8-9jyIDWgHSJMGx_nt5Kcc",
}, },
.themes = .{ .themes = .{
.url = "https://github.com/neurocyte/flow-themes/releases/download/master-952f9f630ea9544088fd30293666ee0650b7a690/flow-themes.tar.gz", .url = "https://github.com/neurocyte/flow-themes/releases/download/master-952f9f630ea9544088fd30293666ee0650b7a690/flow-themes.tar.gz",

28
help.md
View file

@ -157,19 +157,17 @@ Configuration is stored in the standard location
The default configuration will be written the first time The default configuration will be written the first time
Flow Control is started and looks similar to this: Flow Control is started and looks similar to this:
``` ```
{ frame_rate 60
"frame_rate": 60, theme "default"
"theme": "default", input_mode "flow"
"input_mode": "flow", gutter_line_numbers true
"gutter_line_numbers": true, gutter_line_numbers_relative false
"gutter_line_numbers_relative": false, enable_terminal_cursor false
"enable_terminal_cursor": false, highlight_current_line true
"highlight_current_line": true, highlight_current_line_gutter true
"highlight_current_line_gutter": true, show_whitespace false
"show_whitespace": false, animation_min_lag 0
"animation_min_lag": 0, animation_max_lag 150
"animation_max_lag": 150
}
``` ```
Most of these options are fairly self explanitory. 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 `animation_max_lag` controls the maximum amount of time allowed
for rendering scrolling animations. Set to 0 to disable scrolling for rendering scrolling animations. Set to 0 to disable scrolling
animation altogether. 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.

View file

@ -230,6 +230,7 @@ pub fn restore_state_v0(self: *Self, data: []const u8) error{
InvalidPIntType, InvalidPIntType,
JsonIncompatibleType, JsonIncompatibleType,
NotAnObject, NotAnObject,
BadArrayAllocExtract,
}!void { }!void {
tp.trace(tp.channel.debug, .{"restore_state_v0"}); tp.trace(tp.channel.debug, .{"restore_state_v0"});
defer self.sort_files_by_mtime(); defer self.sort_files_by_mtime();

View file

@ -23,10 +23,51 @@ pub const RGB = struct {
return .{ .r = v[0], .g = v[1], .b = v[2] }; 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 { pub fn to_u8s(v: RGB) [3]u8 {
return [_]u8{ v.r, v.g, v.b }; 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 { pub fn contrast(a_: RGB, b_: RGB) f32 {
const a = RGBf.from_RGB(a_).luminance(); const a = RGBf.from_RGB(a_).luminance();
const b = RGBf.from_RGB(b_).luminance(); const b = RGBf.from_RGB(b_).luminance();

202
src/file_type_config.zig Normal file
View file

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

View file

@ -249,6 +249,7 @@
"deinit_command": ["resume_undo_history"], "deinit_command": ["resume_undo_history"],
"press": [ "press": [
["<Esc>", ["move_left_vim"], ["enter_mode", "normal"]], ["<Esc>", ["move_left_vim"], ["enter_mode", "normal"]],
["<C-c>", ["move_left_vim"], ["enter_mode", "normal"]],
["<Del>", "delete_forward"], ["<Del>", "delete_forward"],
["<BS>", "delete_backward"], ["<BS>", "delete_backward"],
["<CR>", "smart_insert_line"], ["<CR>", "smart_insert_line"],

View file

@ -1,5 +1,5 @@
const std = @import("std"); const std = @import("std");
const syntax = @import("syntax"); const file_type_config = @import("file_type_config");
const builtin = @import("builtin"); const builtin = @import("builtin");
const RGB = @import("color").RGB; 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_formatter_len: usize = 0;
var max_extensions_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_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_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)); 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 tty_config.setColor(writer, .reset);
try writer.writeAll("\n"); 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 writer.writeAll(" ");
try setColorRgb(writer, file_type.color); try setColorRgb(writer, file_type.color orelse file_type_config.default.color);
try writer.writeAll(file_type.icon); try writer.writeAll(file_type.icon orelse file_type_config.default.icon);
try tty_config.setColor(writer, .reset); try tty_config.setColor(writer, .reset);
try writer.writeAll(" "); try writer.writeAll(" ");
try write_string(writer, file_type.name, max_language_len + 1); try write_string(writer, file_type.name, max_language_len + 1);

View file

@ -2,6 +2,7 @@ const std = @import("std");
const tui = @import("tui"); const tui = @import("tui");
const cbor = @import("cbor"); const cbor = @import("cbor");
const thespian = @import("thespian"); const thespian = @import("thespian");
const color = @import("color");
const flags = @import("flags"); const flags = @import("flags");
const builtin = @import("builtin"); const builtin = @import("builtin");
const bin_path = @import("bin_path"); const bin_path = @import("bin_path");
@ -148,11 +149,10 @@ pub fn main() anyerror!void {
return list_languages.list(a, stdout.writer(), tty_config); return list_languages.list(a, stdout.writer(), tty_config);
} }
if (builtin.os.tag != .windows) if (builtin.os.tag != .windows and @hasDecl(renderer, "install_crash_handler")) {
if (std.posix.getenv("JITDEBUG")) |_| if (std.posix.getenv("JITDEBUG")) |_| renderer.jit_debugger_enabled = true;
thespian.install_debugger()
else if (@hasDecl(renderer, "install_crash_handler"))
renderer.install_crash_handler(); renderer.install_crash_handler();
}
if (args.debug_wait) { if (args.debug_wait) {
std.debug.print("press return to start", .{}); std.debug.print("press return to start", .{});
@ -445,13 +445,24 @@ pub fn exists_config(T: type) bool {
return true; 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 } { pub fn read_config(T: type, allocator: std.mem.Allocator) struct { T, [][]const u8 } {
config_mutex.lock(); config_mutex.lock();
defer config_mutex.unlock(); defer config_mutex.unlock();
var bufs: [][]const u8 = &[_][]const u8{}; 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]; 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)) { if (!read_config_file(T, allocator, &conf, &bufs, text_file_name)) {
_ = read_config_file(T, allocator, &conf, &bufs, json_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 { 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 }); var file = try std.fs.openFileAbsolute(file_name, .{ .mode = .read_only });
defer file.close(); defer file.close();
const text = try file.readToEndAlloc(allocator, 64 * 1024); const content = try file.readToEndAlloc(allocator, 64 * 1024);
defer allocator.free(text); 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); var cbor_buf = std.ArrayList(u8).init(allocator);
defer cbor_buf.deinit(); defer cbor_buf.deinit();
const writer = cbor_buf.writer(); 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; var lineno: u32 = 0;
while (it.next()) |line| { while (it.next()) |line| {
lineno += 1; 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_.*); var bufs = std.ArrayListUnmanaged([]const u8).fromOwnedSlice(bufs_.*);
bufs.append(allocator, cb) catch @panic("OOM:read_text_config_file"); bufs.append(allocator, cb) catch @panic("OOM:read_text_config_file");
bufs_.* = bufs.toOwnedSlice(allocator) 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 { 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); const cb = try cbor.fromJson(json, cbor_buf);
var iter = cb; var iter = cb;
_ = try cbor.decodeMapHeader(&iter); _ = 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( fn read_cbor_config(
T: type, T: type,
allocator: std.mem.Allocator,
conf: *T, conf: *T,
file_name: []const u8, file_name: []const u8,
cb: []const u8, cb: []const u8,
@ -554,13 +570,30 @@ fn read_cbor_config(
} }
} else if (std.mem.eql(u8, field_name, field_info.name)) { } else if (std.mem.eql(u8, field_name, field_info.name)) {
known = true; known = true;
var value: field_info.type = undefined; switch (field_info.type) {
u24, ?u24 => {
var value: []const u8 = undefined;
if (try cbor.matchValue(&iter, cbor.extract(&value))) { 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; @field(conf, field_info.name) = value;
} else { } else {
try cbor.skipValue(&iter); try cbor.skipValue(&iter);
std.log.err("invalid value for key '{s}'", .{field_name}); std.log.err("invalid value for key '{s}'", .{field_name});
} }
},
}
}; };
if (!known) { if (!known) {
try cbor.skipValue(&iter); try cbor.skipValue(&iter);
@ -613,15 +646,36 @@ pub fn write_config_to_writer(comptime T: type, data: T, writer: anytype) @TypeO
} else { } else {
try writer.print("{s} ", .{field_info.name}); try writer.print("{s} ", .{field_info.name});
} }
var s = std.json.writeStream(writer, .{ .whitespace = .indent_4 }); 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 s.write(@field(data, field_info.name));
},
}
try writer.print("\n", .{}); 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 { fn config_eql(comptime T: type, a: T, b: T) bool {
switch (T) { switch (T) {
[]const u8 => return std.mem.eql(u8, a, b), []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 => {}, else => {},
} }
switch (@typeInfo(T)) { switch (@typeInfo(T)) {
@ -707,7 +761,7 @@ pub fn list_themes(allocator: std.mem.Allocator) ![]const []const u8 {
return result.toOwnedSlice(); 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); return get_app_config_dir(application_name);
} }

View file

@ -3,7 +3,7 @@ const tp = @import("thespian");
const cbor = @import("cbor"); const cbor = @import("cbor");
const log = @import("log"); const log = @import("log");
const tracy = @import("tracy"); const tracy = @import("tracy");
const FileType = @import("syntax").FileType; const file_type_config = @import("file_type_config");
const root = @import("root"); const root = @import("root");
const Buffer = @import("Buffer"); const Buffer = @import("Buffer");
const builtin = @import("builtin"); const builtin = @import("builtin");
@ -137,7 +137,7 @@ pub fn delete_task(task: []const u8) (ProjectManagerError || ProjectError)!void
return send(.{ "delete_task", project, task }); 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; if (ephemeral) return;
const project = tp.env.get().str("project"); const project = tp.env.get().str("project");
if (project.len == 0) if (project.len == 0)

View file

@ -11,10 +11,12 @@ const RGB = @import("color").RGB;
const Plane = @This(); const Plane = @This();
const name_buf_len = 128;
window: vaxis.Window, window: vaxis.Window,
row: i32 = 0, row: i32 = 0,
col: i32 = 0, col: i32 = 0,
name_buf: [128]u8, name_buf: [name_buf_len]u8,
name_len: usize, name_len: usize,
cache: GraphemeCache = .{}, cache: GraphemeCache = .{},
style: vaxis.Cell.Style = .{}, style: vaxis.Cell.Style = .{},
@ -27,7 +29,7 @@ pub const Options = struct {
x: usize = 0, x: usize = 0,
rows: usize = 0, rows: usize = 0,
cols: usize = 0, cols: usize = 0,
name: [*:0]const u8, name: []const u8,
flags: option = .none, flags: option = .none,
}; };
@ -44,13 +46,14 @@ pub fn init(nopts: *const Options, parent_: Plane) !Plane {
.height = @as(u16, @intCast(nopts.rows)), .height = @as(u16, @intCast(nopts.rows)),
.border = .{}, .border = .{},
}; };
const len = @min(nopts.name.len, name_buf_len);
var plane: Plane = .{ var plane: Plane = .{
.window = parent_.window.child(opts), .window = parent_.window.child(opts),
.name_buf = undefined, .name_buf = undefined,
.name_len = std.mem.span(nopts.name).len, .name_len = len,
.scrolling = nopts.flags == .VSCROLL, .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; return plane;
} }

View file

@ -57,6 +57,7 @@ pub const Error = error{
InvalidPIntType, InvalidPIntType,
JsonIncompatibleType, JsonIncompatibleType,
NotAnObject, NotAnObject,
BadArrayAllocExtract,
} || std.Thread.SpawnError; } || std.Thread.SpawnError;
pub fn init(allocator: std.mem.Allocator, handler_ctx: *anyopaque, no_alternate: bool, _: *const fn (ctx: *anyopaque) void) Error!Self { 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); 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 { 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); in_panic.store(true, .release);
const cleanup = panic_cleanup; const cleanup = panic_cleanup;
panic_cleanup = null; panic_cleanup = null;
if (cleanup) |self| { if (cleanup) |self| {
self.vx.deinit(self.allocator, self.tty.anyWriter()); self.vx.deinit(self.allocator, self.tty.anyWriter());
self.tty.deinit(); 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 { pub fn run(self: *Self) Error!void {

View file

@ -1384,7 +1384,7 @@ pub fn attachSegfaultHandler() void {
updateSegfaultHandler(&act); updateSegfaultHandler(&act);
} }
fn resetSegfaultHandler() void { pub fn resetSegfaultHandler() void {
if (native_os == .windows) { if (native_os == .windows) {
if (windows_segfault_handle) |handle| { if (windows_segfault_handle) |handle| {
assert(windows.kernel32.RemoveVectoredExceptionHandler(handle) != 0); 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(); 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(); const stderr = io.getStdErr().writer();
_ = switch (sig) { _ = switch (sig) {
posix.SIG.SEGV => if (native_arch == .x86_64 and native_os == .linux and code == 128) // SI_KERNEL posix.SIG.SEGV => if (native_arch == .x86_64 and native_os == .linux and code == 128) // SI_KERNEL

View file

@ -35,6 +35,7 @@ pub const Error = error{
InvalidPIntType, InvalidPIntType,
JsonIncompatibleType, JsonIncompatibleType,
NotAnObject, NotAnObject,
BadArrayAllocExtract,
} || std.Thread.SpawnError; } || std.Thread.SpawnError;
pub const panic = messageBoxThenPanic(.{ .title = "Flow Panic" }); pub const panic = messageBoxThenPanic(.{ .title = "Flow Panic" });

View file

@ -26,6 +26,7 @@ pub const Error = error{
InvalidPIntType, InvalidPIntType,
JsonIncompatibleType, JsonIncompatibleType,
NotAnObject, NotAnObject,
BadArrayAllocExtract,
}; };
pub const OutputHandler = fn (context: usize, parent: tp.pid_ref, arg0: []const u8, output: []const u8) void; pub const OutputHandler = fn (context: usize, parent: tp.pid_ref, arg0: []const u8, output: []const u8) void;

View file

@ -24,7 +24,8 @@ const CacheEntry = struct {
query: ?*Query, query: ?*Query,
query_arena: ?*std.heap.ArenaAllocator, query_arena: ?*std.heap.ArenaAllocator,
query_type: QueryType, 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 { fn destroy(self: *@This(), allocator: std.mem.Allocator) void {
if (self.query_arena) |a| { 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); 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(); if (self.mutex) |*mtx| mtx.lock();
defer if (self.mutex) |*mtx| mtx.unlock(); 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 = null,
.query_arena = null, .query_arena = null,
.mutex = if (self.mutex) |_| .{} else 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, .query_type = query_type,
}; };
entry_.value_ptr.* = q; 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(); defer if (entry.mutex) |*mtx| mtx.unlock();
return if (entry.query) |query| query else blk: { 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 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 queries = FileType.queries.get(entry.file_type_name) orelse return null;
const query_bin = switch (entry.query_type) { const query_bin = switch (entry.query_type) {
.highlights => queries.highlights_bin, .highlights => queries.highlights_bin,
.errors => queries.errors_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)); const query = try self.get_cached_query(try self.get_cache_entry(file_type, query_type));
self.add_ref_locked(); self.add_ref_locked();
return switch (@typeInfo(ReturnType(query_type))) { return switch (@typeInfo(ReturnType(query_type))) {

View file

@ -20,43 +20,45 @@ comment: []const u8,
formatter: ?[]const []const u8, formatter: ?[]const []const u8,
language_server: ?[]const []const u8, language_server: ?[]const []const u8,
pub fn get_by_name(name: []const u8) ?*const FileType { pub fn get_by_name_static(name: []const u8) ?FileType {
for (file_types) |*file_type| return FileType.static_file_types.get(name);
if (std.mem.eql(u8, file_type.name, 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 file_type;
return null; return null;
} }
pub fn guess(file_path: ?[]const u8, content: []const u8) ?*const FileType { fn guess_first_line_static(content: []const u8) ?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 {
const first_line = if (std.mem.indexOf(u8, content, "\n")) |pos| content[0..pos] else content; 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 (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 file_type;
return null; return null;
} }
fn match_first_line(match: FirstLineMatch, first_line: []const u8) bool { pub fn match_first_line(match_prefix: ?[]const u8, match_content: ?[]const u8, first_line: []const u8) bool {
if (match.prefix) |prefix| 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)) if (prefix.len > first_line.len or !std.mem.eql(u8, first_line[0..prefix.len], prefix))
return false; return false;
if (match.content) |content| if (match_content) |content|
if (std.mem.indexOf(u8, first_line, content)) |_| {} else return false; if (std.mem.indexOf(u8, first_line, content)) |_| {} else return false;
return true; 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 basename = std.fs.path.basename(file_path);
const extension = std.fs.path.extension(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)) if (ext.len == basename.len and std.mem.eql(u8, ext, basename))
return true; return true;
if (extension.len > 0 and ext.len == extension.len - 1 and std.mem.eql(u8, ext, extension[1..])) 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; 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 { pub const FirstLineMatch = struct {
prefix: ?[]const u8 = null, prefix: ?[]const u8 = null,
content: ?[]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 { fn vec(comptime args: anytype) []const []const u8 {
var cmd: []const []const u8 = &[_][]const u8{}; var cmd: []const []const u8 = &[_][]const u8{};
@ -102,7 +105,9 @@ fn vec(comptime args: anytype) []const []const u8 {
return cmd; 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)) { comptime switch (@typeInfo(Namespace)) {
.@"struct" => |info| { .@"struct" => |info| {
var count = 0; var count = 0;
@ -110,12 +115,12 @@ fn load_file_types(comptime Namespace: type) []const FileType {
// @compileLog(decl.name, @TypeOf(@field(Namespace, decl.name))); // @compileLog(decl.name, @TypeOf(@field(Namespace, decl.name)));
count += 1; count += 1;
} }
var construct_types: [count]FileType = undefined; var construct_types: [count]ListEntry = undefined;
var i = 0; var i = 0;
for (info.decls) |decl| { for (info.decls) |decl| {
const lang = decl.name; const lang = decl.name;
const args = @field(Namespace, lang); const args = @field(Namespace, lang);
construct_types[i] = .{ construct_types[i] = .{ lang, .{
.color = if (@hasField(@TypeOf(args), "color")) args.color else 0xffffff, .color = if (@hasField(@TypeOf(args), "color")) args.color else 0xffffff,
.icon = if (@hasField(@TypeOf(args), "icon")) args.icon else "󱀫", .icon = if (@hasField(@TypeOf(args), "icon")) args.icon else "󱀫",
.name = lang, .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, .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, .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, .language_server = if (@hasField(@TypeOf(args), "language_server")) vec(args.language_server) else null,
}; } };
i += 1; i += 1;
} }
const types = construct_types; const types = construct_types;

View file

@ -21,14 +21,13 @@ pub const Node = treez.Node;
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
lang: *const Language, lang: *const Language,
file_type: *const FileType,
parser: *Parser, parser: *Parser,
query: *Query, query: *Query,
errors_query: *Query, errors_query: *Query,
injections: ?*Query, injections: ?*Query,
tree: ?*treez.Tree = null, 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 query = try query_cache.get(file_type, .highlights);
const errors_query = try query_cache.get(file_type, .errors); const errors_query = try query_cache.get(file_type, .errors);
const injections = try query_cache.get(file_type, .injections); 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.* = .{ self.* = .{
.allocator = allocator, .allocator = allocator,
.lang = file_type.lang_fn() orelse std.debug.panic("tree-sitter parser function failed for language: {s}", .{file_type.name}), .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(), .parser = try Parser.create(),
.query = query, .query = query,
.errors_query = errors_query, .errors_query = errors_query,
@ -47,13 +45,13 @@ pub fn create(file_type: *const FileType, allocator: std.mem.Allocator, query_ca
return self; return self;
} }
pub fn create_file_type(allocator: std.mem.Allocator, lang_name: []const u8, query_cache: *QueryCache) !*Self { 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(lang_name) orelse return error.NotFound; const file_type = FileType.get_by_name_static(lang_name) orelse return error.NotFound;
return create(file_type, allocator, query_cache); 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 { 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(file_path, content) orelse return error.NotFound; const file_type = FileType.guess_static(file_path, content) orelse return error.NotFound;
return create(file_type, allocator, query_cache); return create(file_type, allocator, query_cache);
} }

View file

@ -280,6 +280,7 @@ pub const DeserializeError = error{
JsonIncompatibleType, JsonIncompatibleType,
InvalidQueryCbor, InvalidQueryCbor,
NotAnObject, NotAnObject,
BadArrayAllocExtract,
}; };
pub fn fromCbor(cb: []const u8, allocator: std.mem.Allocator) DeserializeError!struct { *TSQuery, *std.heap.ArenaAllocator } { pub fn fromCbor(cb: []const u8, allocator: std.mem.Allocator) DeserializeError!struct { *TSQuery, *std.heap.ArenaAllocator } {

View file

@ -8,6 +8,7 @@ const ripgrep = @import("ripgrep");
const tracy = @import("tracy"); const tracy = @import("tracy");
const text_manip = @import("text_manip"); const text_manip = @import("text_manip");
const syntax = @import("syntax"); const syntax = @import("syntax");
const file_type_config = @import("file_type_config");
const project_manager = @import("project_manager"); const project_manager = @import("project_manager");
const root_mod = @import("root"); const root_mod = @import("root");
@ -330,6 +331,7 @@ pub const Editor = struct {
utf8_sanitized: bool = false, utf8_sanitized: bool = false,
} = .{}, } = .{},
file_type: ?file_type_config = null,
syntax: ?*syntax = null, syntax: ?*syntax = null,
syntax_no_render: bool = false, syntax_no_render: bool = false,
syntax_report_timing: 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); 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" }); const frame_ = tracy.initZone(@src(), .{ .name = "guess" });
defer frame_.deinit(); defer frame_.deinit();
break :blk if (lang_override.len > 0) break :blk if (lang_override.len > 0)
syntax.FileType.get_by_name(lang_override) try file_type_config.get(lang_override)
else else
syntax.FileType.guess(self.file_path, content.items); file_type_config.guess_file_type(self.file_path, content.items);
}; };
const syn = blk: { const syn = blk: {
const frame_ = tracy.initZone(@src(), .{ .name = "create" }); const frame_ = tracy.initZone(@src(), .{ .name = "create" });
defer frame_.deinit(); defer frame_.deinit();
break :blk if (syn_file_type) |ft| break :blk if (self.file_type) |ft|
syntax.create(ft, self.allocator, tui.query_cache()) catch null ft.create_syntax(self.allocator, tui.query_cache()) catch null
else else
null; null;
}; };
if (syn) |syn_| { if (self.file_type) |ft| {
const frame_ = tracy.initZone(@src(), .{ .name = "did_open" }); const frame_ = tracy.initZone(@src(), .{ .name = "did_open" });
defer frame_.deinit(); defer frame_.deinit();
project_manager.did_open( project_manager.did_open(
file_path, file_path,
syn_.file_type, ft,
self.lsp_version, self.lsp_version,
try content.toOwnedSlice(std.heap.c_allocator), try content.toOwnedSlice(std.heap.c_allocator),
new_buf.is_ephemeral(), new_buf.is_ephemeral(),
@ -614,9 +616,9 @@ pub const Editor = struct {
self.syntax_no_render = tp.env.get().is("no-syntax"); self.syntax_no_render = tp.env.get().is("no-syntax");
self.syntax_report_timing = tp.env.get().is("syntax-report-timing"); self.syntax_report_timing = tp.env.get().is("syntax-report-timing");
const ftn = if (self.syntax) |syn| syn.file_type.name else "text"; const ftn = if (self.file_type) |ft| ft.name else file_type_config.default.name;
const fti = if (self.syntax) |syn| syn.file_type.icon else "🖹"; 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.syntax) |syn| syn.file_type.color else 0x000000; 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| { if (self.buffer) |buffer| {
buffer.file_type_name = ftn; buffer.file_type_name = ftn;
buffer.file_type_icon = fti; buffer.file_type_icon = fti;
@ -2946,30 +2948,46 @@ pub const Editor = struct {
for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| { for (self.cursels.items) |*cursel_| if (cursel_.*) |*cursel| {
if (cursel.selection) |_| { if (cursel.selection) |_| {
// just delete selection
root = self.delete_selection(root, cursel, b.allocator) catch continue; root = self.delete_selection(root, cursel, b.allocator) catch continue;
all_stop = false; all_stop = false;
continue; 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; 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| { if (cursel.selection) |*sel| {
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 { const egc_left, _, _ = sel.end.egc_at(root, self.metrics) catch {
root = self.delete_selection(root, cursel, b.allocator) catch continue; root = self.delete_selection(root, cursel, b.allocator) catch continue;
all_stop = false; all_stop = false;
continue; continue;
}; };
// char to the right of char being deleted
const egc_right, _, _ = sel.begin.egc_at(root, self.metrics) catch { const egc_right, _, _ = sel.begin.egc_at(root, self.metrics) catch {
root = self.delete_selection(root, cursel, b.allocator) catch continue; root = self.delete_selection(root, cursel, b.allocator) catch continue;
all_stop = false; all_stop = false;
continue; continue;
}; };
// 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])) { 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 {}; sel.begin.move_right(root, self.metrics) catch {};
break; break;
}; };
} }
}
root = self.delete_selection(root, cursel, b.allocator) catch continue; root = self.delete_selection(root, cursel, b.allocator) catch continue;
all_stop = false; all_stop = false;
}; };
@ -3592,7 +3610,7 @@ pub const Editor = struct {
pub const toggle_prefix_meta: Meta = .{ .arguments = &.{.string} }; pub const toggle_prefix_meta: Meta = .{ .arguments = &.{.string} };
pub fn toggle_comment(self: *Self, _: Context) Result { 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})); return self.toggle_prefix(command.fmt(.{comment}));
} }
pub const toggle_comment_meta: Meta = .{ .description = "Toggle comment" }; pub const toggle_comment_meta: Meta = .{ .description = "Toggle comment" };
@ -3634,7 +3652,7 @@ pub const Editor = struct {
var cursel: CurSel = .{}; var cursel: CurSel = .{};
cursel.cursor = cursor.*; cursel.cursor = cursor.*;
const first = find_first_non_ws(root, cursel.cursor.row, self.metrics); 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 off = first % self.indent_size;
const cols = if (off == 0) self.indent_size else off; const cols = if (off == 0) self.indent_size else off;
const sel = cursel.enable_selection(root, self.metrics) catch return error.Stop; 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; var content = std.ArrayListUnmanaged(u8).empty;
defer content.deinit(self.allocator); defer content.deinit(self.allocator);
try root.store(content.writer(self.allocator), eol_mode); 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, error.NotFound => null,
else => return e, else => return e,
}; };
@ -5521,7 +5539,7 @@ pub const Editor = struct {
pub const select_meta: Meta = .{ .arguments = &.{ .integer, .integer, .integer, .integer } }; pub const select_meta: Meta = .{ .arguments = &.{ .integer, .integer, .integer, .integer } };
fn get_formatter(self: *Self) ?[]const []const u8 { 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; return null;
} }
@ -5727,11 +5745,11 @@ pub const Editor = struct {
saved.cursor = sel.end; saved.cursor = sel.end;
break :ret sel; break :ret sel;
}; };
var result = std.ArrayListUnmanaged(u8).empty; var result = std.ArrayList(u8).init(self.allocator);
defer result.deinit(self.allocator); defer result.deinit();
const writer: struct { const writer: struct {
self_: *Self, self_: *Self,
result: *std.ArrayListUnmanaged(u8), result: *std.ArrayList(u8),
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
const Error = @typeInfo(@typeInfo(@TypeOf(Buffer.unicode.LetterCasing.toUpperStr)).@"fn".return_type.?).error_union.error_set; 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 else
try letter_casing.toLowerStr(writer.self_.allocator, bytes); try letter_casing.toLowerStr(writer.self_.allocator, bytes);
defer writer.self_.allocator.free(flipped); 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 { fn map_error(e: anyerror, _: ?*std.builtin.StackTrace) Error {
return @errorCast(e); return @errorCast(e);
@ -5817,29 +5835,38 @@ pub const Editor = struct {
self.syntax_refresh_full = true; self.syntax_refresh_full = true;
self.syntax_incremental_reparse = false; 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; var content = std.ArrayListUnmanaged(u8).empty;
defer content.deinit(std.heap.c_allocator); defer content.deinit(std.heap.c_allocator);
const root = try self.buf_root(); const root = try self.buf_root();
try root.store(content.writer(std.heap.c_allocator), try self.buf_eol_mode()); 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( project_manager.did_open(
file_path, file_path,
syn_.file_type, ft,
self.lsp_version, self.lsp_version,
try content.toOwnedSlice(std.heap.c_allocator), try content.toOwnedSlice(std.heap.c_allocator),
if (self.buffer) |p| p.is_ephemeral() else true, if (self.buffer) |p| p.is_ephemeral() else true,
) catch |e| ) catch |e|
self.logger.print("project_manager.did_open failed: {any}", .{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_no_render = tp.env.get().is("no-syntax");
self.syntax_report_timing = tp.env.get().is("syntax-report-timing"); self.syntax_report_timing = tp.env.get().is("syntax-report-timing");
const ftn = if (self.syntax) |syn| syn.file_type.name else "text"; const ftn = if (self.file_type) |ft| ft.name else "text";
const fti = if (self.syntax) |syn| syn.file_type.icon else "🖹"; const fti = if (self.file_type) |ft| ft.icon orelse "🖹" else "🖹";
const ftc = if (self.syntax) |syn| syn.file_type.color else 0x000000; 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; 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); try self.send_editor_open(self.file_path orelse "", file_exists, ftn, fti, ftc);
self.logger.print("file type {s}", .{file_type}); self.logger.print("file type {s}", .{file_type});

View file

@ -8,6 +8,8 @@ const location_history = @import("location_history");
const project_manager = @import("project_manager"); const project_manager = @import("project_manager");
const log = @import("log"); const log = @import("log");
const shell = @import("shell"); const shell = @import("shell");
const syntax = @import("syntax");
const file_type_config = @import("file_type_config");
const builtin = @import("builtin"); const builtin = @import("builtin");
const build_options = @import("build_options"); 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 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 { pub fn create_scratch_buffer(self: *Self, ctx: Ctx) Result {
const args = try ctx.args.clone(self.allocator); const args = try ctx.args.clone(self.allocator);
defer self.allocator.free(args.buf); defer self.allocator.free(args.buf);

View file

@ -2,13 +2,16 @@ const std = @import("std");
const cbor = @import("cbor"); const cbor = @import("cbor");
const tp = @import("thespian"); const tp = @import("thespian");
const syntax = @import("syntax"); const syntax = @import("syntax");
const file_type_config = @import("file_type_config");
const Widget = @import("../../Widget.zig"); const Widget = @import("../../Widget.zig");
const tui = @import("../../tui.zig"); const tui = @import("../../tui.zig");
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 Type = @import("palette.zig").Create(@This());
pub const label = "Select file type"; pub const label = label_;
pub const name = " file type"; pub const name = " file type";
pub const description = "file type"; pub const description = "file type";
@ -32,20 +35,21 @@ pub fn load_entries(palette: *Type) !usize {
var idx: usize = 0; var idx: usize = 0;
previous_file_type = blk: { previous_file_type = blk: {
if (tui.get_active_editor()) |editor| if (tui.get_active_editor()) |editor|
if (editor.syntax) |editor_syntax| if (editor.file_type) |editor_file_type|
break :blk editor_syntax.file_type.name; break :blk editor_file_type.name;
break :blk null; break :blk null;
}; };
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;
idx += 1; idx += 1;
(try palette.entries.addOne()).* = .{ (try palette.entries.addOne()).* = .{
.label = file_type.description, .label = file_type.description orelse file_type_config.default.description,
.name = file_type.name, .name = file_type.name,
.icon = file_type.icon, .icon = file_type.icon orelse file_type_config.default.icon,
.color = file_type.color, .color = file_type.color orelse file_type_config.default.color,
}; };
if (previous_file_type) |file_type_name| if (std.mem.eql(u8, file_type.name, file_type_name)) { if (previous_file_type) |previous_name| if (std.mem.eql(u8, file_type.name, previous_name)) {
palette.initial_selected = idx; palette.initial_selected = idx;
}; };
longest_hint = @max(longest_hint, file_type.name.len); longest_hint = @max(longest_hint, file_type.name.len);
@ -123,8 +127,10 @@ fn select(menu: **Type.MenuState, button: *Type.ButtonState) void {
if (!(cbor.matchString(&iter, &icon) catch false)) return; if (!(cbor.matchString(&iter, &icon) catch false)) return;
if (!(cbor.matchInt(u24, &iter, &color) catch false)) return; if (!(cbor.matchInt(u24, &iter, &color) catch false)) return;
if (!(cbor.matchString(&iter, &name_) catch false)) return; if (!(cbor.matchString(&iter, &name_) catch false)) return;
if (previous_file_type) |prev| if (std.mem.eql(u8, prev, name_)) if (!allow_previous) if (previous_file_type) |prev| if (std.mem.eql(u8, prev, name_))
return; 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", "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); tp.self_pid().send(.{ "cmd", command, .{name_} }) catch |e| menu.*.opts.ctx.logger.err("file_type_palette", e);
}
};
} }

View file

@ -105,8 +105,8 @@ fn init(allocator: Allocator) InitError!*Self {
var conf, const conf_bufs = root.read_config(@import("config"), allocator); var conf, const conf_bufs = root.read_config(@import("config"), allocator);
defer root.free_config(allocator, conf_bufs); defer root.free_config(allocator, conf_bufs);
if (conf.start_debugger_on_crash) if (@hasDecl(renderer, "install_crash_handler") and conf.start_debugger_on_crash)
tp.install_debugger(); 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; 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; conf.theme = theme_.name;
@ -917,11 +917,6 @@ const cmds = struct {
} }
pub const change_theme_meta: Meta = .{ .description = "Change color theme" }; 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 { pub fn change_fontface(self: *Self, _: Ctx) Result {
if (build_options.gui) if (build_options.gui)
self.rdr_.get_fontfaces(); self.rdr_.get_fontfaces();
@ -1117,6 +1112,10 @@ pub fn mini_mode() ?*MiniMode {
return if (current().mini_mode_) |*p| p else null; 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 { pub fn query_cache() *syntax.QueryCache {
return current().query_cache_; return current().query_cache_;
} }