From 33eb434e169f7b68d6ef3bc06f6f39d73ea24942 Mon Sep 17 00:00:00 2001 From: Noam Lewis Date: Tue, 24 Feb 2026 21:52:35 +0200 Subject: [PATCH 1/5] Upgrade dependencies: oxc 0.115, ts-rs 12, rquickjs-serde 0.5, tempfile 3.25 Update ts_export.rs for ts-rs v12 breaking change (decl() now takes &Config). Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 95 ++++++++++---------- Cargo.toml | 20 ++--- crates/fresh-editor/Cargo.toml | 6 +- crates/fresh-plugin-runtime/src/ts_export.rs | 79 ++++++++-------- 4 files changed, 103 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 632f2acf4..6053efe4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3662,9 +3662,9 @@ dependencies = [ [[package]] name = "oxc_allocator" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7b9c7293fac710d0be6e941b70749566dc69f1918cf0446a677d0eb9a7c8259" +checksum = "e16d4295cf7888893b80ae70ff65c078ae3f9f52d5381cfc7eeffab089e07305" dependencies = [ "allocator-api2", "hashbrown 0.16.1", @@ -3674,9 +3674,9 @@ dependencies = [ [[package]] name = "oxc_ast" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd97b20b4ad9987795c0e5eda56752de8c24682a4e2cd6b1698fdc8135510e3" +checksum = "be755331a7de00100c60e03151663f26037a0dd720be238de57c036be03b4033" dependencies = [ "bitflags 2.11.0", "oxc_allocator", @@ -3691,9 +3691,9 @@ dependencies = [ [[package]] name = "oxc_ast_macros" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b1eb3b6f9ed42c528030161d0370b023229ed05b785baf7a80d7e99a794da2" +checksum = "a13a58adcfaadd4710b4f7d80ad422599ed5bb4956f4747d07e821c5897b16ef" dependencies = [ "phf 0.13.1", "proc-macro2", @@ -3703,9 +3703,9 @@ dependencies = [ [[package]] name = "oxc_ast_visit" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936eaf04ad8fd9f1e7613e277a7a0a2f8575fa9543c7a0fac4a8a6f590c8527c" +checksum = "4e33ffb874949ea07fce9b686c2dba7e221c849e047232c04a84b13bae4496ef" dependencies = [ "oxc_allocator", "oxc_ast", @@ -3715,9 +3715,9 @@ dependencies = [ [[package]] name = "oxc_codegen" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb04590335665806a3bc39485bb2f3c31d198c1dfd6bf7aa8405cd93b8205a0" +checksum = "f81db7038dc0288704c5ad72453c96933a46e2d5139376c87b1f5730b3d9cd03" dependencies = [ "bitflags 2.11.0", "cow-utils", @@ -3736,9 +3736,9 @@ dependencies = [ [[package]] name = "oxc_compat" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a1d5d8480010cab1dd1ede5287085472224f39aace766dcd2ae4c0005f3273" +checksum = "c96a136e3422c1b14babd3fe1103e4bc93036c10e72fe4f8634c881ec5285c2d" dependencies = [ "cow-utils", "oxc-browserslist", @@ -3749,18 +3749,18 @@ dependencies = [ [[package]] name = "oxc_data_structures" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea257e0e5a91b5cfcf06fd91744514d24e53c5450620f54a9fa1053f2b3fdf2" +checksum = "fd6c22a48542899e5f74162d55710ea2f95735c5d3a809196308b2dbf557f434" dependencies = [ "ropey", ] [[package]] name = "oxc_diagnostics" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1dce4194036de316f09d86c9a02e42aab1693c20423f20dda694c5d9f04394" +checksum = "fe5961a78ce2a24d288f5e7090f19ce49d062486e0d65e6140d01582198c94fd" dependencies = [ "cow-utils", "oxc-miette", @@ -3769,9 +3769,9 @@ dependencies = [ [[package]] name = "oxc_ecmascript" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f9465ce204eaddca376dcc235a44915c05ad512e280417d2cafd6bb1934f04" +checksum = "e1fb3d121c372df31514f95d87c92693001739d2c9e56be37909499b5396faf1" dependencies = [ "cow-utils", "num-bigint", @@ -3785,9 +3785,9 @@ dependencies = [ [[package]] name = "oxc_estree" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1bc44f56db73d6b7a5b8c4b4979cde79809e7c18d7fe5d99dffc37128bedd8c" +checksum = "d38fc12975751e104dc53c369cba1598ff15aa8ca30aaac49e63937256316969" [[package]] name = "oxc_index" @@ -3801,9 +3801,9 @@ dependencies = [ [[package]] name = "oxc_parser" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38f73b67e2ae42ce4a14e4e1dc305d65e1ada635c52959dbfaad5eec7245a15" +checksum = "341602ba5eb6629f7f90e49c1fce06bb8eed989412a4178acd8e7f48cf2c7f9d" dependencies = [ "bitflags 2.11.0", "cow-utils", @@ -3824,9 +3824,9 @@ dependencies = [ [[package]] name = "oxc_regular_expression" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b53ad034b3b87531190c0adde3dca1ee8a3d09e9009960576077a4062d9bc10" +checksum = "8e810182cbde172aeada70acc45dae74f6773384e0d295cc27e6e377b1fc277c" dependencies = [ "bitflags 2.11.0", "oxc_allocator", @@ -3840,9 +3840,9 @@ dependencies = [ [[package]] name = "oxc_semantic" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bc73688fe48cf7f8cd6202216864c5d569f3903758b9b0c1733dbd1eedc1fce" +checksum = "ffb04bd9f59bb6d8340bb186b0003bb6e8f1988e17048c61a5473ea216e9ed71" dependencies = [ "itertools 0.14.0", "memchr", @@ -3875,9 +3875,9 @@ dependencies = [ [[package]] name = "oxc_span" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4807a64b6063717dcd863fb4c1ce5ec628728d037e2f20e3ffdcf3aa4adf96ca" +checksum = "b9999ef787b0b989b8c2b31669069d3bdca20d017ff34a7284ff9e983cf7b1d8" dependencies = [ "compact_str", "oxc-miette", @@ -3889,20 +3889,21 @@ dependencies = [ [[package]] name = "oxc_str" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c64a431903dbb9b8505324824d1bd50e52407ebc30bf9a42279cd477328223e" +checksum = "a6fde66bc256ea0d09895c2a56a24f79e76abffd977f6c171516e42f1efdea51" dependencies = [ "compact_str", + "hashbrown 0.16.1", "oxc_allocator", "oxc_estree", ] [[package]] name = "oxc_syntax" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c8d491f4b2755a81aac85cde4706b591129215b3a79229ed0607ef622ed38b" +checksum = "e77ea5bd4ea42ce05b2f51bcef8e46a4598cea5038ab25877a2d27601a90da83" dependencies = [ "bitflags 2.11.0", "cow-utils", @@ -3910,7 +3911,6 @@ dependencies = [ "nonmax", "oxc_allocator", "oxc_ast_macros", - "oxc_data_structures", "oxc_estree", "oxc_index", "oxc_span", @@ -3920,9 +3920,9 @@ dependencies = [ [[package]] name = "oxc_transformer" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "768f8763f5901f4f517b96a25235a838805764cff3c5d2a67eed3c07493f8894" +checksum = "58dd1805067e1770a648cd53fcf6c48da4312fedda734ef556880936f975320f" dependencies = [ "base64", "compact_str", @@ -3949,9 +3949,9 @@ dependencies = [ [[package]] name = "oxc_traverse" -version = "0.112.0" +version = "0.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b66d5f2c7cb914d0b773560986d39b0ae5efcf59ce2367c45d4e30f551500b4" +checksum = "aea73a8421e6a433a187fca1c5fe48237ee65eaf40e5dae158d2853f0b2d8949" dependencies = [ "itoa", "oxc_allocator", @@ -3961,6 +3961,7 @@ dependencies = [ "oxc_ecmascript", "oxc_semantic", "oxc_span", + "oxc_str", "oxc_syntax", "rustc-hash 2.1.1", ] @@ -4859,9 +4860,9 @@ dependencies = [ [[package]] name = "rquickjs-serde" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df908bfc058ac1fb7e7b4609afe712bf2ce4ea86b49cd06e041a86c89220e8b" +checksum = "e9382258c8bd1cc5c555887e5e379ab1491ff0287ca68ceb07be39ea561beb23" dependencies = [ "rquickjs", "serde", @@ -6049,9 +6050,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.26.5" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12987371f54efc9b9306a20dc87ed5aaee9f320c8a8b115e28515c412b2efe39" +checksum = "13f456d2108c3fef07342ba4689a8503ec1fb5beed245e2b9be93096ef394848" dependencies = [ "cc", "regex", @@ -6123,9 +6124,9 @@ dependencies = [ [[package]] name = "tree-sitter-highlight" -version = "0.26.5" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b688407049ea1b55a7e872f138947d22389118b9c4d09b459cb34ca205e41c0" +checksum = "185918b7780c968608f2bd079233349df795846ac79562b3fd38477e287fca5c" dependencies = [ "regex", "streaming-iterator", @@ -6283,9 +6284,9 @@ dependencies = [ [[package]] name = "ts-rs" -version = "11.1.0" +version = "12.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4994acea2522cd2b3b85c1d9529a55991e3ad5e25cdcd3de9d505972c4379424" +checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8" dependencies = [ "serde_json", "thiserror 2.0.18", @@ -6294,9 +6295,9 @@ dependencies = [ [[package]] name = "ts-rs-macros" -version = "11.1.0" +version = "12.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6ff59666c9cbaec3533964505d39154dc4e0a56151fdea30a09ed0301f62e2" +checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 92b3e5382..d8532cb33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,22 +38,22 @@ tracing = "0.1" rust-i18n = "3.1.5" schemars = { version = "1.2", features = ["preserve_order"] } rquickjs = { version = "0.11", features = ["bindgen", "futures", "macro"] } -rquickjs-serde = "0.4" -oxc_allocator = "0.112.0" -oxc_ast = "0.112.0" -oxc_parser = "0.112.0" -oxc_transformer = "0.112.0" -oxc_codegen = "0.112.0" -oxc_span = "0.112.0" -oxc_semantic = "0.112.0" -oxc_diagnostics = "0.112.0" +rquickjs-serde = "0.5" +oxc_allocator = "0.115.0" +oxc_ast = "0.115.0" +oxc_parser = "0.115.0" +oxc_transformer = "0.115.0" +oxc_codegen = "0.115.0" +oxc_span = "0.115.0" +oxc_semantic = "0.115.0" +oxc_diagnostics = "0.115.0" tree-sitter = "0.26.5" tree-sitter-highlight = "0.26.3" crossterm = "0.29" winit = "0.30" wgpu = "28.0" lsp-types = "0.97" -ts-rs = { version = "11.1", features = ["serde_json"] } +ts-rs = { version = "12.0", features = ["serde_json"] } # Add more as needed during refactor [profile.release] diff --git a/crates/fresh-editor/Cargo.toml b/crates/fresh-editor/Cargo.toml index 003868171..65e3ae9c5 100644 --- a/crates/fresh-editor/Cargo.toml +++ b/crates/fresh-editor/Cargo.toml @@ -140,7 +140,7 @@ nix = { version = "0.31", features = ["signal", "pthread", "resource", "poll", " # Plugin API proc macros for type-safe bindings fresh-plugin-api-macros = { workspace = true, optional = true } # TypeScript type generation from Rust structs -ts-rs = { version = "11.1", optional = true } +ts-rs = { version = "12.0", optional = true } # anyhow is always needed for model error handling anyhow = { workspace = true } @@ -177,7 +177,7 @@ interprocess = { version = "2.2", features = ["tokio"] } # Embedded plugins support (optional) include_dir = { version = "0.7", optional = true } -tempfile = { version = "3.24", optional = true } +tempfile = { version = "3.25", optional = true } trash = { version = "5.2.5", optional = true } open = { version = "5.3", optional = true } @@ -186,7 +186,7 @@ fresh-gui = { workspace = true, optional = true } [dev-dependencies] proptest = "1.9" -tempfile = "3.24.0" +tempfile = "3.25" insta = { version = "1.46", features = ["yaml"] } vt100 = "0.16" # Virtual terminal emulator for testing real ANSI output ctor = "0.6.3" diff --git a/crates/fresh-plugin-runtime/src/ts_export.rs b/crates/fresh-plugin-runtime/src/ts_export.rs index a0b6b171a..5f5d3b385 100644 --- a/crates/fresh-plugin-runtime/src/ts_export.rs +++ b/crates/fresh-plugin-runtime/src/ts_export.rs @@ -12,7 +12,7 @@ use oxc_allocator::Allocator; use oxc_codegen::Codegen; use oxc_parser::Parser; use oxc_span::SourceType; -use ts_rs::TS; +use ts_rs::{Config as TsConfig, TS}; use fresh_core::api::{ ActionPopupAction, ActionPopupOptions, ActionSpec, BackgroundProcessResult, BufferInfo, @@ -32,70 +32,75 @@ use fresh_core::file_explorer::FileExplorerDecoration; /// Returns None if the type is not known (not registered in this mapping). /// Add new types here when they're added to api.rs with `#[derive(TS)]`. fn get_type_decl(type_name: &str) -> Option { + let cfg = TsConfig::default(); // Map TypeScript type names to their ts-rs declarations // The type name should match either the Rust struct name or the ts(rename = "...") value match type_name { // Core types - "BufferInfo" => Some(BufferInfo::decl()), - "CursorInfo" => Some(CursorInfo::decl()), - "ViewportInfo" => Some(ViewportInfo::decl()), - "ActionSpec" => Some(ActionSpec::decl()), - "BufferSavedDiff" => Some(BufferSavedDiff::decl()), - "LayoutHints" => Some(LayoutHints::decl()), + "BufferInfo" => Some(BufferInfo::decl(&cfg)), + "CursorInfo" => Some(CursorInfo::decl(&cfg)), + "ViewportInfo" => Some(ViewportInfo::decl(&cfg)), + "ActionSpec" => Some(ActionSpec::decl(&cfg)), + "BufferSavedDiff" => Some(BufferSavedDiff::decl(&cfg)), + "LayoutHints" => Some(LayoutHints::decl(&cfg)), // Process types - "SpawnResult" => Some(SpawnResult::decl()), - "BackgroundProcessResult" => Some(BackgroundProcessResult::decl()), + "SpawnResult" => Some(SpawnResult::decl(&cfg)), + "BackgroundProcessResult" => Some(BackgroundProcessResult::decl(&cfg)), // Terminal types - "TerminalResult" => Some(TerminalResult::decl()), - "CreateTerminalOptions" => Some(CreateTerminalOptions::decl()), + "TerminalResult" => Some(TerminalResult::decl(&cfg)), + "CreateTerminalOptions" => Some(CreateTerminalOptions::decl(&cfg)), // Composite buffer types (ts-rs renames these with Ts prefix) - "TsCompositeLayoutConfig" | "CompositeLayoutConfig" => Some(CompositeLayoutConfig::decl()), - "TsCompositeSourceConfig" | "CompositeSourceConfig" => Some(CompositeSourceConfig::decl()), - "TsCompositePaneStyle" | "CompositePaneStyle" => Some(CompositePaneStyle::decl()), - "TsCompositeHunk" | "CompositeHunk" => Some(CompositeHunk::decl()), + "TsCompositeLayoutConfig" | "CompositeLayoutConfig" => { + Some(CompositeLayoutConfig::decl(&cfg)) + } + "TsCompositeSourceConfig" | "CompositeSourceConfig" => { + Some(CompositeSourceConfig::decl(&cfg)) + } + "TsCompositePaneStyle" | "CompositePaneStyle" => Some(CompositePaneStyle::decl(&cfg)), + "TsCompositeHunk" | "CompositeHunk" => Some(CompositeHunk::decl(&cfg)), "TsCreateCompositeBufferOptions" | "CreateCompositeBufferOptions" => { - Some(CreateCompositeBufferOptions::decl()) + Some(CreateCompositeBufferOptions::decl(&cfg)) } // View transform types - "ViewTokenWireKind" => Some(ViewTokenWireKind::decl()), - "ViewTokenStyle" => Some(ViewTokenStyle::decl()), - "ViewTokenWire" => Some(ViewTokenWire::decl()), + "ViewTokenWireKind" => Some(ViewTokenWireKind::decl(&cfg)), + "ViewTokenStyle" => Some(ViewTokenStyle::decl(&cfg)), + "ViewTokenWire" => Some(ViewTokenWire::decl(&cfg)), // UI types (ts-rs renames these with Ts prefix) - "TsActionPopupAction" | "ActionPopupAction" => Some(ActionPopupAction::decl()), - "ActionPopupOptions" => Some(ActionPopupOptions::decl()), - "TsHighlightSpan" => Some(TsHighlightSpan::decl()), - "FileExplorerDecoration" => Some(FileExplorerDecoration::decl()), + "TsActionPopupAction" | "ActionPopupAction" => Some(ActionPopupAction::decl(&cfg)), + "ActionPopupOptions" => Some(ActionPopupOptions::decl(&cfg)), + "TsHighlightSpan" => Some(TsHighlightSpan::decl(&cfg)), + "FileExplorerDecoration" => Some(FileExplorerDecoration::decl(&cfg)), // Virtual buffer option types - "TextPropertyEntry" | "JsTextPropertyEntry" => Some(JsTextPropertyEntry::decl()), - "CreateVirtualBufferOptions" => Some(CreateVirtualBufferOptions::decl()), - "CreateVirtualBufferInSplitOptions" => Some(CreateVirtualBufferInSplitOptions::decl()), + "TextPropertyEntry" | "JsTextPropertyEntry" => Some(JsTextPropertyEntry::decl(&cfg)), + "CreateVirtualBufferOptions" => Some(CreateVirtualBufferOptions::decl(&cfg)), + "CreateVirtualBufferInSplitOptions" => Some(CreateVirtualBufferInSplitOptions::decl(&cfg)), "CreateVirtualBufferInExistingSplitOptions" => { - Some(CreateVirtualBufferInExistingSplitOptions::decl()) + Some(CreateVirtualBufferInExistingSplitOptions::decl(&cfg)) } // Return types - "TextPropertiesAtCursor" => Some(TextPropertiesAtCursor::decl()), - "VirtualBufferResult" => Some(VirtualBufferResult::decl()), + "TextPropertiesAtCursor" => Some(TextPropertiesAtCursor::decl(&cfg)), + "VirtualBufferResult" => Some(VirtualBufferResult::decl(&cfg)), // Prompt and directory types - "PromptSuggestion" | "Suggestion" => Some(Suggestion::decl()), - "DirEntry" => Some(DirEntry::decl()), + "PromptSuggestion" | "Suggestion" => Some(Suggestion::decl(&cfg)), + "DirEntry" => Some(DirEntry::decl(&cfg)), // Diagnostic types - "JsDiagnostic" => Some(JsDiagnostic::decl()), - "JsRange" => Some(JsRange::decl()), - "JsPosition" => Some(JsPosition::decl()), + "JsDiagnostic" => Some(JsDiagnostic::decl(&cfg)), + "JsRange" => Some(JsRange::decl(&cfg)), + "JsPosition" => Some(JsPosition::decl(&cfg)), // Language pack types - "LanguagePackConfig" => Some(LanguagePackConfig::decl()), - "LspServerPackConfig" => Some(LspServerPackConfig::decl()), - "FormatterPackConfig" => Some(FormatterPackConfig::decl()), + "LanguagePackConfig" => Some(LanguagePackConfig::decl(&cfg)), + "LspServerPackConfig" => Some(LspServerPackConfig::decl(&cfg)), + "FormatterPackConfig" => Some(FormatterPackConfig::decl(&cfg)), _ => None, } From d286576041805c9d2f73e86107921d924fe60454 Mon Sep 17 00:00:00 2001 From: Noam Lewis Date: Tue, 24 Feb 2026 22:51:26 +0200 Subject: [PATCH 2/5] Non-blocking grammar build: move SyntaxSet::build() to background thread Start with pre-compiled syntect defaults (~0ms, ~50 common languages) and build the full grammar registry on a background thread. When complete, swap in the new registry and re-detect syntax for all open buffers. Plugin grammar rebuilds also moved off the main thread via AsyncMessage::GrammarRegistryBuilt, with batching to coalesce multiple RegisterGrammar+ReloadGrammars sequences into a single rebuild. Removes UnbuiltGrammarRegistry (no longer needed) and the startup command drain loop. Co-Authored-By: Claude Opus 4.6 --- crates/fresh-editor/src/app/async_messages.rs | 3 + crates/fresh-editor/src/app/mod.rs | 79 +++++++++-- .../fresh-editor/src/app/plugin_commands.rs | 133 ++++++++---------- crates/fresh-editor/src/app/render.rs | 3 + .../src/primitives/grammar/types.rs | 25 ++++ .../fresh-editor/src/services/async_bridge.rs | 5 + 6 files changed, 162 insertions(+), 86 deletions(-) diff --git a/crates/fresh-editor/src/app/async_messages.rs b/crates/fresh-editor/src/app/async_messages.rs index 304aafefd..a8023f3d4 100644 --- a/crates/fresh-editor/src/app/async_messages.rs +++ b/crates/fresh-editor/src/app/async_messages.rs @@ -1142,6 +1142,9 @@ impl Editor { } } + // Flush any deferred grammar rebuilds as a single batch + self.flush_pending_grammars(); + has_visual_commands } diff --git a/crates/fresh-editor/src/app/mod.rs b/crates/fresh-editor/src/app/mod.rs index 94672e248..6b8c55d7b 100644 --- a/crates/fresh-editor/src/app/mod.rs +++ b/crates/fresh-editor/src/app/mod.rs @@ -284,6 +284,15 @@ pub struct Editor { /// Pending grammars registered by plugins, waiting for reload_grammars() to apply pending_grammars: Vec, + /// Whether a grammar reload has been requested but not yet flushed. + /// This allows batching multiple RegisterGrammar+ReloadGrammars sequences + /// into a single rebuild. + grammar_reload_pending: bool, + + /// Whether a background grammar build is in progress. + /// When true, `flush_pending_grammars()` defers work until the build completes. + grammar_build_in_progress: bool, + /// Active theme theme: crate::view::theme::Theme, @@ -966,8 +975,7 @@ impl Editor { color_capability: crate::view::color_support::ColorCapability, filesystem: Arc, ) -> AnyhowResult { - let grammar_registry = - crate::primitives::grammar::GrammarRegistry::for_editor(dir_context.config_dir.clone()); + let grammar_registry = crate::primitives::grammar::GrammarRegistry::defaults_only(); Self::with_options( config, width, @@ -998,8 +1006,8 @@ impl Editor { time_source: Option, grammar_registry: Option>, ) -> AnyhowResult { - let grammar_registry = grammar_registry - .unwrap_or_else(|| crate::primitives::grammar::GrammarRegistry::empty()); + let grammar_registry = + grammar_registry.unwrap_or_else(crate::primitives::grammar::GrammarRegistry::empty); Self::with_options( config, width, @@ -1062,11 +1070,6 @@ impl Editor { // Set terminal cursor color to match theme theme.set_terminal_cursor_color(); - tracing::info!( - "Grammar registry has {} syntaxes", - grammar_registry.available_syntaxes().len() - ); - let keybindings = KeybindingResolver::new(&config); // Create an empty initial buffer @@ -1274,6 +1277,27 @@ impl Editor { } } + // Spawn background thread to build the full grammar registry + // (includes embedded grammars, user grammars, and language packs). + // The defaults-only registry is used until this completes. + let grammar_build_in_progress = enable_plugins; // only needed when plugins may register grammars + { + let grammar_sender = async_bridge.sender(); + let grammar_config_dir = dir_context.config_dir.clone(); + std::thread::Builder::new() + .name("grammar-build".to_string()) + .spawn(move || { + let registry = + crate::primitives::grammar::GrammarRegistry::for_editor(grammar_config_dir); + let _ = grammar_sender.send( + crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt { + registry, + }, + ); + }) + .ok(); + } + // Extract config values before moving config into the struct let file_explorer_width = config.file_explorer.width; let recovery_enabled = config.editor.recovery_enabled; @@ -1309,6 +1333,8 @@ impl Editor { dir_context: dir_context.clone(), grammar_registry, pending_grammars: Vec::new(), + grammar_reload_pending: false, + grammar_build_in_progress, theme, theme_registry, theme_cache, @@ -4643,6 +4669,41 @@ impl Editor { exit_code, ); } + AsyncMessage::GrammarRegistryBuilt { registry } => { + tracing::info!( + "Background grammar build completed ({} syntaxes)", + registry.available_syntaxes().len() + ); + self.grammar_registry = registry; + self.grammar_build_in_progress = false; + + // Re-detect syntax for all open buffers with the full registry + let buffers_to_update: Vec<_> = self + .buffer_metadata + .iter() + .filter_map(|(id, meta)| meta.file_path().map(|p| (*id, p.to_path_buf()))) + .collect(); + + for (buf_id, path) in buffers_to_update { + if let Some(state) = self.buffers.get_mut(&buf_id) { + let detected = + crate::primitives::detected_language::DetectedLanguage::from_path( + &path, + &self.grammar_registry, + &self.config.languages, + ); + + if detected.highlighter.has_highlighting() + || !state.highlighter.has_highlighting() + { + state.apply_language(detected); + } + } + } + + // Flush any plugin grammars that arrived during the build + self.flush_pending_grammars(); + } } } diff --git a/crates/fresh-editor/src/app/plugin_commands.rs b/crates/fresh-editor/src/app/plugin_commands.rs index 7f07b1ba2..e79c45496 100644 --- a/crates/fresh-editor/src/app/plugin_commands.rs +++ b/crates/fresh-editor/src/app/plugin_commands.rs @@ -1510,32 +1510,55 @@ impl Editor { } /// Handle ReloadGrammars command - /// Rebuilds the grammar registry with pending grammars and invalidates highlight caches + /// Defers the actual rebuild — sets a flag so all pending grammars from the + /// current command batch are collected before a single rebuild. pub(super) fn handle_reload_grammars(&mut self) { - use crate::primitives::grammar::GrammarRegistry; - use std::path::PathBuf; - - tracing::info!( - "[SYNTAX DEBUG] handle_reload_grammars called, pending_grammars count: {}", + tracing::debug!( + "ReloadGrammars requested, pending_grammars count: {}", self.pending_grammars.len() ); + self.grammar_reload_pending = true; + } + + /// Flush pending grammars: spawn a background rebuild if any ReloadGrammars + /// commands were received during this command batch. + /// + /// Called after processing all plugin commands in a batch, so that multiple + /// RegisterGrammar+ReloadGrammars pairs result in only one rebuild. + /// The rebuild happens on a background thread; when complete, a + /// `GrammarRegistryBuilt` message swaps in the new registry. + pub(super) fn flush_pending_grammars(&mut self) { + if !self.grammar_reload_pending { + return; + } + self.grammar_reload_pending = false; + + // If a background build is already in progress, it will call + // flush_pending_grammars() again when it completes — so just + // re-arm the flag and return. + if self.grammar_build_in_progress { + self.grammar_reload_pending = true; + tracing::debug!("Grammar build in progress, deferring flush"); + return; + } + + use std::path::PathBuf; if self.pending_grammars.is_empty() { - tracing::debug!("ReloadGrammars called but no pending grammars"); + tracing::debug!("Grammar reload requested but no pending grammars"); return; } + tracing::info!( + "Flushing {} pending grammars via background rebuild", + self.pending_grammars.len() + ); + // Collect pending grammars let additional: Vec<_> = self .pending_grammars .drain(..) .map(|g| { - tracing::info!( - "[SYNTAX DEBUG] pending grammar: lang='{}', path='{}', extensions={:?}", - g.language, - g.grammar_path, - g.extensions - ); ( g.language.clone(), PathBuf::from(g.grammar_path), @@ -1544,8 +1567,6 @@ impl Editor { }) .collect(); - let grammar_count = additional.len(); - // Update config.languages with the extensions so detect_language() works for (language, _path, extensions) in &additional { let lang_config = self @@ -1553,78 +1574,36 @@ impl Editor { .languages .entry(language.clone()) .or_insert_with(Default::default); - // Add extensions that aren't already present for ext in extensions { if !lang_config.extensions.contains(ext) { lang_config.extensions.push(ext.clone()); } } - tracing::info!( - "[SYNTAX DEBUG] updated config.languages['{}']: extensions={:?}, grammar='{}'", - language, - lang_config.extensions, - lang_config.grammar - ); } - tracing::info!( - "[SYNTAX DEBUG] before rebuild: registry has {} syntaxes, user_extensions: {}", - self.grammar_registry.available_syntaxes().len(), - self.grammar_registry.user_extensions_debug() - ); - - // Rebuild registry with pending grammars - match GrammarRegistry::with_additional_grammars(&self.grammar_registry, &additional) { - Some(new_registry) => { - tracing::info!( - "[SYNTAX DEBUG] after rebuild: new registry has {} syntaxes, user_extensions: {}", - new_registry.available_syntaxes().len(), - new_registry.user_extensions_debug() - ); - self.grammar_registry = std::sync::Arc::new(new_registry); - - // Re-detect syntax for all buffers that might now have highlighting - // Collect buffer IDs and paths first to avoid borrow issues - let buffers_to_update: Vec<_> = self - .buffer_metadata - .iter() - .filter_map(|(id, meta)| meta.file_path().map(|p| (*id, p.to_path_buf()))) - .collect(); - - for (buf_id, path) in buffers_to_update { - if let Some(state) = self.buffers.get_mut(&buf_id) { - let detected = - crate::primitives::detected_language::DetectedLanguage::from_path( - &path, - &self.grammar_registry, - &self.config.languages, - ); - - // Only update if the new engine has highlighting capability - // or if the current one doesn't (don't downgrade) - if detected.highlighter.has_highlighting() - || !state.highlighter.has_highlighting() - { - state.apply_language(detected); - tracing::debug!( - "Updated syntax highlighting for {:?}", - path.file_name() + // Spawn background rebuild + let base_registry = std::sync::Arc::clone(&self.grammar_registry); + if let Some(bridge) = &self.async_bridge { + let sender = bridge.sender(); + self.grammar_build_in_progress = true; + std::thread::Builder::new() + .name("grammar-rebuild".to_string()) + .spawn(move || { + use crate::primitives::grammar::GrammarRegistry; + match GrammarRegistry::with_additional_grammars(&base_registry, &additional) { + Some(new_registry) => { + let _ = sender.send( + crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt { + registry: std::sync::Arc::new(new_registry), + }, ); } + None => { + tracing::error!("Failed to rebuild grammar registry in background"); + } } - } - - // Emit event for plugins that might want to react - self.emit_event( - "grammars_changed", - serde_json::json!({ "count": grammar_count }), - ); - - tracing::info!("Grammars reloaded ({} new grammars)", grammar_count); - } - None => { - tracing::error!("Failed to rebuild grammar registry"); - } + }) + .ok(); } } } diff --git a/crates/fresh-editor/src/app/render.rs b/crates/fresh-editor/src/app/render.rs index 1874142b8..66718ccb5 100644 --- a/crates/fresh-editor/src/app/render.rs +++ b/crates/fresh-editor/src/app/render.rs @@ -365,6 +365,9 @@ impl Editor { tracing::error!("Error handling plugin command: {}", e); } } + + // Flush any deferred grammar rebuilds as a single batch + self.flush_pending_grammars(); } // Render editor content (same for both layouts) diff --git a/crates/fresh-editor/src/primitives/grammar/types.rs b/crates/fresh-editor/src/primitives/grammar/types.rs index b1ebfcc9d..d7d48e3d1 100644 --- a/crates/fresh-editor/src/primitives/grammar/types.rs +++ b/crates/fresh-editor/src/primitives/grammar/types.rs @@ -46,6 +46,14 @@ pub const TYPST_GRAMMAR: &str = include_str!("../../grammars/typst.sublime-synta /// /// This struct holds the compiled syntax set and provides lookup methods. /// It does not perform I/O directly - use `GrammarLoader` for loading grammars. +impl std::fmt::Debug for GrammarRegistry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GrammarRegistry") + .field("syntax_count", &self.syntax_set.syntaxes().len()) + .finish() + } +} + pub struct GrammarRegistry { /// Combined syntax set (built-in + embedded + user grammars) syntax_set: Arc, @@ -87,6 +95,23 @@ impl GrammarRegistry { }) } + /// Create a registry with only syntect's pre-compiled defaults (~0ms). + /// + /// This provides instant syntax highlighting for ~50 common languages + /// (Rust, Python, JS/TS, C/C++, Go, Java, HTML, CSS, Markdown, etc.) + /// without any `SyntaxSetBuilder::build()` call. Use this at startup, + /// then swap in a full registry built on a background thread. + pub fn defaults_only() -> Arc { + let syntax_set = SyntaxSet::load_defaults_newlines(); + let filename_scopes = Self::build_filename_scopes(); + Arc::new(Self { + syntax_set: Arc::new(syntax_set), + user_extensions: HashMap::new(), + filename_scopes, + loaded_grammar_paths: Vec::new(), + }) + } + /// Build the default filename -> scope mappings for dotfiles and special files. pub fn build_filename_scopes() -> HashMap { let mut map = HashMap::new(); diff --git a/crates/fresh-editor/src/services/async_bridge.rs b/crates/fresh-editor/src/services/async_bridge.rs index 127f27f05..0c76c7470 100644 --- a/crates/fresh-editor/src/services/async_bridge.rs +++ b/crates/fresh-editor/src/services/async_bridge.rs @@ -241,6 +241,11 @@ pub enum AsyncMessage { status: LspServerStatus, message: Option, }, + + /// Background grammar build completed — swap in the new registry + GrammarRegistryBuilt { + registry: std::sync::Arc, + }, } /// LSP progress value types From be02ac6f1661fed0e4eb4ca452083b8aab24781a Mon Sep 17 00:00:00 2001 From: Noam Lewis Date: Tue, 24 Feb 2026 23:41:27 +0200 Subject: [PATCH 3/5] Drain buffered input events before rendering for CPU-constrained responsiveness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under CPU pressure (e.g., cgroups), terminal.draw() can take 500-800ms wall clock time due to repeated process descheduling. During that time, keyboard events buffer in the kernel. Previously the loop would render after each event, causing N keystrokes to trigger N expensive draws. Restructure the event loop to drain all pending events (non-blocking poll) before drawing a single frame. After each event, recompute_layout_cached() updates the visual layout (view_line_mappings, viewport sync, scroll groups) so subsequent events see correct state — same pattern as macro replay. Only the terminal draw is deferred. Resize events break out of the drain loop immediately since they require a real render to establish new frame dimensions. Co-Authored-By: Claude Opus 4.6 --- crates/fresh-editor/src/app/render.rs | 9 ++ crates/fresh-editor/src/main.rs | 150 ++++++++++++++++---------- 2 files changed, 104 insertions(+), 55 deletions(-) diff --git a/crates/fresh-editor/src/app/render.rs b/crates/fresh-editor/src/app/render.rs index 66718ccb5..c3e0442fb 100644 --- a/crates/fresh-editor/src/app/render.rs +++ b/crates/fresh-editor/src/app/render.rs @@ -4316,6 +4316,15 @@ impl Editor { format!("{} → Play Macro", palette_key) } + /// Recompute layout using the last rendered frame dimensions. + /// Used by the event loop to keep layout caches fresh between events + /// when drawing is deferred (e.g., draining buffered input). + pub fn recompute_layout_cached(&mut self) { + let w = self.cached_layout.last_frame_width; + let h = self.cached_layout.last_frame_height; + self.recompute_layout(w, h); + } + /// Recompute the view_line_mappings layout without drawing. /// Used during macro replay so that visual-line movements (MoveLineEnd, /// MoveUp, MoveDown on wrapped lines) see correct, up-to-date layout diff --git a/crates/fresh-editor/src/main.rs b/crates/fresh-editor/src/main.rs index dd26d0511..ffe08858a 100644 --- a/crates/fresh-editor/src/main.rs +++ b/crates/fresh-editor/src/main.rs @@ -2932,76 +2932,116 @@ where break; } - if needs_render && last_render.elapsed() >= FRAME_DURATION { - { - let _span = tracing::info_span!("terminal_draw").entered(); - terminal.draw(|frame| editor.render(frame))?; - } - last_render = Instant::now(); - needs_render = false; - } - - let event = if let Some(e) = pending_event.take() { - Some(e) - } else { - let timeout = if needs_render { - FRAME_DURATION.saturating_sub(last_render.elapsed()) + // Drain all pending input events before rendering. + // Under CPU pressure (cgroups, etc.), terminal.draw() takes long wall-clock + // time during which input buffers in the kernel. Draining first ensures all + // buffered keystrokes are applied to a single frame rather than triggering + // multiple expensive draws. + // + // After each event, recompute_layout() updates the cached visual layout + // (view_line_mappings, viewport sync, scroll groups) so that subsequent + // events see correct layout state — same as macro replay does. + // The actual terminal draw is deferred until after the drain. + // + // The drain is capped at 500ms to guarantee periodic renders even under + // sustained input (e.g., held key repeat). This is deliberately longer + // than FRAME_DURATION — under CPU pressure, a shorter cap would defeat + // the purpose of batching. + let drain_deadline = Instant::now() + Duration::from_millis(500); + loop { + let event = if let Some(e) = pending_event.take() { + Some(e) } else { - Duration::from_millis(50) + poll_event(Duration::ZERO)? }; - poll_event(timeout)? - }; - - let Some(event) = event else { continue }; + let Some(event) = event else { break }; - let (event, next) = coalesce_mouse_moves(event)?; - pending_event = next; + let (event, next) = coalesce_mouse_moves(event)?; + pending_event = next; - // Event debug dialog receives ALL RAW events (before any translation or processing) - // This is essential for diagnosing terminal keybinding issues - if editor.is_event_debug_active() { - if let CrosstermEvent::Key(key_event) = event { - if key_event.kind == KeyEventKind::Press { - editor.handle_event_debug_input(&key_event); - needs_render = true; + // Event debug dialog receives ALL RAW events (before any translation or processing) + // This is essential for diagnosing terminal keybinding issues + if editor.is_event_debug_active() { + if let CrosstermEvent::Key(key_event) = event { + if key_event.kind == KeyEventKind::Press { + editor.handle_event_debug_input(&key_event); + needs_render = true; + } } + // Consume all events while event debug is active + continue; } - // Consume all events while event debug is active - continue; - } - match event { - CrosstermEvent::Key(key_event) => { - if key_event.kind == KeyEventKind::Press { - let _span = tracing::trace_span!( - "handle_key", - code = ?key_event.code, - modifiers = ?key_event.modifiers, - ) - .entered(); - // Apply key translation (for input calibration) - // Use editor's translator so calibration changes take effect immediately - let translated_event = editor.key_translator().translate(key_event); - handle_key_event(editor, translated_event)?; + match event { + CrosstermEvent::Key(key_event) => { + if key_event.kind == KeyEventKind::Press { + let _span = tracing::trace_span!( + "handle_key", + code = ?key_event.code, + modifiers = ?key_event.modifiers, + ) + .entered(); + // Apply key translation (for input calibration) + // Use editor's translator so calibration changes take effect immediately + let translated_event = editor.key_translator().translate(key_event); + handle_key_event(editor, translated_event)?; + needs_render = true; + } + } + CrosstermEvent::Mouse(mouse_event) => { + if handle_mouse_event(editor, mouse_event)? { + needs_render = true; + } + } + CrosstermEvent::Resize(w, h) => { + editor.resize(w, h); needs_render = true; + // Resize needs a real render to establish new frame + // dimensions. Stop draining and fall through to draw. + break; } - } - CrosstermEvent::Mouse(mouse_event) => { - if handle_mouse_event(editor, mouse_event)? { + CrosstermEvent::Paste(text) => { + // External paste from terminal (bracketed paste mode) + editor.paste_text(text); needs_render = true; } + _ => {} } - CrosstermEvent::Resize(w, h) => { - editor.resize(w, h); - needs_render = true; + + // Update layout caches after each event so subsequent events in + // this drain batch see correct visual layout (wrapped lines, + // viewport positions, scroll sync). Drawing is deferred. + if needs_render { + editor.recompute_layout_cached(); } - CrosstermEvent::Paste(text) => { - // External paste from terminal (bracketed paste mode) - editor.paste_text(text); - needs_render = true; + + if Instant::now() >= drain_deadline { + break; + } + } + + if needs_render && last_render.elapsed() >= FRAME_DURATION { + { + let _span = tracing::info_span!("terminal_draw").entered(); + terminal.draw(|frame| editor.render(frame))?; } - _ => {} + last_render = Instant::now(); + needs_render = false; + // Loop back immediately to drain any events that arrived during + // the render before considering another frame. + continue; + } + + // Block until next event or timeout + let timeout = if needs_render { + FRAME_DURATION.saturating_sub(last_render.elapsed()) + } else { + Duration::from_millis(50) + }; + + if let Some(event) = poll_event(timeout)? { + pending_event = Some(event); } } From c9dd301fc6e119581ee1ac593ab142b2a2c2b52b Mon Sep 17 00:00:00 2001 From: Noam Lewis Date: Tue, 24 Feb 2026 23:57:33 +0200 Subject: [PATCH 4/5] Fix clippy let_underscore_must_use lint on channel sends Co-Authored-By: Claude Opus 4.6 --- crates/fresh-editor/src/app/mod.rs | 5 +++-- crates/fresh-editor/src/app/plugin_commands.rs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/fresh-editor/src/app/mod.rs b/crates/fresh-editor/src/app/mod.rs index 6b8c55d7b..94a694a97 100644 --- a/crates/fresh-editor/src/app/mod.rs +++ b/crates/fresh-editor/src/app/mod.rs @@ -1289,11 +1289,12 @@ impl Editor { .spawn(move || { let registry = crate::primitives::grammar::GrammarRegistry::for_editor(grammar_config_dir); - let _ = grammar_sender.send( + // Ok to ignore: receiver may be gone if app is shutting down. + drop(grammar_sender.send( crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt { registry, }, - ); + )); }) .ok(); } diff --git a/crates/fresh-editor/src/app/plugin_commands.rs b/crates/fresh-editor/src/app/plugin_commands.rs index e79c45496..a4f0c2bf4 100644 --- a/crates/fresh-editor/src/app/plugin_commands.rs +++ b/crates/fresh-editor/src/app/plugin_commands.rs @@ -1592,11 +1592,12 @@ impl Editor { use crate::primitives::grammar::GrammarRegistry; match GrammarRegistry::with_additional_grammars(&base_registry, &additional) { Some(new_registry) => { - let _ = sender.send( + // Ok to ignore: receiver may be gone if app is shutting down. + drop(sender.send( crate::services::async_bridge::AsyncMessage::GrammarRegistryBuilt { registry: std::sync::Arc::new(new_registry), }, - ); + )); } None => { tracing::error!("Failed to rebuild grammar registry in background"); From 9bfb1e69918b858169ed1d20f8e57315ff77f5da Mon Sep 17 00:00:00 2001 From: Noam Lewis Date: Tue, 24 Feb 2026 23:58:34 +0200 Subject: [PATCH 5/5] cargo clippy --fix && cargo fmt --- .../fresh-editor/src/app/buffer_management.rs | 7 ++--- crates/fresh-editor/src/app/file_explorer.rs | 2 +- .../fresh-editor/src/app/file_operations.rs | 9 ++++--- crates/fresh-editor/src/app/input.rs | 8 +++--- .../src/app/keybinding_editor/editor.rs | 5 +--- crates/fresh-editor/src/app/mod.rs | 9 +++---- .../fresh-editor/src/app/on_save_actions.rs | 12 +++------ .../fresh-editor/src/app/plugin_commands.rs | 6 +---- crates/fresh-editor/src/app/prompt_actions.rs | 9 +++---- crates/fresh-editor/src/app/render.rs | 8 +++--- crates/fresh-editor/src/app/split_actions.rs | 6 +---- crates/fresh-editor/src/app/workspace.rs | 6 ++--- crates/fresh-editor/src/input/actions.rs | 4 +-- crates/fresh-editor/src/input/line_move.rs | 2 +- .../src/input/quick_open/providers.rs | 2 +- crates/fresh-editor/src/main.rs | 6 ++--- crates/fresh-editor/src/model/buffer.rs | 12 ++++----- crates/fresh-editor/src/model/filesystem.rs | 8 +++--- crates/fresh-editor/src/model/piece_tree.rs | 12 ++------- .../fresh-editor/src/server/input_parser.rs | 2 +- crates/fresh-editor/src/server/ipc/mod.rs | 27 +++++++------------ .../fresh-editor/src/services/fs/manager.rs | 12 ++++----- .../src/services/process_limits.rs | 2 +- .../src/services/remote/connection.rs | 5 +--- .../src/services/remote/filesystem.rs | 2 +- .../src/services/remote/spawner.rs | 2 +- .../fresh-editor/src/view/settings/state.rs | 8 +++--- crates/fresh-editor/src/view/theme/loader.rs | 2 +- .../fresh-editor/src/view/ui/file_explorer.rs | 2 +- .../src/view/ui/split_rendering.rs | 16 +++++------ 30 files changed, 81 insertions(+), 132 deletions(-) diff --git a/crates/fresh-editor/src/app/buffer_management.rs b/crates/fresh-editor/src/app/buffer_management.rs index 7f2a3e4c3..a9b28fba0 100644 --- a/crates/fresh-editor/src/app/buffer_management.rs +++ b/crates/fresh-editor/src/app/buffer_management.rs @@ -2570,9 +2570,10 @@ impl Editor { None => return Ok(()), }; - let rt = self.tokio_runtime.as_ref().ok_or_else(|| { - std::io::Error::new(std::io::ErrorKind::Other, "async runtime not available") - })?; + let rt = self + .tokio_runtime + .as_ref() + .ok_or_else(|| std::io::Error::other("async runtime not available"))?; let io_results: Vec> = rt.block_on(async { let mut handles = Vec::with_capacity(io_work.len()); diff --git a/crates/fresh-editor/src/app/file_explorer.rs b/crates/fresh-editor/src/app/file_explorer.rs index 96a992017..252f07b18 100644 --- a/crates/fresh-editor/src/app/file_explorer.rs +++ b/crates/fresh-editor/src/app/file_explorer.rs @@ -566,7 +566,7 @@ impl Editor { let delete_result = if self.filesystem.remote_connection_info().is_some() { self.move_to_remote_trash(&path) } else { - trash::delete(&path).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + trash::delete(&path).map_err(|e| std::io::Error::other(e)) }; match delete_result { diff --git a/crates/fresh-editor/src/app/file_operations.rs b/crates/fresh-editor/src/app/file_operations.rs index cdb96c120..d02c8a565 100644 --- a/crates/fresh-editor/src/app/file_operations.rs +++ b/crates/fresh-editor/src/app/file_operations.rs @@ -903,10 +903,11 @@ impl Editor { let path = self.active_state().buffer.file_path()?; // Get current file modification time - let current_mtime = match self.filesystem.metadata(path).ok().and_then(|m| m.modified) { - Some(mtime) => mtime, - None => return None, // File doesn't exist or can't read metadata - }; + let current_mtime = self + .filesystem + .metadata(path) + .ok() + .and_then(|m| m.modified)?; // Compare with our recorded modification time match self.file_mod_times.get(path) { diff --git a/crates/fresh-editor/src/app/input.rs b/crates/fresh-editor/src/app/input.rs index 75c9ea3cf..fa0f91ae3 100644 --- a/crates/fresh-editor/src/app/input.rs +++ b/crates/fresh-editor/src/app/input.rs @@ -311,7 +311,7 @@ impl Editor { let has_line_index = self .buffers .get(&self.active_buffer()) - .map_or(true, |s| s.buffer.line_count().is_some()); + .is_none_or(|s| s.buffer.line_count().is_some()); if has_line_index { self.start_prompt( t!("file.goto_line_prompt").to_string(), @@ -1380,7 +1380,7 @@ impl Editor { .unwrap_or(0); if let Some(view_state) = self .composite_view_states - .get_mut(&(active_split.into(), buffer_id)) + .get_mut(&(active_split, buffer_id)) { view_state.scroll(delta as isize, max_row); tracing::trace!( @@ -2922,7 +2922,7 @@ impl Editor { // Add all available syntaxes from the grammar registry (100+ languages) let mut syntax_names: Vec<&str> = self.grammar_registry.available_syntaxes(); // Sort alphabetically for easier navigation - syntax_names.sort_unstable_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); + syntax_names.sort_unstable_by_key(|a| a.to_lowercase()); let mut current_index_found = None; for syntax_name in syntax_names { @@ -2935,7 +2935,7 @@ impl Editor { // config key, e.g. "rust" not "Rust"). let is_current = self .resolve_language_id(syntax_name) - .map_or(false, |id| id == current_language); + .is_some_and(|id| id == current_language); if is_current { current_index_found = Some(suggestions.len()); } diff --git a/crates/fresh-editor/src/app/keybinding_editor/editor.rs b/crates/fresh-editor/src/app/keybinding_editor/editor.rs index 88e74e790..8b5c2d696 100644 --- a/crates/fresh-editor/src/app/keybinding_editor/editor.rs +++ b/crates/fresh-editor/src/app/keybinding_editor/editor.rs @@ -628,10 +628,7 @@ impl KeybindingEditor { /// Apply the edit dialog to create/update a binding. /// Returns an error message if validation fails. pub fn apply_edit_dialog(&mut self) -> Option { - let dialog = match self.edit_dialog.take() { - Some(d) => d, - None => return None, - }; + let dialog = self.edit_dialog.take()?; if dialog.key_code.is_none() || dialog.action_text.is_empty() { self.edit_dialog = Some(dialog); diff --git a/crates/fresh-editor/src/app/mod.rs b/crates/fresh-editor/src/app/mod.rs index 94a694a97..b5f3aedf3 100644 --- a/crates/fresh-editor/src/app/mod.rs +++ b/crates/fresh-editor/src/app/mod.rs @@ -3419,9 +3419,8 @@ impl Editor { /// Update Quick Open suggestions based on current input fn update_quick_open_suggestions(&mut self, input: &str) { - let suggestions = if input.starts_with('>') { + let suggestions = if let Some(query) = input.strip_prefix('>') { // Command mode - let query = &input[1..]; let active_buffer_mode = self .buffer_metadata .get(&self.active_buffer()) @@ -3444,13 +3443,11 @@ impl Editor { active_buffer_mode, has_lsp_config, ) - } else if input.starts_with('#') { + } else if let Some(query) = input.strip_prefix('#') { // Buffer mode - let query = &input[1..]; self.get_buffer_suggestions(query) - } else if input.starts_with(':') { + } else if let Some(line_str) = input.strip_prefix(':') { // Go to line mode - let line_str = &input[1..]; self.get_goto_line_suggestions(line_str) } else { // File mode (default) diff --git a/crates/fresh-editor/src/app/on_save_actions.rs b/crates/fresh-editor/src/app/on_save_actions.rs index ee3af4f11..450de2dd1 100644 --- a/crates/fresh-editor/src/app/on_save_actions.rs +++ b/crates/fresh-editor/src/app/on_save_actions.rs @@ -37,16 +37,12 @@ impl Editor { let mut ran_any_action = false; // Run whitespace cleanup actions first (before formatter) - if self.config.editor.trim_trailing_whitespace_on_save { - if self.trim_trailing_whitespace()? { - ran_any_action = true; - } + if self.config.editor.trim_trailing_whitespace_on_save && self.trim_trailing_whitespace()? { + ran_any_action = true; } - if self.config.editor.ensure_final_newline_on_save { - if self.ensure_final_newline()? { - ran_any_action = true; - } + if self.config.editor.ensure_final_newline_on_save && self.ensure_final_newline()? { + ran_any_action = true; } // If whitespace cleanup made changes, re-save diff --git a/crates/fresh-editor/src/app/plugin_commands.rs b/crates/fresh-editor/src/app/plugin_commands.rs index a4f0c2bf4..baf998600 100644 --- a/crates/fresh-editor/src/app/plugin_commands.rs +++ b/crates/fresh-editor/src/app/plugin_commands.rs @@ -1569,11 +1569,7 @@ impl Editor { // Update config.languages with the extensions so detect_language() works for (language, _path, extensions) in &additional { - let lang_config = self - .config - .languages - .entry(language.clone()) - .or_insert_with(Default::default); + let lang_config = self.config.languages.entry(language.clone()).or_default(); for ext in extensions { if !lang_config.extensions.contains(ext) { lang_config.extensions.push(ext.clone()); diff --git a/crates/fresh-editor/src/app/prompt_actions.rs b/crates/fresh-editor/src/app/prompt_actions.rs index ea62a2723..424325636 100644 --- a/crates/fresh-editor/src/app/prompt_actions.rs +++ b/crates/fresh-editor/src/app/prompt_actions.rs @@ -1095,21 +1095,18 @@ impl Editor { selected_index: Option, ) -> PromptResult { // Determine the mode based on prefix - if input.starts_with('>') { + if let Some(query) = input.strip_prefix('>') { // Command mode - find and execute the selected command - let query = &input[1..]; return self.handle_quick_open_command(query, selected_index); } - if input.starts_with('#') { + if let Some(query) = input.strip_prefix('#') { // Buffer mode - switch to selected buffer - let query = &input[1..]; return self.handle_quick_open_buffer(query, selected_index); } - if input.starts_with(':') { + if let Some(line_str) = input.strip_prefix(':') { // Go to line mode - let line_str = &input[1..]; if let Ok(line_num) = line_str.parse::() { if line_num > 0 { self.goto_line_col(line_num, None); diff --git a/crates/fresh-editor/src/app/render.rs b/crates/fresh-editor/src/app/render.rs index c3e0442fb..c8701cd75 100644 --- a/crates/fresh-editor/src/app/render.rs +++ b/crates/fresh-editor/src/app/render.rs @@ -2257,11 +2257,9 @@ impl Editor { let (new_pos, new_sticky) = match &visual_action { VisualAction::UpDown { direction, .. } => { // Calculate current visual column from cached layout - let current_visual_col = - match self.cached_layout.byte_to_visual_column(split_id, position) { - Some(col) => col, - None => return None, - }; + let current_visual_col = self + .cached_layout + .byte_to_visual_column(split_id, position)?; let goal_visual_col = if sticky_column > 0 { sticky_column diff --git a/crates/fresh-editor/src/app/split_actions.rs b/crates/fresh-editor/src/app/split_actions.rs index 5b52e59e4..20dfc9223 100644 --- a/crates/fresh-editor/src/app/split_actions.rs +++ b/crates/fresh-editor/src/app/split_actions.rs @@ -179,11 +179,7 @@ impl Editor { // Ensure the active tab is visible in the newly active split let split_id = self.split_manager.active_split(); - self.ensure_active_tab_visible( - split_id.into(), - self.active_buffer(), - self.effective_tabs_width(), - ); + self.ensure_active_tab_visible(split_id, self.active_buffer(), self.effective_tabs_width()); let buffer_id = self.active_buffer(); diff --git a/crates/fresh-editor/src/app/workspace.rs b/crates/fresh-editor/src/app/workspace.rs index b57eda659..c985cb864 100644 --- a/crates/fresh-editor/src/app/workspace.rs +++ b/crates/fresh-editor/src/app/workspace.rs @@ -819,8 +819,7 @@ impl Editor { // Restore label if present if let Some(label) = label { - self.split_manager - .set_label(current_leaf_id.into(), label.clone()); + self.split_manager.set_label(current_leaf_id, label.clone()); } // Restore the view state for this split @@ -854,8 +853,7 @@ impl Editor { // Restore label if present if let Some(label) = label { - self.split_manager - .set_label(current_leaf_id.into(), label.clone()); + self.split_manager.set_label(current_leaf_id, label.clone()); } self.split_manager diff --git a/crates/fresh-editor/src/input/actions.rs b/crates/fresh-editor/src/input/actions.rs index 9a09ac9bf..776b40a3b 100644 --- a/crates/fresh-editor/src/input/actions.rs +++ b/crates/fresh-editor/src/input/actions.rs @@ -1839,7 +1839,7 @@ pub fn action_to_events( let mut found_pos = None; while let Some((line_start, line_content)) = iter.prev() { // Check if this is an empty line (only whitespace/newline) - let trimmed = line_content.trim_end_matches(|c| c == '\n' || c == '\r'); + let trimmed = line_content.trim_end_matches(['\n', '\r']); if trimmed.is_empty() || trimmed.chars().all(char::is_whitespace) { found_pos = Some(line_start); break; @@ -1876,7 +1876,7 @@ pub fn action_to_events( let mut found_pos = None; while let Some((line_start, line_content)) = iter.next_line() { // Check if this is an empty line (only whitespace/newline) - let trimmed = line_content.trim_end_matches(|c| c == '\n' || c == '\r'); + let trimmed = line_content.trim_end_matches(['\n', '\r']); if trimmed.is_empty() || trimmed.chars().all(char::is_whitespace) { found_pos = Some(line_start); break; diff --git a/crates/fresh-editor/src/input/line_move.rs b/crates/fresh-editor/src/input/line_move.rs index 4bad7839d..02cc6561b 100644 --- a/crates/fresh-editor/src/input/line_move.rs +++ b/crates/fresh-editor/src/input/line_move.rs @@ -228,7 +228,7 @@ pub(crate) fn move_lines( .map(|(cursor_id, cursor)| { ( cursor_id, - cursor.selection_range().map(|range| range.clone()), + cursor.selection_range(), cursor.position, cursor.anchor, cursor.sticky_column, diff --git a/crates/fresh-editor/src/input/quick_open/providers.rs b/crates/fresh-editor/src/input/quick_open/providers.rs index 1aa88a559..421fdac88 100644 --- a/crates/fresh-editor/src/input/quick_open/providers.rs +++ b/crates/fresh-editor/src/input/quick_open/providers.rs @@ -411,7 +411,7 @@ impl FileProvider { .try_git_files(cwd) .or_else(|| self.try_fd_files(cwd)) .or_else(|| self.try_find_files(cwd)) - .unwrap_or_else(Vec::new); + .unwrap_or_default(); // Add frecency scores let files: Vec = files diff --git a/crates/fresh-editor/src/main.rs b/crates/fresh-editor/src/main.rs index ffe08858a..cb5b866f9 100644 --- a/crates/fresh-editor/src/main.rs +++ b/crates/fresh-editor/src/main.rs @@ -1675,7 +1675,7 @@ fn create_plugin_package( "#, name, if description.is_empty() { - format!("A Fresh plugin") + "A Fresh plugin".to_string() } else { description.to_string() }, @@ -1776,7 +1776,7 @@ fn create_theme_package( "#, name, if description.is_empty() { - format!("A Fresh theme") + "A Fresh theme".to_string() } else { description.to_string() }, @@ -1905,7 +1905,7 @@ fn create_language_package( "#, name, if description.is_empty() { - format!("Language support for Fresh") + "Language support for Fresh".to_string() } else { description.to_string() }, diff --git a/crates/fresh-editor/src/model/buffer.rs b/crates/fresh-editor/src/model/buffer.rs index a00cb70ff..482930122 100644 --- a/crates/fresh-editor/src/model/buffer.rs +++ b/crates/fresh-editor/src/model/buffer.rs @@ -1268,7 +1268,7 @@ impl TextBuffer { ) -> io::Result<()> { const CHUNK_SIZE: usize = 1024 * 1024; // 1MB chunks - let file_size = self.fs.metadata(src_path)?.size as u64; + let file_size = self.fs.metadata(src_path)?.size; let mut offset = 0u64; while offset < file_size { @@ -1820,7 +1820,7 @@ impl TextBuffer { // querying the entire leaf. This handles unloaded segments in // large file mode after line scanning has populated the metadata. if start == 0 && len == leaf.bytes { - leaf.line_feed_cnt.map(|c| c as usize) + leaf.line_feed_cnt.map(|c| c) } else { tracing::warn!( "diff line_counter: returning None for partial leaf query: \ @@ -2204,11 +2204,9 @@ impl TextBuffer { .map(|b| !b.is_loaded()) .unwrap_or(false); - if needs_loading { - if self.chunk_split_and_load(&piece_view, current_offset)? { - restarted_iteration = true; - break; - } + if needs_loading && self.chunk_split_and_load(&piece_view, current_offset)? { + restarted_iteration = true; + break; } // Calculate the range to read from this piece diff --git a/crates/fresh-editor/src/model/filesystem.rs b/crates/fresh-editor/src/model/filesystem.rs index d33f6adea..00ad84207 100644 --- a/crates/fresh-editor/src/model/filesystem.rs +++ b/crates/fresh-editor/src/model/filesystem.rs @@ -860,9 +860,7 @@ impl FileSystem for StdFileSystem { .stdout(Stdio::null()) .stderr(Stdio::piped()) .spawn() - .map_err(|e| { - io::Error::new(io::ErrorKind::Other, format!("failed to spawn sudo: {}", e)) - })?; + .map_err(|e| io::Error::other(format!("failed to spawn sudo: {}", e)))?; if let Some(mut stdin) = child.stdin.take() { use std::io::Write; @@ -883,7 +881,7 @@ impl FileSystem for StdFileSystem { .args(["chmod", &format!("{:o}", mode), &path.to_string_lossy()]) .status()?; if !status.success() { - return Err(io::Error::new(io::ErrorKind::Other, "sudo chmod failed")); + return Err(io::Error::other("sudo chmod failed")); } // Set ownership via sudo chown @@ -895,7 +893,7 @@ impl FileSystem for StdFileSystem { ]) .status()?; if !status.success() { - return Err(io::Error::new(io::ErrorKind::Other, "sudo chown failed")); + return Err(io::Error::other("sudo chown failed")); } Ok(()) diff --git a/crates/fresh-editor/src/model/piece_tree.rs b/crates/fresh-editor/src/model/piece_tree.rs index e84d1b372..18699882e 100644 --- a/crates/fresh-editor/src/model/piece_tree.rs +++ b/crates/fresh-editor/src/model/piece_tree.rs @@ -1114,16 +1114,8 @@ impl PieceTree { } // Partial overlap - keep parts outside delete range - let before_bytes = if delete_start > piece_start { - delete_start - piece_start - } else { - 0 - }; - let after_bytes = if delete_end < piece_end { - piece_end - delete_end - } else { - 0 - }; + let before_bytes = delete_start.saturating_sub(piece_start); + let after_bytes = piece_end.saturating_sub(delete_end); if before_bytes > 0 && after_bytes > 0 { // Delete in the middle of this leaf - split into two diff --git a/crates/fresh-editor/src/server/input_parser.rs b/crates/fresh-editor/src/server/input_parser.rs index 7c690a5a0..730debb92 100644 --- a/crates/fresh-editor/src/server/input_parser.rs +++ b/crates/fresh-editor/src/server/input_parser.rs @@ -514,7 +514,7 @@ fn byte_to_keycode(byte: u8) -> KeyCode { 28..=31 => KeyCode::Char((b'\\' + byte - 28) as char), 32 => KeyCode::Char(' '), 127 => KeyCode::Backspace, - b if b >= 32 && b < 127 => KeyCode::Char(b as char), + b if (32..127).contains(&b) => KeyCode::Char(b as char), _ => KeyCode::Null, } } diff --git a/crates/fresh-editor/src/server/ipc/mod.rs b/crates/fresh-editor/src/server/ipc/mod.rs index 77d846f2e..a1ab5f489 100644 --- a/crates/fresh-editor/src/server/ipc/mod.rs +++ b/crates/fresh-editor/src/server/ipc/mod.rs @@ -207,17 +207,8 @@ impl ServerListener { pub fn accept(&mut self) -> io::Result> { // Try to accept on control socket first (client connects here first) // Use set_nonblocking for non-blocking accept - if let Err(e) = self - .control_listener - .set_nonblocking(ListenerNonblockingMode::Accept) - { - // On Windows, set_nonblocking might fail if the pipe is in a certain state - #[cfg(windows)] - if platform_windows::is_transient_pipe_error(&e) { - return Ok(None); - } - return Err(e); - } + self.control_listener + .set_nonblocking(ListenerNonblockingMode::Accept)?; let control_stream = match self.control_listener.accept() { Ok(stream) => stream, @@ -282,7 +273,7 @@ impl StreamWrapper { pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> { self.0 .lock() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex poisoned"))? + .map_err(|_| io::Error::other("mutex poisoned"))? .set_nonblocking(nonblocking) } @@ -291,7 +282,7 @@ impl StreamWrapper { let mut guard = self .0 .lock() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex poisoned"))?; + .map_err(|_| io::Error::other("mutex poisoned"))?; Write::write_all(&mut *guard, buf) } @@ -300,7 +291,7 @@ impl StreamWrapper { let mut guard = self .0 .lock() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex poisoned"))?; + .map_err(|_| io::Error::other("mutex poisoned"))?; Write::flush(&mut *guard) } @@ -315,11 +306,11 @@ impl StreamWrapper { )); } Err(std::sync::TryLockError::Poisoned(_)) => { - return Err(io::Error::new(io::ErrorKind::Other, "mutex poisoned")); + return Err(io::Error::other("mutex poisoned")); } }; - platform::try_read_nonblocking(&mut *guard, buf) + platform::try_read_nonblocking(&mut guard, buf) } } @@ -340,7 +331,7 @@ impl Read for StreamWrapper { let result = self .0 .lock() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex poisoned"))? + .map_err(|_| io::Error::other("mutex poisoned"))? .read(buf); map_windows_pipe_error(result) } @@ -351,7 +342,7 @@ impl Read for &StreamWrapper { let result = self .0 .lock() - .map_err(|_| io::Error::new(io::ErrorKind::Other, "mutex poisoned"))? + .map_err(|_| io::Error::other("mutex poisoned"))? .read(buf); map_windows_pipe_error(result) } diff --git a/crates/fresh-editor/src/services/fs/manager.rs b/crates/fresh-editor/src/services/fs/manager.rs index a18f0e18d..00dfdfd69 100644 --- a/crates/fresh-editor/src/services/fs/manager.rs +++ b/crates/fresh-editor/src/services/fs/manager.rs @@ -74,7 +74,7 @@ impl FsManager { let path_clone = path.clone(); let result = tokio::task::spawn_blocking(move || fs.read_dir(&path_clone)) .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| io::Error::other(e.to_string()))?; // Notify all waiting requesters let mut pending = self.pending_dir_requests.lock().await; @@ -120,7 +120,7 @@ impl FsManager { for task in tasks { match task.await { Ok(result) => results.push(result), - Err(e) => results.push(Err(io::Error::new(io::ErrorKind::Other, e.to_string()))), + Err(e) => results.push(Err(io::Error::other(e.to_string()))), } } @@ -133,7 +133,7 @@ impl FsManager { let path = path.to_path_buf(); tokio::task::spawn_blocking(move || fs.metadata(&path)) .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))? + .map_err(|e| io::Error::other(e.to_string()))? } /// Check if a path exists @@ -151,7 +151,7 @@ impl FsManager { let path = path.to_path_buf(); tokio::task::spawn_blocking(move || fs.is_dir(&path)) .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))? + .map_err(|e| io::Error::other(e.to_string()))? } /// Get a complete entry for a path (with metadata) @@ -204,7 +204,7 @@ impl FsManager { } }) .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))? + .map_err(|e| io::Error::other(e.to_string()))? } /// Get canonical path @@ -213,7 +213,7 @@ impl FsManager { let path = path.to_path_buf(); tokio::task::spawn_blocking(move || fs.canonicalize(&path)) .await - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))? + .map_err(|e| io::Error::other(e.to_string()))? } /// List directory and fetch metadata for all entries in parallel diff --git a/crates/fresh-editor/src/services/process_limits.rs b/crates/fresh-editor/src/services/process_limits.rs index 9535d1384..b9618a6bd 100644 --- a/crates/fresh-editor/src/services/process_limits.rs +++ b/crates/fresh-editor/src/services/process_limits.rs @@ -318,7 +318,7 @@ fn apply_memory_limit_setrlimit(bytes: u64) -> io::Result<()> { // Set RLIMIT_AS (address space / virtual memory limit) // On 32-bit platforms, rlim_t is u32, so we need to convert carefully. // If bytes exceeds what rlim_t can represent, clamp to rlim_t::MAX. - let limit = bytes.min(nix::libc::rlim_t::MAX as u64) as nix::libc::rlim_t; + let limit = bytes.min(nix::libc::rlim_t::MAX) as nix::libc::rlim_t; setrlimit(Resource::RLIMIT_AS, limit, limit) .map_err(|e| io::Error::other(format!("setrlimit AS failed: {}", e))) } diff --git a/crates/fresh-editor/src/services/remote/connection.rs b/crates/fresh-editor/src/services/remote/connection.rs index 55f7e1125..13e9f9f66 100644 --- a/crates/fresh-editor/src/services/remote/connection.rs +++ b/crates/fresh-editor/src/services/remote/connection.rs @@ -218,10 +218,7 @@ impl Drop for SshConnection { // If it fails (process already exited, permission error, etc.) // there's nothing we can do in a Drop impl — the OS will clean // up the zombie when our process exits. - match self.process.start_kill() { - Ok(()) => {} - Err(_) => {} - } + if let Ok(()) = self.process.start_kill() {} } } diff --git a/crates/fresh-editor/src/services/remote/filesystem.rs b/crates/fresh-editor/src/services/remote/filesystem.rs index 4d37a13e0..38f710a25 100644 --- a/crates/fresh-editor/src/services/remote/filesystem.rs +++ b/crates/fresh-editor/src/services/remote/filesystem.rs @@ -60,7 +60,7 @@ impl RemoteFileSystem { }; io::Error::new(kind, msg) } - e => io::Error::new(io::ErrorKind::Other, e.to_string()), + e => io::Error::other(e.to_string()), } } diff --git a/crates/fresh-editor/src/services/remote/spawner.rs b/crates/fresh-editor/src/services/remote/spawner.rs index c71a993b8..23482e3a5 100644 --- a/crates/fresh-editor/src/services/remote/spawner.rs +++ b/crates/fresh-editor/src/services/remote/spawner.rs @@ -133,7 +133,7 @@ impl ProcessSpawner for RemoteProcessSpawner { let result = result_rx .await .map_err(|_| SpawnError::Channel(ChannelError::ChannelClosed))? - .map_err(|e| SpawnError::Process(e))?; + .map_err(SpawnError::Process)?; let exit_code = result .get("code") diff --git a/crates/fresh-editor/src/view/settings/state.rs b/crates/fresh-editor/src/view/settings/state.rs index 4df8f74b7..7e3f1305c 100644 --- a/crates/fresh-editor/src/view/settings/state.rs +++ b/crates/fresh-editor/src/view/settings/state.rs @@ -1611,11 +1611,9 @@ impl SettingsState { } } self.on_value_changed(); - } else { - if let Some(item) = self.current_item_mut() { - if let SettingControl::Json(state) = &mut item.control { - state.revert(); - } + } else if let Some(item) = self.current_item_mut() { + if let SettingControl::Json(state) = &mut item.control { + state.revert(); } } self.editing_text = false; diff --git a/crates/fresh-editor/src/view/theme/loader.rs b/crates/fresh-editor/src/view/theme/loader.rs index 7c6947291..72835659f 100644 --- a/crates/fresh-editor/src/view/theme/loader.rs +++ b/crates/fresh-editor/src/view/theme/loader.rs @@ -15,7 +15,7 @@ use super::types::{Theme, ThemeFile, ThemeInfo, BUILTIN_THEMES}; /// This ensures that theme names can be matched regardless of how they appear /// in filenames vs. JSON content (e.g., "Catppuccin Mocha" matches "catppuccin-mocha"). pub fn normalize_theme_name(name: &str) -> String { - name.to_lowercase().replace('_', "-").replace(' ', "-") + name.to_lowercase().replace(['_', ' '], "-") } /// A registry holding all loaded themes. diff --git a/crates/fresh-editor/src/view/ui/file_explorer.rs b/crates/fresh-editor/src/view/ui/file_explorer.rs index f7c0abf66..15bd9c534 100644 --- a/crates/fresh-editor/src/view/ui/file_explorer.rs +++ b/crates/fresh-editor/src/view/ui/file_explorer.rs @@ -113,7 +113,7 @@ impl FileExplorerRenderer { // Extract just the hostname from "user@host" or "user@host:port" let hostname = host .split('@') - .last() + .next_back() .unwrap_or(host) .split(':') .next() diff --git a/crates/fresh-editor/src/view/ui/split_rendering.rs b/crates/fresh-editor/src/view/ui/split_rendering.rs index 66779cc01..affbdca67 100644 --- a/crates/fresh-editor/src/view/ui/split_rendering.rs +++ b/crates/fresh-editor/src/view/ui/split_rendering.rs @@ -1088,7 +1088,7 @@ impl SplitRenderer { .as_deref() .and_then(|vs| vs.get(&split_id)) .map(|vs| vs.cursors.clone()) - .unwrap_or_else(crate::model::cursor::Cursors::new); + .unwrap_or_default(); // Resolve hidden fold byte ranges so ensure_visible can skip // folded lines when counting distance to the cursor. let hidden_ranges: Vec<(usize, usize)> = split_view_states @@ -1333,7 +1333,7 @@ impl SplitRenderer { let split_cursors = split_view_states .get(&split_id) .map(|vs| vs.cursors.clone()) - .unwrap_or_else(crate::model::cursor::Cursors::new); + .unwrap_or_default(); // Resolve hidden fold byte ranges so ensure_visible can skip // folded lines when counting distance to the cursor. let hidden_ranges: Vec<(usize, usize)> = split_view_states @@ -2557,7 +2557,7 @@ impl SplitRenderer { let viewport_end = tokens .iter() .filter_map(|t| t.source_offset) - .last() + .next_back() .unwrap_or(viewport.top_byte) + 1; let soft_breaks = state.soft_breaks.query_viewport( @@ -2576,7 +2576,7 @@ impl SplitRenderer { let viewport_end = tokens .iter() .filter_map(|t| t.source_offset) - .last() + .next_back() .unwrap_or(viewport.top_byte) + 1; let conceal_ranges = @@ -3937,10 +3937,8 @@ impl SplitRenderer { // Skip markdown compose overlays in Source mode — they should only // render in the Compose-mode split. - if !is_compose { - if overlay.namespace.as_ref() == Some(&md_emphasis_ns) { - continue; - } + if !is_compose && overlay.namespace.as_ref() == Some(&md_emphasis_ns) { + continue; } viewport_overlays.push((overlay.clone(), range)); @@ -5247,7 +5245,7 @@ impl SplitRenderer { .map(|m| m.line_end_byte) .unwrap_or(0); let near_buffer_end = last_mapped_byte + 2 >= state.buffer.len(); - let already_mapped = view_line_mappings.last().map_or(false, |m| { + let already_mapped = view_line_mappings.last().is_some_and(|m| { m.char_source_bytes.is_empty() && m.line_end_byte == state.buffer.len() }); if near_buffer_end && !already_mapped {