diff --git a/Cargo.toml b/Cargo.toml index f57c2e2..514c487 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "egui_code_editor" authors = ["Roman Chumak "] -version = "0.2.19" +version = "0.2.20" edition = "2024" license = "MIT" repository = "https://github.com/p4ymak/egui_code_editor" -description = "egui Code Editor widget with numbered lines and syntax highlighting.." +description = "egui Code Editor widget with numbered lines, syntax highlighting and auto-completion.." readme = "README.md" categories = ["gui", "text-editors"] -keywords = ["egui", "GUI", "editor", "syntax", "highlighting"] +keywords = ["egui", "GUI", "editor", "syntax", "highlighting", "completion"] [dependencies] egui = { version = "0.33", optional = true } diff --git a/src/completer/mod.rs b/src/completer/mod.rs index b270e1e..306648c 100644 --- a/src/completer/mod.rs +++ b/src/completer/mod.rs @@ -1,5 +1,7 @@ mod trie; +use std::collections::BTreeSet; + use crate::{ColorTheme, Syntax, Token, TokenType, format_token}; use egui::{ Event, Frame, Modifiers, Sense, Stroke, TextBuffer, text_edit::TextEditOutput, @@ -42,7 +44,7 @@ pub struct Completer { trie_syntax: Trie, trie_user: Option, variant_id: usize, - completions: Vec, + completions: BTreeSet, } impl Completer { @@ -75,15 +77,14 @@ impl Completer { return; } - let mut completions_syntax = self.trie_syntax.find_completions(&self.prefix); - completions_syntax.reverse(); - let mut completions_user = self + let completions_syntax = self.trie_syntax.find_completions(&self.prefix); + let completions_user = self .trie_user .as_ref() .map(|t| t.find_completions(&self.prefix)) .unwrap_or_default(); - completions_user.reverse(); - self.completions = [completions_syntax, completions_user].concat(); + self.completions = + BTreeSet::from_iter(completions_syntax.into_iter().chain(completions_user)); if self.completions.is_empty() { return; } @@ -106,7 +107,8 @@ impl Completer { } else if i.consume_key(Modifiers::NONE, egui::Key::Tab) { let completion = self .completions - .get(self.variant_id) + .iter() + .nth(self.variant_id) .map(String::from) .unwrap_or_default(); i.events.push(Event::Paste(completion)); @@ -197,33 +199,49 @@ impl Completer { .show(|ui| { ui.response().sense = Sense::empty(); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + let height = (fontsize + + ui.style().visuals.widgets.hovered.bg_stroke.width * 2.0 + + ui.style().spacing.button_padding.y * 2.0 + + ui.style().spacing.item_spacing.y) + * self.completions.len().min(10) as f32 + - ui.style().spacing.item_spacing.y; + ui.set_height(height); + + egui::ScrollArea::vertical() + .auto_shrink([true, true]) + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) + .show(ui, |ui| { + for (i, completion) in self.completions.iter().enumerate() { + let word = format!("{}{completion}", &self.prefix); + let token_type = match &word { + word if syntax.is_keyword(word) => TokenType::Keyword, + word if syntax.is_special(word) => TokenType::Special, + word if syntax.is_type(word) => TokenType::Type, + _ => TokenType::Literal, + }; + let fmt = format_token(theme, fontsize, token_type); + let colored_text = egui::text::LayoutJob::single_section(word, fmt); + let selected = i == self.variant_id; - for (i, completion) in self.completions.iter().enumerate() { - let word = format!("{}{completion}", &self.prefix); - let token_type = match &word { - word if syntax.is_keyword(word) => TokenType::Keyword, - word if syntax.is_special(word) => TokenType::Special, - word if syntax.is_type(word) => TokenType::Type, - _ => TokenType::Literal, - }; - let fmt = format_token(theme, fontsize, token_type); - let colored_text = egui::text::LayoutJob::single_section(word, fmt); - let selected = i == self.variant_id; - ui.add( - egui::Button::new(colored_text) - .sense(Sense::empty()) - .frame(true) - .fill(theme.bg()) - .stroke(if selected { - Stroke::new( - ui.style().visuals.widgets.hovered.bg_stroke.width, - theme.type_color(TokenType::Literal), - ) - } else { - Stroke::NONE - }), - ); - } + let button = ui.add( + egui::Button::new(colored_text) + .sense(Sense::empty()) + .frame(true) + .fill(theme.bg()) + .stroke(if selected { + Stroke::new( + ui.style().visuals.widgets.hovered.bg_stroke.width, + theme.type_color(TokenType::Literal), + ) + } else { + Stroke::NONE + }), + ); + if selected { + button.scroll_to_me(None); + } + } + }); }); } } diff --git a/src/completer/trie.rs b/src/completer/trie.rs index ed9eb38..5b768f6 100644 --- a/src/completer/trie.rs +++ b/src/completer/trie.rs @@ -98,10 +98,10 @@ impl Trie { let mut start = " ".to_string(); start.push_str(prefix); let mut part = start.chars().peekable(); - self.find_recursice(&mut part, &mut found); + self.find_recursive(&mut part, &mut found); found } - fn find_recursice<'a>(&'a self, part: &mut Peekable, found: &mut Option<&'a Trie>) { + fn find_recursive<'a>(&'a self, part: &mut Peekable, found: &mut Option<&'a Trie>) { if let Some(c) = part.next() && self.root == c { @@ -110,7 +110,7 @@ impl Trie { } self.leaves .iter() - .for_each(|l| l.find_recursice(&mut part.clone(), found)) + .for_each(|l| l.find_recursive(&mut part.clone(), found)) } } }