Skip to content
Open

Bringup #1107

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
475 changes: 239 additions & 236 deletions Cargo.lock

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions crates/fresh-editor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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 }

Expand All @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions crates/fresh-editor/src/app/async_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,9 @@ impl Editor {
}
}

// Flush any deferred grammar rebuilds as a single batch
self.flush_pending_grammars();

has_visual_commands
}

Expand Down
7 changes: 4 additions & 3 deletions crates/fresh-editor/src/app/buffer_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2377,9 +2377,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<std::io::Result<(usize, usize)>> = rt.block_on(async {
let mut handles = Vec::with_capacity(io_work.len());
Expand Down
2 changes: 1 addition & 1 deletion crates/fresh-editor/src/app/file_explorer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 5 additions & 4 deletions crates/fresh-editor/src/app/file_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -900,10 +900,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) {
Expand Down
8 changes: 4 additions & 4 deletions crates/fresh-editor/src/app/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,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(),
Expand Down Expand Up @@ -1345,7 +1345,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!(
Expand Down Expand Up @@ -2862,7 +2862,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 {
Expand All @@ -2875,7 +2875,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());
}
Expand Down
5 changes: 1 addition & 4 deletions crates/fresh-editor/src/app/keybinding_editor/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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);
Expand Down
125 changes: 85 additions & 40 deletions crates/fresh-editor/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,15 @@ pub struct Editor {
/// Pending grammars registered by plugins, waiting for reload_grammars() to apply
pending_grammars: Vec<PendingGrammar>,

/// 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,

Expand Down Expand Up @@ -872,8 +881,7 @@ impl Editor {
color_capability: crate::view::color_support::ColorCapability,
filesystem: Arc<dyn FileSystem + Send + Sync>,
) -> AnyhowResult<Self> {
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,
Expand Down Expand Up @@ -904,8 +912,8 @@ impl Editor {
time_source: Option<SharedTimeSource>,
grammar_registry: Option<Arc<crate::primitives::grammar::GrammarRegistry>>,
) -> AnyhowResult<Self> {
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,
Expand All @@ -925,7 +933,7 @@ impl Editor {
/// to verify editor behavior under various I/O conditions
#[allow(clippy::too_many_arguments)]
fn with_options(
mut config: Config,
config: Config,
width: u16,
height: u16,
working_dir: Option<PathBuf>,
Expand Down Expand Up @@ -968,11 +976,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
Expand Down Expand Up @@ -1147,33 +1150,41 @@ impl Editor {
);
}

// Load from all found plugin directories, respecting config
for plugin_dir in plugin_dirs {
tracing::info!("Loading TypeScript plugins from: {:?}", plugin_dir);
let (errors, discovered_plugins) =
plugin_manager.load_plugins_from_dir_with_config(&plugin_dir, &config.plugins);

// Merge discovered plugins into config
// discovered_plugins already contains the merged config (saved enabled state + discovered path)
for (name, plugin_config) in discovered_plugins {
config.plugins.insert(name, plugin_config);
}

if !errors.is_empty() {
for err in &errors {
tracing::error!("TypeScript plugin load error: {}", err);
}
// In debug/test builds, panic to surface plugin loading errors
#[cfg(debug_assertions)]
panic!(
"TypeScript plugin loading failed with {} error(s): {}",
errors.len(),
errors.join("; ")
);
}
// Fire-and-forget: send all plugin dirs to the plugin thread
if !plugin_dirs.is_empty() {
let dirs: Vec<(
std::path::PathBuf,
std::collections::HashMap<String, fresh_core::config::PluginConfig>,
)> = plugin_dirs
.into_iter()
.map(|dir| (dir, config.plugins.clone()))
.collect();
plugin_manager.load_plugins_from_dir_with_config(dirs);
}
}

// 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 = true;
{
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);
// Ok to ignore: receiver may be gone if app is shutting down.
drop(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;
Expand Down Expand Up @@ -1208,6 +1219,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,
ansi_background: None,
Expand Down Expand Up @@ -3190,9 +3203,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())
Expand All @@ -3205,13 +3217,11 @@ impl Editor {
&self.active_custom_contexts,
active_buffer_mode,
)
} 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)
Expand Down Expand Up @@ -4417,6 +4427,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();
}
}
}

Expand Down
12 changes: 4 additions & 8 deletions crates/fresh-editor/src/app/on_save_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading