Skip to content

Commit 00d2dd2

Browse files
committed
Naive spellbook integration
1 parent 0dd2529 commit 00d2dd2

File tree

7 files changed

+86
-3
lines changed

7 files changed

+86
-3
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ tree-sitter = { version = "0.22" }
4242
nucleo = "0.5.0"
4343
slotmap = "1.0.7"
4444
thiserror = "1.0"
45+
spellbook = { git = "https://github.com/helix-editor/spellbook.git" }
4546

4647
[workspace.package]
4748
version = "24.7.0"

helix-loader/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ fn find_runtime_file(rel_path: &Path) -> Option<PathBuf> {
107107
/// The valid runtime directories are searched in priority order and the first
108108
/// file found to exist is returned, otherwise the path to the final attempt
109109
/// that failed.
110-
pub fn runtime_file(rel_path: &Path) -> PathBuf {
110+
pub fn runtime_file<P: AsRef<Path>>(rel_path: P) -> PathBuf {
111+
let rel_path = rel_path.as_ref();
111112
find_runtime_file(rel_path).unwrap_or_else(|| {
112113
RUNTIME_DIRS
113114
.last()

helix-term/src/ui/editor.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ use helix_core::{
2020
unicode::width::UnicodeWidthStr,
2121
visual_offset_from_block, Change, Position, Range, Selection, Transaction,
2222
};
23+
use helix_stdx::rope::RopeSliceExt;
2324
use helix_view::{
2425
annotations::diagnostics::DiagnosticFilter,
2526
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
2627
editor::{CompleteAction, CursorShapeConfig},
2728
graphics::{Color, CursorKind, Modifier, Rect, Style},
2829
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
2930
keyboard::{KeyCode, KeyModifiers},
30-
Document, Editor, Theme, View,
31+
Dictionary, Document, Editor, Theme, View,
3132
};
3233
use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
3334

@@ -154,6 +155,10 @@ impl EditorView {
154155
}
155156
overlay_highlights = Box::new(syntax::merge(overlay_highlights, diagnostic));
156157
}
158+
let spell = Self::doc_spell_highlights(&editor.dictionary, doc, view, theme);
159+
if !spell.is_empty() {
160+
overlay_highlights = Box::new(syntax::merge(overlay_highlights, spell));
161+
}
157162

158163
if is_focused {
159164
let mut highlights = syntax::merge(
@@ -506,6 +511,55 @@ impl EditorView {
506511
]
507512
}
508513

514+
pub fn doc_spell_highlights(
515+
dict: &Dictionary,
516+
doc: &Document,
517+
view: &View,
518+
theme: &Theme,
519+
) -> Vec<(usize, std::ops::Range<usize>)> {
520+
// This is **very** ***very*** naive and not at all reflective of what the actual
521+
// integration will look like. Doing this per-render is very needlessly expensive.
522+
// Instead it should be done in the background and possibly incrementally (only
523+
// re-checking ranges that are affected by document changes). However regex-cursor
524+
// is very fast and so is spellbook (degenerate cases max out at 1μs in a release
525+
// build on my machine, i.e. a worst case throughput of 2 million words / second) so
526+
// this is suitable for my testing. I mostly want to find cases where spellbook's
527+
// results are surprising.
528+
// Also we want to use tree-sitter to mark nodes as ones that should be spellchecked
529+
// and maybe specify strategies for doing tokenization (try to tokenize prose vs.
530+
// programming languages).
531+
// Plus these should really be proper diagnostics so that we can pull them up in the
532+
// diagnostics picker and jump to them.
533+
use helix_stdx::rope::Regex;
534+
use once_cell::sync::Lazy;
535+
use std::borrow::Cow;
536+
static WORDS: Lazy<Regex> = Lazy::new(|| Regex::new(r#"[0-9A-Z]*(['-]?[a-z]+)*"#).unwrap());
537+
538+
let mut spans = Vec::new();
539+
let error = theme.find_scope_index("diagnostic.error").unwrap();
540+
541+
let text = doc.text().slice(..);
542+
let start = text.line_to_char(text.char_to_line(doc.view_offset(view.id).anchor));
543+
let end = text.line_to_char(view.estimate_last_doc_line(doc) + 1);
544+
545+
for match_ in WORDS.find_iter(text.regex_input_at(start..end)) {
546+
let range = text.byte_to_char(match_.start())..text.byte_to_char(match_.end());
547+
// TODO: consider how to allow passing the RopeSlice to spellbook:
548+
// * Use an Input trait like regex-cursor?
549+
// * Accept `impl Iterator<Item = char>`?
550+
// * Maybe spellbook should have an internal `String` buffer and it should try to copy
551+
// the word into that? Only in the best case do you not have to allocate at all.
552+
// Maybe we should use a single string buffer and perform all changes to the string
553+
// in-place instead of using `replace` from the stdlib and Cows.
554+
let word = Cow::from(text.slice(range.clone()));
555+
if !dict.check(&word) {
556+
spans.push((error, range))
557+
}
558+
}
559+
560+
spans
561+
}
562+
509563
/// Get highlight spans for selections in a document view.
510564
pub fn doc_selection_highlights(
511565
mode: Mode,

helix-view/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ log = "~0.4"
5252
parking_lot = "0.12.3"
5353
thiserror.workspace = true
5454

55+
spellbook.workspace = true
56+
5557
[target.'cfg(windows)'.dependencies]
5658
clipboard-win = { version = "5.4", features = ["std"] }
5759

helix-view/src/editor.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::{
1111
register::Registers,
1212
theme::{self, Theme},
1313
tree::{self, Tree},
14-
Document, DocumentId, View, ViewId,
14+
Dictionary, Document, DocumentId, View, ViewId,
1515
};
1616
use dap::StackFrame;
1717
use helix_event::dispatch;
@@ -1087,6 +1087,9 @@ pub struct Editor {
10871087

10881088
pub mouse_down_range: Option<Range>,
10891089
pub cursor_cache: CursorCache,
1090+
1091+
/// HACK:
1092+
pub dictionary: Dictionary,
10901093
}
10911094

10921095
pub type Motion = Box<dyn Fn(&mut Editor)>;
@@ -1167,6 +1170,16 @@ impl Editor {
11671170
// HAXX: offset the render area height by 1 to account for prompt/commandline
11681171
area.height -= 1;
11691172

1173+
// HACK:
1174+
let aff =
1175+
std::fs::read_to_string(helix_loader::runtime_file("dictionaries/en_US/en_US.aff"))
1176+
.unwrap();
1177+
let dic =
1178+
std::fs::read_to_string(helix_loader::runtime_file("dictionaries/en_US/en_US.dic"))
1179+
.unwrap();
1180+
let mut dictionary = Dictionary::new(&aff, &dic).unwrap();
1181+
dictionary.add("khepri").unwrap();
1182+
11701183
Self {
11711184
mode: Mode::Normal,
11721185
tree: Tree::new(area),
@@ -1205,6 +1218,7 @@ impl Editor {
12051218
handlers,
12061219
mouse_down_range: None,
12071220
cursor_cache: CursorCache::default(),
1221+
dictionary,
12081222
}
12091223
}
12101224

helix-view/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,6 @@ pub fn align_view(doc: &mut Document, view: &View, align: Align) {
7575
pub use document::Document;
7676
pub use editor::Editor;
7777
use helix_core::char_idx_at_visual_offset;
78+
pub use spellbook::Dictionary;
7879
pub use theme::Theme;
7980
pub use view::View;

0 commit comments

Comments
 (0)