From 9624d72d7e3d0de19cad116a3abd1e06af487e1f Mon Sep 17 00:00:00 2001 From: Simon Gardling Date: Wed, 30 Mar 2022 17:23:02 -0400 Subject: [PATCH] implement ACTUAL autocompletion --- Cargo.toml | 1 + build.rs | 112 +++++++++++++++++++++++---------------------- src/egui_app.rs | 42 ++--------------- src/function.rs | 46 ++++++++++++++++++- src/suggestions.rs | 60 ++++++++++++++++++------ 5 files changed, 154 insertions(+), 107 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b448b24..fcbe7b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ phf = "0.10.1" shadow-rs = "0.11.0" command-run = "1.1.1" phf_codegen = "0.10.0" +itertools = "0.10.3" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] instant = { version = "0.1.12" } diff --git a/build.rs b/build.rs index 91625a1..dce1d4e 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,4 @@ +use itertools::Itertools; use std::collections::HashSet; use std::env; use std::fs::File; @@ -23,7 +24,7 @@ fn generate_hashmap() { write!( &mut file, - "static COMPLETION_HASHMAP: phf::Map<&'static str, &'static str> = {}", + "static COMPLETION_HASHMAP: phf::Map<&'static str, HintEnum> = {}", compile_hashmap().build() ) .unwrap(); @@ -36,69 +37,70 @@ const SUPPORTED_FUNCTIONS: [&str; 22] = [ "round", "ceil", "trunc", "fract", "exp", "sqrt", "cbrt", "ln", "log2", "log10", ]; -const QUOTE: char = '"'; -fn compile_hashmap() -> phf_codegen::Map { - let mut tuple_list: Vec<(String, String)> = Vec::new(); +const CONST: char = '"'; - for entry in SUPPORTED_FUNCTIONS +fn compile_hashmap() -> phf_codegen::Map { + let functions_processed: Vec = SUPPORTED_FUNCTIONS .iter() - .map(|entry| format!("{}(", entry)) - .collect::>() - { - for i in 1..entry.len() { - let (first, last) = entry.split_at(i); - tuple_list.push((first.to_string(), last.to_string())); + .map(|e| e.to_string() + "(") + .collect(); + + let mut seen = HashSet::new(); + + let powerset = functions_processed + .into_iter() + .map(|func| func.chars().collect::>()) + .powerset() + .flatten() + .filter(|e| e.len() > 1) + .filter(|ele| { + if seen.contains(ele) { + return false; + } else { + seen.insert(ele.clone()); + return true; + } + }) + .collect::>>(); + + let mut tuple_list_1: Vec<(String, String)> = Vec::new(); + + let mut seen_2: HashSet<(String, String)> = HashSet::new(); + for ele in powerset { + for i in 1..ele.len() { + let string = ele.clone().into_iter().collect::(); + let (first, last) = string.split_at(i); + let data = (first.to_string(), last.to_string()); + if seen_2.contains(&data) { + continue; + } + seen_2.insert(data.clone()); + tuple_list_1.push(data) } } - let mut output: phf_codegen::Map = phf_codegen::Map::new(); + let keys: Vec<&String> = tuple_list_1.iter().map(|(a, _)| a).collect(); + let mut output = phf_codegen::Map::new(); + let mut seen_3: HashSet = HashSet::new(); - let key_list: Vec = tuple_list.iter().map(|(key, _)| key.clone()).collect(); - - let mut seen = HashSet::new(); - for (key, value) in tuple_list.clone() { - if seen.contains(&key) { + for (key, value) in tuple_list_1.iter() { + if seen_3.contains(&*key) { continue; } - seen.insert(key.clone()); - - let duplicate_num = key_list.iter().filter(|ele| **ele == key).count(); - if 1 == duplicate_num { - output.entry(key, &(QUOTE.to_string() + &value + "E.to_string())); - continue; - } - - let same_keys_merged: Vec = tuple_list - .iter() - .filter(|(a, _)| **a == key) - .map(|(a, b)| a.clone() + b) - .collect(); - - let merged_key_value = key.clone() + &value; - - let mut common_substr: Option = None; - for same_key in same_keys_merged { - if let Some(common_substr_unwrapped) = common_substr { - common_substr = common_substring(&common_substr_unwrapped, &same_key); - } else { - common_substr = common_substring(&same_key, &merged_key_value) - } - - if common_substr.is_none() { - break; - } - } - - if let Some(common_substr_unwrapped) = common_substr { - if !common_substr_unwrapped.is_empty() { - output.entry( - key.clone(), - &(QUOTE.to_string() - + &common_substr_unwrapped.replace(&key, "") - + "E.to_string()), - ); - } + seen_3.insert(key.clone()); + if keys.iter().filter(|a| a == &&key).count() == 1 { + output.entry( + key.clone(), + &format!("HintEnum::Single({}{}{})", CONST, value, CONST), + ); + } else { + let multi_data = tuple_list_1 + .iter() + .filter(|(a, _)| a == key) + .map(|(_, b)| b) + .collect::>(); + output.entry(key.clone(), &format!("HintEnum::Many(&{:?})", multi_data)); } } diff --git a/src/egui_app.rs b/src/egui_app.rs index f8cf263..a0d935e 100644 --- a/src/egui_app.rs +++ b/src/egui_app.rs @@ -2,14 +2,12 @@ use crate::consts::*; use crate::function::{FunctionEntry, Riemann, DEFAULT_FUNCTION_ENTRY}; use crate::misc::{dyn_mut_iter, option_vec_printer, JsonFileOutput, SerdeValueHelper}; use crate::parsing::{process_func_str, test_func}; -use crate::suggestions::generate_hint; +use crate::suggestions::auto_complete; use eframe::{egui, epi}; -use egui::epaint::text::cursor::{PCursor, RCursor}; use egui::plot::Plot; use egui::{ - text::CCursor, text_edit::CursorRange, Button, CentralPanel, Color32, ComboBox, Context, - FontData, FontDefinitions, FontFamily, Key, RichText, SidePanel, Slider, TextEdit, - TopBottomPanel, Vec2, Visuals, Widget, Window, + Button, CentralPanel, Color32, ComboBox, Context, FontData, FontDefinitions, FontFamily, Key, + RichText, SidePanel, Slider, TopBottomPanel, Vec2, Visuals, Window, }; use epi::Frame; use instant::Duration; @@ -503,40 +501,8 @@ impl MathApp { .clicked(); // Contains the function string in a text box that the user can edit - let hint = generate_hint(&self.func_strs[i]).unwrap_or_default(); - - let te_id = ui.make_persistent_id("text_edit_ac".to_string()); - - let func_edit_focus = TextEdit::singleline(&mut self.func_strs[i]) - .hint_text(&hint) - .hint_forward(true) - .id(te_id) - .ui(ui) - .has_focus(); - - // If in focus and right arrow key was pressed, apply hint - // TODO: change position of cursor - if func_edit_focus { + if function.auto_complete(ui, &mut self.func_strs[i]) { self.text_boxes_focused = true; - if ui.input().key_down(Key::ArrowRight) { - self.func_strs[i] += &hint; - let mut state = TextEdit::load_state(ui.ctx(), te_id).unwrap(); - state.set_cursor_range(Some(CursorRange::one( - egui::epaint::text::cursor::Cursor { - ccursor: CCursor { - index: 0, - prefer_next_row: false, - }, - rcursor: RCursor { row: 0, column: 0 }, - pcursor: PCursor { - paragraph: 0, - offset: 10000, - prefer_next_row: false, - }, - }, - ))); - TextEdit::store_state(ui.ctx(), te_id, state); - } } }); diff --git a/src/function.rs b/src/function.rs index 356cda4..7bd56a0 100644 --- a/src/function.rs +++ b/src/function.rs @@ -3,11 +3,16 @@ use crate::egui_app::AppSettings; use crate::misc::*; use crate::parsing::BackingFunction; -use eframe::{egui, epaint}; +use crate::suggestions::generate_hint; +use eframe::{egui, epaint, epaint}; +use egui::{ + epaint::text::cursor::Cursor, text::CCursor, text_edit::CursorRange, Key, TextEdit, Widget, +}; use egui::{ plot::{BarChart, PlotUi, Value}, widgets::plot::Bar, }; +use epaint::text::cursor::{PCursor, RCursor}; use epaint::Color32; use std::fmt::{self, Debug}; @@ -59,6 +64,8 @@ pub struct FunctionEntry { derivative_data: Option>, extrema_data: Option>, roots_data: Option>, + + auto_complete_i: Option, } impl Default for FunctionEntry { @@ -76,6 +83,7 @@ impl Default for FunctionEntry { derivative_data: None, extrema_data: None, roots_data: None, + auto_complete_i: None, } } } @@ -108,6 +116,42 @@ impl FunctionEntry { } } + pub fn auto_complete(&mut self, ui: &mut egui::Ui, string: &mut String) -> bool { + let te_id = ui.make_persistent_id("text_edit_ac".to_string()); + + let hint = generate_hint(&string).unwrap_or_default(); + + let func_edit_focus = egui::TextEdit::singleline(string) + .hint_text(&hint) + .hint_forward(true) + .id(te_id) + .ui(ui) + .has_focus(); + + // If in focus and right arrow key was pressed, apply hint + if func_edit_focus { + if ui.input().key_down(Key::ArrowRight) { + *string = string.clone() + &hint; + let mut state = TextEdit::load_state(ui.ctx(), te_id).unwrap(); + state.set_cursor_range(Some(CursorRange::one(Cursor { + ccursor: CCursor { + index: 0, + prefer_next_row: false, + }, + rcursor: RCursor { row: 0, column: 0 }, + pcursor: PCursor { + paragraph: 0, + offset: 10000, + prefer_next_row: false, + }, + }))); + TextEdit::store_state(ui.ctx(), te_id, state); + } + return true; + } + return false; + } + /// Creates and does the math for creating all the rectangles under the /// graph fn integral_rectangles( diff --git a/src/suggestions.rs b/src/suggestions.rs index d3f9686..ede8994 100644 --- a/src/suggestions.rs +++ b/src/suggestions.rs @@ -1,9 +1,9 @@ use crate::misc::chars_take; /// Generate a hint based on the input `input`, returns an `Option` -pub fn generate_hint(input: &str) -> Option { +pub fn generate_hint(input: &str) -> Option { if input.is_empty() { - return Some("x^2".to_owned()); + return Some(HintEnum::Single("x^2")); } let chars: Vec = input.chars().collect(); @@ -17,7 +17,7 @@ pub fn generate_hint(input: &str) -> Option { }); if open_parens > closed_parens { - return Some(")".to_owned()); + return Some(HintEnum::Single(")")); } let len = chars.len(); @@ -53,22 +53,49 @@ pub fn generate_hint(input: &str) -> Option { None } -include!(concat!(env!("OUT_DIR"), "/codegen.rs")); +#[derive(Clone, PartialEq)] +pub enum HintEnum<'a> { + Single(&'static str), + Many(&'a [&'static str]), +} +impl std::fmt::Debug for HintEnum<'static> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_string()) + } +} + +impl ToString for HintEnum<'static> { + fn to_string(&self) -> String { + match self { + HintEnum::Single(single_data) => single_data.to_string(), + HintEnum::Many(multi_data) => multi_data + .iter() + .map(|a| a.to_string()) + .collect::() + .to_string(), + } + } +} + +impl HintEnum<'static> { + pub fn ensure_single(&self) -> String { + match self { + HintEnum::Single(single_data) => single_data.to_string(), + HintEnum::Many(_) => String::new(), + } + } +} + +include!(concat!(env!("OUT_DIR"), "/codegen.rs")); /// Gets completion from `COMPLETION_HASHMAP` -pub fn get_completion(key: String) -> Option { +pub fn get_completion(key: String) -> Option> { if key.is_empty() { return None; } match COMPLETION_HASHMAP.get(&key) { - Some(data_x) => { - if data_x.is_empty() { - None - } else { - Some(data_x.to_string()) - } - } + Some(data_x) => Some(data_x.clone()), None => None, } } @@ -106,6 +133,7 @@ mod tests { } } + /* #[test] fn completion_hashmap_test() { let values = hashmap_test_gen(); @@ -119,7 +147,12 @@ mod tests { } ); - assert_eq!(get_completion(key.to_string()), value); + assert_eq!( + get_completion(key.to_string()) + + .unwrap_or(String::new()), + value.unwrap_or(String::new()) + ); } } @@ -170,4 +203,5 @@ mod tests { } values } + */ }