Split app.rs into an app/ module tree
src/app.rs had grown to 3,700 lines, ~2,400 of them a single impl App block. Move it to src/app/mod.rs and spread the behaviour across eleven child modules grouped by feature: workspace, grammar, spelling, beats, find, editor, autocomplete, file_list, ui, style and util. The new modules are children of app rather than siblings, so they still reach App's private fields without widening its interface; methods and free helpers that are now used across module boundaries are marked pub(super). mod.rs keeps the state types, App::new and the eframe::App update loop. This is pure code motion - every non-blank line of the original file reappears exactly once, and the only edits are the pub(super) markers, the module scaffolding, and rewrapping five signatures that the added prefix pushed past 100 columns. Largest file is now editor.rs at 520 lines. Tests still 56/56, and cargo clippy --release reports the same five warnings as before the split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBWj9TphFMCoh7VHaSRnvQ
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
//! Offline spell checking: loading a bundled or system Hunspell dictionary,
|
||||
//! running checks in the background, and applying a correction.
|
||||
|
||||
use super::*;
|
||||
|
||||
impl App {
|
||||
/// Apply a spell-check suggestion to the buffer, keeping the remaining
|
||||
/// misspelling underlines aligned.
|
||||
pub(super) fn apply_spell_fix(&mut self, match_idx: usize, rep_idx: usize) {
|
||||
if self.buffer != self.spell_checked_text {
|
||||
return;
|
||||
}
|
||||
if splice_fix(&mut self.buffer, &mut self.spell_matches, match_idx, rep_idx) {
|
||||
self.dirty = true;
|
||||
self.find_needs_refresh = true;
|
||||
// Keep these matches valid without forcing a full re-check.
|
||||
self.spell_checked_text = self.buffer.clone();
|
||||
self.spell_status = match self.spell_matches.len() {
|
||||
0 => "No spelling issues".to_string(),
|
||||
1 => "1 spelling issue".to_string(),
|
||||
n => format!("{n} spelling issues"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Spell check (offline) ---------------------------------------------
|
||||
|
||||
/// (Re)load the dictionary named by `config.spell_language`, falling back to
|
||||
/// the default and then to whatever is available. Resets the live results so
|
||||
/// the buffer is re-checked against the new dictionary.
|
||||
pub(super) fn load_spell_dict(&mut self) {
|
||||
let want = self.config.spell_language.clone();
|
||||
let entry = self
|
||||
.spell_dicts
|
||||
.iter()
|
||||
.find(|d| d.id == want)
|
||||
.or_else(|| {
|
||||
self.spell_dicts
|
||||
.iter()
|
||||
.find(|d| d.id == crate::spell::DEFAULT_LANGUAGE)
|
||||
})
|
||||
.or_else(|| self.spell_dicts.first())
|
||||
.cloned();
|
||||
|
||||
match entry {
|
||||
Some(e) => match e.load() {
|
||||
Ok(dict) => {
|
||||
self.spell_dict = Some(Arc::new(dict));
|
||||
self.spell_status = format!("Dictionary: {}", e.id);
|
||||
}
|
||||
Err(err) => {
|
||||
self.spell_dict = None;
|
||||
self.spell_status = err;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
self.spell_dict = None;
|
||||
self.spell_status = "No spelling dictionary available".to_string();
|
||||
}
|
||||
}
|
||||
self.spell_matches.clear();
|
||||
self.spell_checked_text.clear();
|
||||
self.spell_dirty = true;
|
||||
self.spell_last_edit = None;
|
||||
}
|
||||
|
||||
/// Switch the active dictionary and persist the choice.
|
||||
pub(super) fn set_spell_language(&mut self, id: &str) {
|
||||
if self.config.spell_language == id {
|
||||
return;
|
||||
}
|
||||
self.config.spell_language = id.to_string();
|
||||
self.config.save();
|
||||
self.load_spell_dict();
|
||||
}
|
||||
|
||||
/// The status line for the spelling checker given a match count.
|
||||
pub(super) fn spell_count_status(n: usize) -> String {
|
||||
match n {
|
||||
0 => "No spelling issues".to_string(),
|
||||
1 => "1 spelling issue".to_string(),
|
||||
n => format!("{n} spelling issues"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pick up a finished background spell check.
|
||||
pub(super) fn poll_spell(&mut self) {
|
||||
let received = self.spell_rx.as_ref().and_then(|rx| rx.try_recv().ok());
|
||||
if let Some((text, matches)) = received {
|
||||
self.spell_rx = None;
|
||||
if self.spell_dict.is_some() {
|
||||
self.spell_status = Self::spell_count_status(matches.len());
|
||||
}
|
||||
self.spell_matches = matches;
|
||||
self.spell_checked_text = text;
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a background spell check of the current buffer if one is due:
|
||||
/// enabled, a dictionary loaded, a file open, the buffer changed, no check
|
||||
/// already running, and the debounce interval elapsed since the last edit.
|
||||
pub(super) fn maybe_start_spell_check(&mut self, ctx: &egui::Context) {
|
||||
if !self.config.spell_check || self.selected.is_none() || !self.spell_dirty {
|
||||
return;
|
||||
}
|
||||
// LanguageTool results already cover this exact buffer (spelling too), so
|
||||
// don't spend effort on a spell pass that wouldn't be shown.
|
||||
if self.lt_is_current() {
|
||||
return;
|
||||
}
|
||||
let Some(dict) = self.spell_dict.clone() else {
|
||||
return;
|
||||
};
|
||||
if self.spell_rx.is_some() {
|
||||
return;
|
||||
}
|
||||
// Debounce so we don't re-check on every keystroke.
|
||||
const DEBOUNCE: Duration = Duration::from_millis(400);
|
||||
if let Some(edited) = self.spell_last_edit {
|
||||
let elapsed = edited.elapsed();
|
||||
if elapsed < DEBOUNCE {
|
||||
ctx.request_repaint_after(DEBOUNCE - elapsed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.spell_dirty = false;
|
||||
let text = self.buffer.clone();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
self.spell_rx = Some(rx);
|
||||
let ctx = ctx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let matches = crate::spell::check(&dict, &text);
|
||||
let _ = tx.send((text, matches));
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user