From 1b30a5275fac306d6c8d31d3234d21425fc61e93 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Wed, 22 Mar 2023 21:10:37 +0400 Subject: [PATCH] Cursor aware Component Browser (#5770) Closes #5220 This PR implements the cursor-aware behavior of the CB, as described in [pivotal issue](https://www.pivotaltracker.com/n/projects/2539304/stories/183521918) https://user-images.githubusercontent.com/6566674/221807206-39f93cb4-8253-421d-a33a-33ac0aa56e54.mp4 https://user-images.githubusercontent.com/6566674/223124947-259153ca-e656-4349-87b5-47c06fd21af2.mp4 # Important Notes - The `intended_method` of the node's metadata is marked as deprecated, and all usages are removed. - It seems *all usages of Scala parser are removed from IDE*. We no longer use it to parse documentation for snippets. This is how the snippets docs look now: Screenshot 2023-02-28 at 13 18 11 --- .../double-representation/src/import.rs | 9 +- app/gui/language/ast/impl/src/crumbs.rs | 2 +- app/gui/language/ast/impl/src/opr.rs | 9 +- app/gui/language/ast/impl/src/repr.rs | 3 + app/gui/src/controller/graph.rs | 140 +- app/gui/src/controller/graph/executed.rs | 79 +- app/gui/src/controller/searcher.rs | 1534 ++++++----------- app/gui/src/controller/searcher/action.rs | 22 +- app/gui/src/controller/searcher/component.rs | 54 +- .../controller/searcher/component/builder.rs | 4 +- .../searcher/component/hardcoded.rs | 93 +- app/gui/src/controller/searcher/input.rs | 700 ++++++++ app/gui/src/model/module.rs | 8 +- app/gui/src/model/module/plain.rs | 3 +- app/gui/src/model/project/synchronized.rs | 2 +- app/gui/src/model/undo_redo.rs | 6 +- app/gui/src/presenter/searcher.rs | 36 +- app/gui/src/presenter/searcher/provider.rs | 14 +- app/gui/src/test.rs | 7 +- app/gui/src/tests.rs | 110 -- app/gui/suggestion-database/src/entry.rs | 18 +- app/gui/view/examples/icons/src/lib.rs | 1 + .../view/graph-editor/src/component/node.rs | 2 + .../src/component/node/action_bar.rs | 13 +- .../src/component/node/input/area.rs | 22 + app/gui/view/graph-editor/src/lib.rs | 59 +- app/gui/view/src/project.rs | 62 +- lib/rust/ensogl/component/text/src/buffer.rs | 21 +- .../component/text/src/component/text.rs | 20 +- 29 files changed, 1643 insertions(+), 1410 deletions(-) create mode 100644 app/gui/src/controller/searcher/input.rs diff --git a/app/gui/controller/double-representation/src/import.rs b/app/gui/controller/double-representation/src/import.rs index 9bbb8495821..2ac18b7d82f 100644 --- a/app/gui/controller/double-representation/src/import.rs +++ b/app/gui/controller/double-representation/src/import.rs @@ -73,7 +73,8 @@ impl Info { } } - /// Obtain the qualified name of the module. + /// Return the module path as [`QualifiedName`]. Returns [`Err`] if the path is not a valid + /// module name. pub fn qualified_module_name(&self) -> FallibleResult { QualifiedName::from_all_segments(&self.module) } @@ -108,12 +109,6 @@ impl Info { self.hash(&mut hasher); hasher.finish() } - - /// Return the module path as [`QualifiedName`]. Returns [`Err`] if the path is not a valid - /// module name. - pub fn module_qualified_name(&self) -> FallibleResult { - QualifiedName::from_all_segments(self.module.iter()) - } } impl Display for Info { diff --git a/app/gui/language/ast/impl/src/crumbs.rs b/app/gui/language/ast/impl/src/crumbs.rs index 65e1a0d8fde..4e03ab3b2de 100644 --- a/app/gui/language/ast/impl/src/crumbs.rs +++ b/app/gui/language/ast/impl/src/crumbs.rs @@ -672,7 +672,7 @@ pub trait TraversableAst: Sized { /// Recursively traverses AST to retrieve AST node located by given crumbs sequence. fn get_traversing(&self, crumbs: &[Crumb]) -> FallibleResult<&Ast>; - /// Get the `Ast` node corresponging to `Self`. + /// Get the `Ast` node corresponding to `Self`. fn my_ast(&self) -> FallibleResult<&Ast> { self.get_traversing(&[]) } diff --git a/app/gui/language/ast/impl/src/opr.rs b/app/gui/language/ast/impl/src/opr.rs index 54ed54e457f..70b033c3ccd 100644 --- a/app/gui/language/ast/impl/src/opr.rs +++ b/app/gui/language/ast/impl/src/opr.rs @@ -511,10 +511,11 @@ impl Chain { while !self.args.is_empty() { self.fold_arg() } - // TODO[ao] the only case when target is none is when chain have None target and empty - // arguments list. Such Chain cannot be generated from Ast, but someone could think that - // this is still a valid chain. To consider returning error here. - self.target.unwrap().arg + if let Some(target) = self.target { + target.arg + } else { + SectionSides { opr: self.operator.into() }.into() + } } /// True if all operands are set, i.e. there are no section shapes in this chain. diff --git a/app/gui/language/ast/impl/src/repr.rs b/app/gui/language/ast/impl/src/repr.rs index 179a1fbb7ce..c3ee8d8e2bc 100644 --- a/app/gui/language/ast/impl/src/repr.rs +++ b/app/gui/language/ast/impl/src/repr.rs @@ -51,6 +51,9 @@ pub const RAW_BLOCK_QUOTES: &str = "\"\"\""; /// Quotes opening block of the formatted text. pub const FMT_BLOCK_QUOTES: &str = "'''"; +/// A list of possible delimiters of the text literals. +pub const STRING_DELIMITERS: &[&str] = &[RAW_BLOCK_QUOTES, FMT_BLOCK_QUOTES, "\"", "'"]; + // ============= diff --git a/app/gui/src/controller/graph.rs b/app/gui/src/controller/graph.rs index e1faa3b4ef3..d88d96c661a 100644 --- a/app/gui/src/controller/graph.rs +++ b/app/gui/src/controller/graph.rs @@ -26,7 +26,6 @@ use engine_protocol::language_server; use parser::Parser; use span_tree::action::Action; use span_tree::action::Actions; -use span_tree::generate::context::CalledMethodInfo; use span_tree::generate::Context as SpanTreeContext; use span_tree::SpanTree; @@ -790,6 +789,20 @@ impl Handle { module::locate(&module_ast, &self.id) } + /// The span of the definition of this graph in the module's AST. + pub fn definition_span(&self) -> FallibleResult> { + let def = self.definition()?; + self.module.ast().range_of_descendant_at(&def.crumbs) + } + + /// The location of the last byte of the definition of this graph in the module's AST. + pub fn definition_end_location(&self) -> FallibleResult> { + let module_ast = self.module.ast(); + let module_repr: enso_text::Rope = module_ast.repr().into(); + let def_span = self.definition_span()?; + Ok(module_repr.offset_to_location_snapped(def_span.end)) + } + /// Updates the AST of the definition of this graph. #[profile(Debug)] pub fn update_definition_ast(&self, f: F) -> FallibleResult @@ -1023,24 +1036,6 @@ impl Handle { } } - -// === Span Tree Context === - -/// Span Tree generation context for a graph that does not know about execution. -/// -/// It just applies the information from the metadata. -impl span_tree::generate::Context for Handle { - fn call_info(&self, id: node::Id, name: Option<&str>) -> Option { - let db = &self.suggestion_db; - let metadata = self.module.node_metadata(id).ok()?; - let db_entry = db.lookup_method(metadata.intended_method?)?; - // If the name is different than intended method than apparently it is not intended anymore - // and should be ignored. - let matching = if let Some(name) = name { name == db_entry.name } else { true }; - matching.then(|| db_entry.invocation_info(db, &self.parser)) - } -} - impl model::undo_redo::Aware for Handle { fn undo_redo_repository(&self) -> Rc { self.module.undo_redo_repository() @@ -1069,15 +1064,11 @@ pub mod tests { use engine_protocol::language_server::MethodPointer; use enso_text::index::*; use parser::Parser; - use span_tree::generate::MockContext; - /// Returns information about all the connections between graph's nodes. - /// - /// Will use `self` as the context for span tree generation. pub fn connections(graph: &Handle) -> FallibleResult { - graph.connections(graph) + graph.connections(&span_tree::generate::context::Empty) } /// All the data needed to set up and run the graph controller in mock environment. @@ -1253,45 +1244,6 @@ main = }) } - #[test] - fn span_tree_context_handling_metadata_and_name() { - let entry = crate::test::mock::data::suggestion_entry_foo(); - let mut test = Fixture::set_up(); - test.data.suggestions.insert(0, entry.clone()); - test.data.code = "main = bar".to_owned(); - test.run(|graph| async move { - let nodes = graph.nodes().unwrap(); - assert_eq!(nodes.len(), 1); - let id = nodes[0].info.id(); - graph - .module - .set_node_metadata(id, NodeMetadata { - intended_method: entry.method_id(), - ..default() - }) - .unwrap(); - - let get_invocation_info = || { - let node = &graph.nodes().unwrap()[0]; - assert_eq!(node.info.id(), id); - let expression = node.info.expression().repr(); - graph.call_info(id, Some(expression.as_str())) - }; - - // Now node is `bar` while the intended method is `foo`. - // No invocation should be reported, as the name is mismatched. - assert!(get_invocation_info().is_none()); - - // Now the name should be good and we should the information about node being a call. - graph.set_expression(id, &entry.name).unwrap(); - crate::test::assert_call_info(get_invocation_info().unwrap(), &entry); - - // Now we remove metadata, so the information is no more. - graph.module.remove_node_metadata(id).unwrap(); - assert!(get_invocation_info().is_none()); - }) - } - #[test] fn graph_controller_used_names_in_inline_def() { let mut test = Fixture::set_up(); @@ -1751,7 +1703,6 @@ main = struct Case { dest_node_expr: &'static str, dest_node_expected: &'static str, - info: Option, } @@ -1763,57 +1714,28 @@ main = let expected = format!("{}{}", MAIN_PREFIX, self.dest_node_expected); let this = self.clone(); test.run(|graph| async move { - let error_message = format!("{this:?}"); - let ctx = match this.info.clone() { - Some(info) => { - let nodes = graph.nodes().expect(&error_message); - let dest_node_id = nodes.last().expect(&error_message).id(); - MockContext::new_single(dest_node_id, info) - } - None => MockContext::default(), - }; - let connections = graph.connections(&ctx).expect(&error_message); - let connection = connections.connections.first().expect(&error_message); - graph.disconnect(connection, &ctx).expect(&error_message); - let new_main = graph.definition().expect(&error_message).ast.repr(); - assert_eq!(new_main, expected, "{error_message}"); + let connections = connections(&graph).unwrap(); + let connection = connections.connections.first().unwrap(); + graph.disconnect(connection, &span_tree::generate::context::Empty).unwrap(); + let new_main = graph.definition().unwrap().ast.repr(); + assert_eq!(new_main, expected, "Case {this:?}"); }) } } - let info = || { - Some(CalledMethodInfo { - parameters: vec![ - span_tree::ArgumentInfo::named("arg1"), - span_tree::ArgumentInfo::named("arg2"), - span_tree::ArgumentInfo::named("arg3"), - ], - ..default() - }) - }; - - #[rustfmt::skip] let cases = &[ - Case { info: None, dest_node_expr: "var + a", dest_node_expected: "_ + a" }, - Case { info: None, dest_node_expr: "a + var", dest_node_expected: "a + _" }, - Case { info: None, dest_node_expr: "var + b + c", dest_node_expected: "b + c" }, - Case { info: None, dest_node_expr: "a + var + c", dest_node_expected: "a + c" }, - Case { info: None, dest_node_expr: "a + b + var", dest_node_expected: "a + b" }, - Case { info: None, dest_node_expr: "foo var", dest_node_expected: "foo _" }, - Case { info: None, dest_node_expr: "foo var a", dest_node_expected: "foo a" }, - Case { info: None, dest_node_expr: "foo a var", dest_node_expected: "foo a" }, - Case { info: info(), dest_node_expr: "foo var", dest_node_expected: "foo" }, - Case { info: info(), dest_node_expr: "foo var a", dest_node_expected: "foo arg2=a" }, - Case { info: info(), dest_node_expr: "foo a var", dest_node_expected: "foo a" }, - Case { info: info(), dest_node_expr: "foo arg2=var a", dest_node_expected: "foo a" }, - Case { info: info(), dest_node_expr: "foo arg1=var a", dest_node_expected: "foo arg2=a" }, + Case { dest_node_expr: "var + a", dest_node_expected: "_ + a" }, + Case { dest_node_expr: "a + var", dest_node_expected: "a + _" }, + Case { dest_node_expr: "var + b + c", dest_node_expected: "b + c" }, + Case { dest_node_expr: "a + var + c", dest_node_expected: "a + c" }, + Case { dest_node_expr: "a + b + var", dest_node_expected: "a + b" }, + Case { dest_node_expr: "foo var", dest_node_expected: "foo _" }, + Case { dest_node_expr: "foo var a", dest_node_expected: "foo a" }, + Case { dest_node_expr: "foo a var", dest_node_expected: "foo a" }, + Case { dest_node_expr: "foo arg2=var a", dest_node_expected: "foo a" }, + Case { dest_node_expr: "foo arg1=var a", dest_node_expected: "foo a" }, + Case { dest_node_expr: "foo arg2=var a c", dest_node_expected: "foo a c" }, Case { - info: info(), - dest_node_expr: "foo arg2=var a c", - dest_node_expected: "foo a arg3=c" - }, - Case { - info: None, dest_node_expr: "f\n bar a var", dest_node_expected: "f\n bar a _", }, diff --git a/app/gui/src/controller/graph/executed.rs b/app/gui/src/controller/graph/executed.rs index 5986499f313..671f4ccc556 100644 --- a/app/gui/src/controller/graph/executed.rs +++ b/app/gui/src/controller/graph/executed.rs @@ -75,7 +75,6 @@ pub enum Notification { /// Handle providing executed graph controller interface. #[derive(Clone, CloneRef, Debug)] pub struct Handle { - #[allow(missing_docs)] /// A handle to basic graph operations. graph: Rc>, /// Execution Context handle, its call stack top contains `graph`'s definition. @@ -343,30 +342,26 @@ impl Handle { /// Span Tree generation context for a graph that does not know about execution. /// Provides information based on computed value registry, using metadata as a fallback. impl Context for Handle { - fn call_info(&self, id: ast::Id, name: Option<&str>) -> Option { - let lookup_registry = || { - let info = self.computed_value_info_registry().get(&id)?; - let method_call = info.method_call.as_ref()?; - let suggestion_db = self.project.suggestion_db(); - let maybe_entry = suggestion_db.lookup_by_method_pointer(method_call).map(|e| { - let invocation_info = e.invocation_info(&suggestion_db, &self.parser()); - invocation_info.with_called_on_type(false) - }); + fn call_info(&self, id: ast::Id, _name: Option<&str>) -> Option { + let info = self.computed_value_info_registry().get(&id)?; + let method_call = info.method_call.as_ref()?; + let suggestion_db = self.project.suggestion_db(); + let maybe_entry = suggestion_db.lookup_by_method_pointer(method_call).map(|e| { + let invocation_info = e.invocation_info(&suggestion_db, &self.parser()); + invocation_info.with_called_on_type(false) + }); - // When the entry was not resolved but the `defined_on_type` has a `.type` suffix, - // try resolving it again with the suffix stripped. This indicates that a method was - // called on type, either because it is a static method, or because it uses qualified - // method syntax. - maybe_entry.or_else(|| { - let defined_on_type = method_call.defined_on_type.strip_suffix(".type")?.to_owned(); - let method_call = MethodPointer { defined_on_type, ..method_call.clone() }; - let entry = suggestion_db.lookup_by_method_pointer(&method_call)?; - let invocation_info = entry.invocation_info(&suggestion_db, &self.parser()); - Some(invocation_info.with_called_on_type(true)) - }) - }; - let fallback = || self.graph.borrow().call_info(id, name); - lookup_registry().or_else(fallback) + // When the entry was not resolved but the `defined_on_type` has a `.type` suffix, + // try resolving it again with the suffix stripped. This indicates that a method was + // called on type, either because it is a static method, or because it uses qualified + // method syntax. + maybe_entry.or_else(|| { + let defined_on_type = method_call.defined_on_type.strip_suffix(".type")?.to_owned(); + let method_call = MethodPointer { defined_on_type, ..method_call.clone() }; + let entry = suggestion_db.lookup_by_method_pointer(&method_call)?; + let invocation_info = entry.invocation_info(&suggestion_db, &self.parser()); + Some(invocation_info.with_called_on_type(true)) + }) } } @@ -387,10 +382,8 @@ pub mod tests { use super::*; use crate::model::execution_context::ExpressionId; - use crate::model::module::NodeMetadata; use crate::test; - use engine_protocol::language_server::types::test::value_update_with_method_ptr; use engine_protocol::language_server::types::test::value_update_with_type; use wasm_bindgen_test::wasm_bindgen_test; use wasm_bindgen_test::wasm_bindgen_test_configure; @@ -477,38 +470,4 @@ pub mod tests { notifications.expect_pending(); } - - #[wasm_bindgen_test] - fn span_tree_context() { - use crate::test::assert_call_info; - use crate::test::mock; - - let mut data = mock::Unified::new(); - let entry1 = data.suggestions.get(&1).unwrap().clone(); - let entry2 = data.suggestions.get(&2).unwrap().clone(); - data.set_inline_code(&entry1.name); - - let mock::Fixture { graph, executed_graph, module, .. } = &data.fixture(); - let id = graph.nodes().unwrap()[0].info.id(); - let get_invocation_info = || executed_graph.call_info(id, Some(&entry1.name)); - assert!(get_invocation_info().is_none()); - - // Check that if we set metadata, executed graph can see this info. - module - .set_node_metadata(id, NodeMetadata { - intended_method: entry1.method_id(), - ..default() - }) - .unwrap(); - let info = get_invocation_info().unwrap(); - assert_call_info(info, &entry1); - - // Now send update that expression actually was computed to be a call to the second - // suggestion entry and check that executed graph provides this info over the metadata one. - let method_pointer = entry2.clone().try_into().unwrap(); - let update = value_update_with_method_ptr(id, method_pointer); - executed_graph.computed_value_info_registry().apply_updates(vec![update]); - let info = get_invocation_info().unwrap(); - assert_call_info(info, &entry2); - } } diff --git a/app/gui/src/controller/searcher.rs b/app/gui/src/controller/searcher.rs index cd0bb8e608f..c18b924da36 100644 --- a/app/gui/src/controller/searcher.rs +++ b/app/gui/src/controller/searcher.rs @@ -14,7 +14,6 @@ use const_format::concatcp; use double_representation::graph::GraphInfo; use double_representation::graph::LocationHint; use double_representation::import; -use double_representation::module::MethodId; use double_representation::name::project; use double_representation::name::QualifiedName; use double_representation::name::QualifiedNameRef; @@ -22,11 +21,11 @@ use double_representation::node::NodeInfo; use engine_protocol::language_server; use enso_suggestion_database::documentation_ir::EntryDocumentation; use enso_suggestion_database::entry::Id as EntryId; +use enso_text as text; use enso_text::Byte; use enso_text::Location; use enso_text::Rope; use flo_stream::Subscriber; -use parser::Parser; // ============== @@ -36,6 +35,7 @@ use parser::Parser; pub mod action; pub mod breadcrumbs; pub mod component; +pub mod input; pub use action::Action; @@ -55,7 +55,6 @@ pub const ASSIGN_NAMES_FOR_NODES: bool = true; const ENSO_PROJECT_SPECIAL_MODULE: &str = concatcp!(project::STANDARD_BASE_LIBRARY_PATH, ".Enso_Project"); -const MINIMUM_PATTERN_OFFSET: usize = 1; // ============== @@ -155,162 +154,17 @@ impl Default for Actions { -// =================== -// === Input Parts === -// =================== +// ====================== +// === RequiredImport === +// ====================== -/// An identification of input fragment filled by picking suggestion. -/// -/// Essentially, this is a crumb for ParsedInput's expression. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[allow(missing_docs)] -pub enum CompletedFragmentId { - /// The called "function" part, defined as a `func` element in Prefix Chain - /// (see `ast::prefix::Chain`). - Function, - /// The `id`th argument of the called function. - Argument { index: usize }, -} - -/// The enum summarizing what user is currently doing basing on the searcher input. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum UserAction { - /// User is about to type/chose function (the input is likely empty). - StartingTypingFunction, - /// User is about to type next argument. - StartingTypingArgument, - /// User is in the middle of typing function. - TypingFunction, - /// User is in the middle of typing argument. - TypingArgument, -} - -/// A Searcher Input which is parsed to the _expression_ and _pattern_ parts. -/// -/// We parse the input for better understanding what user wants to add. -#[derive(Clone, Debug, Default)] -pub struct ParsedInput { - /// The part of input which is treated as completed function and some set of arguments. - /// - /// The expression is kept as prefix chain, as it allows us to easily determine what kind of - /// entity we can put at this moment (is it a function or argument? What type of the - /// argument?). - pub expression: Option>, - /// An offset between expression and pattern. - pub pattern_offset: usize, - /// The part of input being a function/argument which is still typed by user. It is used - /// for filtering actions. - pub pattern: String, -} - -impl ParsedInput { - /// Constructor from the plain input. - #[profile(Debug)] - fn new(input: impl Into, parser: &Parser) -> FallibleResult { - let mut input = input.into(); - let leading_spaces = input.chars().take_while(|c| *c == ' ').count(); - // To properly guess what is "still typed argument" we simulate type of one letter by user. - // This letter will be added to the last argument (or function if there is no argument), or - // will be a new argument (so the user starts filling a new argument). - // - // See also `parsed_input` test to see all cases we want to cover. - input.push('a'); - let ast = parser.parse_line_ast(input.trim_start())?; - let mut prefix = ast::prefix::Chain::from_ast_non_strict(&ast); - if let Some(last_arg) = prefix.args.pop() { - let mut last_arg_repr = last_arg.sast.wrapped.repr(); - last_arg_repr.pop(); - Ok(ParsedInput { - expression: Some(ast::Shifted::new(leading_spaces, prefix)), - pattern_offset: last_arg.sast.off, - pattern: last_arg_repr, - }) - } else { - let mut func_repr = prefix.func.repr(); - func_repr.pop(); - Ok(ParsedInput { - expression: None, - pattern_offset: leading_spaces, - pattern: func_repr, - }) - } - } - - fn new_from_ast(ast: &Ast) -> Self { - let prefix = ast::prefix::Chain::from_ast_non_strict(ast); - ParsedInput { - expression: Some(ast::Shifted::new(default(), prefix)), - pattern_offset: 0, - pattern: default(), - } - } - - /// Returns the id of the next fragment potentially filled by picking completion suggestion. - fn next_completion_id(&self) -> CompletedFragmentId { - match &self.expression { - None => CompletedFragmentId::Function, - Some(expression) => CompletedFragmentId::Argument { index: expression.args.len() }, - } - } - - /// Get the picked fragment from the Searcher's input. - pub fn completed_fragment(&self, fragment: CompletedFragmentId) -> Option { - use CompletedFragmentId::*; - match (fragment, &self.expression) { - (_, None) => None, - (Function, Some(expr)) => Some(expr.func.repr()), - (Argument { index }, Some(expr)) => Some(expr.args.get(index)?.sast.wrapped.repr()), - } - } - - /// Get the user action basing of this input (see `UserAction` docs). - pub fn user_action(&self) -> UserAction { - use UserAction::*; - let empty_pattern = self.pattern.is_empty(); - match self.next_completion_id() { - CompletedFragmentId::Function if empty_pattern => StartingTypingFunction, - CompletedFragmentId::Function => TypingFunction, - CompletedFragmentId::Argument { .. } if empty_pattern => StartingTypingArgument, - CompletedFragmentId::Argument { .. } => TypingArgument, - } - } - - /// Convert the current input to Prefix Chain representation. - pub fn as_prefix_chain(&self, parser: &Parser) -> Option> { - let parsed_pattern = parser.parse_line_ast(&self.pattern).ok(); - let pattern_sast = parsed_pattern.map(|p| ast::Shifted::new(self.pattern_offset, p)); - // If there is an expression part of input, we add current pattern as the last argument. - if let Some(chain) = &self.expression { - let mut chain = chain.clone(); - if let Some(sast) = pattern_sast { - let prefix_id = None; - let argument = ast::prefix::Argument { sast, prefix_id }; - chain.wrapped.args.push(argument); - } - Some(chain) - // If there isn't any expression part, the pattern is the whole input. - } else if let Some(sast) = pattern_sast { - let chain = ast::prefix::Chain::from_ast_non_strict(&sast.wrapped); - Some(ast::Shifted::new(self.pattern_offset, chain)) - } else { - None - } - } -} - -impl HasRepr for ParsedInput { - fn repr(&self) -> String { - let mut repr = self.expression.as_ref().map_or("".to_string(), HasRepr::repr); - repr.extend(itertools::repeat_n(' ', self.pattern_offset)); - repr.push_str(&self.pattern); - repr - } -} - -impl Display for ParsedInput { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.repr()) - } +/// An import that is needed for the picked suggestion. +#[derive(Debug, Clone)] +pub enum RequiredImport { + /// A specific entry needs to be imported. + Entry(Rc), + /// An entry with a specific name needs to be imported. + Name(QualifiedName), } @@ -391,7 +245,7 @@ impl Mode { pub fn node_id(&self) -> ast::Id { match self { Mode::NewNode { node_id, .. } => *node_id, - Mode::EditNode { node_id } => *node_id, + Mode::EditNode { node_id, .. } => *node_id, } } } @@ -402,29 +256,20 @@ impl Mode { /// what imports should be added when inserting node. #[derive(Clone, Debug)] #[allow(missing_docs)] -pub struct FragmentAddedByPickingSuggestion { - pub id: CompletedFragmentId, - pub picked_suggestion: action::Suggestion, +pub struct PickedSuggestion { + pub entry: action::Suggestion, + pub inserted_code: String, + pub import: Option, } -impl FragmentAddedByPickingSuggestion { +impl PickedSuggestion { /// Check if the picked fragment is still unmodified by user. - fn is_still_unmodified(&self, input: &ParsedInput) -> bool { - let expected = self.code_to_insert(&None); - input.completed_fragment(self.id).contains(&expected) - } - - fn code_to_insert(&self, this_node: &Option) -> Cow { - let generate_this = self.id != CompletedFragmentId::Function || this_node.is_none(); - self.picked_suggestion.code_to_insert(generate_this) - } - - fn required_imports( - &self, - db: &model::SuggestionDatabase, - current_module: QualifiedNameRef, - ) -> impl IntoIterator { - self.picked_suggestion.required_imports(db, current_module) + fn is_still_unmodified(&self, input: &input::Input) -> bool { + match &input.ast { + input::InputAst::Line(ast) => + ast.elem.iter_recursive().any(|ast_node| ast_node.repr() == self.inserted_code), + input::InputAst::Invalid(string) => *string == self.inserted_code, + } } } @@ -432,14 +277,14 @@ impl FragmentAddedByPickingSuggestion { #[derive(Clone, Debug, Default)] pub struct Data { /// The current searcher's input. - pub input: ParsedInput, + pub input: input::Input, /// The action list which should be displayed. - pub actions: Actions, + pub actions: Actions, /// The component list which should be displayed. - pub components: component::List, - /// All fragments of input which were added by picking suggestions. If the fragment will be - /// changed by user, it will be removed from this list. - pub fragments_added_by_picking: Vec, + pub components: component::List, + /// All picked suggestions. If the user changes the generated code, it will be removed from + /// this list. + pub picked_suggestions: Vec, } impl Data { @@ -452,35 +297,16 @@ impl Data { #[profile(Debug)] fn new_with_edited_node( graph: &controller::Graph, - database: &model::SuggestionDatabase, edited_node_id: ast::Id, + cursor_position: Byte, ) -> FallibleResult { let edited_node = graph.node(edited_node_id)?; - let input = ParsedInput::new_from_ast(&edited_node.info.expression()); + let input_ast = ast::BlockLine { elem: edited_node.info.expression(), off: 0 }; + let input = input::Input::new(input_ast, cursor_position); let actions = default(); let components = default(); - let intended_method = edited_node.metadata.and_then(|md| md.intended_method); - let initial_entry = intended_method.and_then(|metadata| database.lookup_method(metadata)); - let initial_fragment = initial_entry.and_then(|entry| { - let fragment = FragmentAddedByPickingSuggestion { - id: CompletedFragmentId::Function, - picked_suggestion: action::Suggestion::FromDatabase(entry), - }; - // This is meant to work with single function calls (without "this" argument). - // In other case we should know what the function is from the engine, as the function - // should be resolved. - fragment.is_still_unmodified(&input).then_some(fragment) - }); - let mut fragments_added_by_picking = Vec::::new(); - initial_fragment.for_each(|f| fragments_added_by_picking.push(f)); - Ok(Data { input, actions, components, fragments_added_by_picking }) - } - - fn find_picked_fragment( - &self, - id: CompletedFragmentId, - ) -> Option<&FragmentAddedByPickingSuggestion> { - self.fragments_added_by_picking.iter().find(|fragment| fragment.id == id) + let picked_suggestions = default(); + Ok(Data { input, actions, components, picked_suggestions }) } } @@ -567,6 +393,15 @@ impl ComponentsProvider { } } +/// An information used for filtering entries. +#[derive(Debug, Clone, CloneRef)] +pub struct Filter { + /// The part of the input used for filtering. + pub pattern: ImString, + /// Additional context. A string representation of the edited accessor chain. + pub context: Option, +} + /// Searcher Controller. /// /// This is an object providing all required functionalities for Searcher View: mainly it is the @@ -597,22 +432,6 @@ pub struct Searcher { } impl Searcher { - /// Create new Searcher Controller. - pub async fn new( - ide: controller::Ide, - project: &model::Project, - method: language_server::MethodPointer, - mode: Mode, - ) -> FallibleResult { - let graph = controller::ExecutedGraph::new(project.clone_ref(), method).await?; - Self::new_from_graph_controller(ide, project, graph, mode) - } - - /// Abort editing and perform cleanup. - pub fn abort_editing(&self) { - self.clear_temporary_imports(); - } - /// Create new Searcher Controller, when you have Executed Graph Controller handy. #[profile(Task)] pub fn new_from_graph_controller( @@ -620,20 +439,16 @@ impl Searcher { project: &model::Project, graph: controller::ExecutedGraph, mode: Mode, + cursor_position: Byte, + position_in_code: Location, ) -> FallibleResult { let project = project.clone_ref(); - let database = project.suggestion_db(); let data = if let Mode::EditNode { node_id } = mode { - Data::new_with_edited_node(&graph.graph(), &database, node_id)? + Data::new_with_edited_node(&graph.graph(), node_id, cursor_position)? } else { default() }; let node_metadata_guard = Rc::new(Some(EditGuard::new(&mode, graph.clone_ref()))); - let module_ast = graph.graph().module.ast(); - let def_id = graph.graph().id; - let def_span = double_representation::module::definition_span(&module_ast, &def_id)?; - let module_repr: Rope = module_ast.repr().into(); - let position = module_repr.offset_to_location_snapped(def_span.end); let this_arg = Rc::new(match mode { Mode::NewNode { source_node: Some(node), .. } => ThisNode::new(node, &graph.graph()), _ => None, @@ -649,7 +464,7 @@ impl Searcher { mode: Immutable(mode), database: project.suggestion_db(), language_server: project.json_rpc(), - position_in_code: Immutable(position), + position_in_code: Immutable(position_in_code), project, node_edit_guard: node_metadata_guard, }; @@ -661,9 +476,14 @@ impl Searcher { self } + /// Abort editing and perform cleanup. + pub fn abort_editing(&self) { + self.clear_temporary_imports(); + } + /// Return true if user is currently filtering entries (the input has non-empty _pattern_ part). pub fn is_filtering(&self) -> bool { - !self.data.borrow().input.pattern.is_empty() + !self.data.borrow().input.filter().pattern.is_empty() } /// Subscribe to controller's notifications. @@ -730,24 +550,28 @@ impl Searcher { /// This function should be called each time user modifies Searcher input in view. It may result /// in a new action list (the appropriate notification will be emitted). #[profile(Debug)] - pub fn set_input(&self, new_input: String) -> FallibleResult { - debug!("Manually setting input to {new_input}."); - let parsed_input = ParsedInput::new(new_input, self.ide.parser())?; - let old_expr = self.data.borrow().input.expression.repr(); - let new_expr = parsed_input.expression.repr(); + pub fn set_input(&self, new_input: String, cursor_position: Byte) -> FallibleResult { + debug!("Manually setting input to {new_input} with cursor position {cursor_position}"); + let parsed_input = input::Input::parse(self.ide.parser(), new_input, cursor_position); + let new_context = parsed_input.context().map(|ctx| ctx.into_ast().repr()); + let new_literal = parsed_input.edited_literal().cloned(); + let old_input = mem::replace(&mut self.data.borrow_mut().input, parsed_input); + let old_context = old_input.context().map(|ctx| ctx.into_ast().repr()); + let old_literal = old_input.edited_literal(); - self.data.borrow_mut().input = parsed_input; - self.invalidate_fragments_added_by_picking(); - let expression_changed = old_expr != new_expr; - if expression_changed { + self.invalidate_picked_suggestions(); + let context_changed = old_context != new_context; + let literal_changed = old_literal != new_literal.as_ref(); + if context_changed || literal_changed { debug!("Reloading list."); self.reload_list(); } else { let data = self.data.borrow(); - data.components.update_filtering(&data.input.pattern); + let filter = data.input.filter(); + data.components.update_filtering(filter.clone_ref()); if let Actions::Loaded { list } = &data.actions { debug!("Update filtering."); - list.update_filtering(&data.input.pattern); + list.update_filtering(filter.pattern); executor::global::spawn(self.notifier.publish(Notification::NewActionList)); } } @@ -758,68 +582,40 @@ impl Searcher { self.this_arg.deref().as_ref().map(|this| this.var.as_ref()) } - /// Code that will be inserted by expanding given suggestion at given location. - /// - /// Code depends on the location, as the first fragment can introduce `self` variable access, - /// and then we don't want to put any module name. - fn code_to_insert<'a>(&self, fragment: &'a FragmentAddedByPickingSuggestion) -> Cow<'a, str> { - fragment.code_to_insert(&self.this_arg) - } - /// Pick a completion suggestion. /// /// This function should be called when user do the _use as suggestion_ action as a code /// suggestion (see struct documentation). The picked suggestion will be remembered, and the /// searcher's input will be updated and returned by this function. #[profile(Debug)] - pub fn use_suggestion(&self, picked_suggestion: action::Suggestion) -> FallibleResult { - info!("Picking suggestion: {picked_suggestion:?}."); - let id = self.data.borrow().input.next_completion_id(); - let picked_completion = FragmentAddedByPickingSuggestion { id, picked_suggestion }; - let code_to_insert = self.code_to_insert(&picked_completion); - debug!("Code to insert: \"{code_to_insert}\""); - let added_ast = self.ide.parser().parse_line_ast(code_to_insert)?; - let pattern_offset = self.data.borrow().input.pattern_offset; - let new_expression_chain = self.create_new_expression_chain(added_ast, pattern_offset); - let new_parsed_input = ParsedInput { - expression: Some(new_expression_chain), - pattern_offset: 1, - pattern: "".to_string(), + pub fn use_suggestion( + &self, + picked_suggestion: action::Suggestion, + ) -> FallibleResult> { + debug!("Picking suggestion: {picked_suggestion:?}."); + let change = { + let mut data = self.data.borrow_mut(); + let has_this = self.this_var().is_some(); + let inserted = data.input.after_inserting_suggestion(&picked_suggestion, has_this)?; + let new_cursor_position = inserted.inserted_text.end; + let inserted_code = inserted.new_input[inserted.inserted_code].to_owned(); + let import = inserted.import.clone(); + let parser = self.ide.parser(); + data.input = input::Input::parse(parser, &inserted.new_input, new_cursor_position); + let suggestion = PickedSuggestion { entry: picked_suggestion, inserted_code, import }; + data.picked_suggestions.push(suggestion); + inserted.input_change() }; - let new_input = new_parsed_input.repr(); - self.data.borrow_mut().input = new_parsed_input; - self.data.borrow_mut().fragments_added_by_picking.push(picked_completion); self.reload_list(); self.breadcrumbs.set_content(iter::empty()); - Ok(new_input) - } - - fn create_new_expression_chain( - &self, - added_ast: Ast, - pattern_offset: usize, - ) -> ast::Shifted { - match self.data.borrow().input.expression.clone() { - None => { - let ast = ast::prefix::Chain::from_ast_non_strict(&added_ast); - ast::Shifted::new(pattern_offset, ast) - } - Some(mut expression) => { - let new_argument = ast::prefix::Argument { - sast: ast::Shifted::new( - pattern_offset.max(MINIMUM_PATTERN_OFFSET), - added_ast, - ), - prefix_id: default(), - }; - expression.args.push(new_argument); - expression - } - } + Ok(change) } /// Use action at given index as a suggestion. The exact outcome depends on the action's type. - pub fn use_as_suggestion(&self, index: usize) -> FallibleResult { + pub fn use_as_suggestion( + &self, + index: usize, + ) -> FallibleResult> { let error = || NoSuchAction { index }; let suggestion = { let data = self.data.borrow(); @@ -856,28 +652,19 @@ impl Searcher { debug!("Previewing suggestion: \"{picked_suggestion:?}\"."); self.clear_temporary_imports(); - let id = self.data.borrow().input.next_completion_id(); - let picked_completion = FragmentAddedByPickingSuggestion { id, picked_suggestion }; - let code_to_insert = self.code_to_insert(&picked_completion); - debug!("Code to insert: \"{code_to_insert}\".",); - let added_ast = self.ide.parser().parse_line_ast(code_to_insert)?; - let pattern_offset = self.data.borrow().input.pattern_offset; + let has_this = self.this_var().is_none(); + let preview_change = + self.data.borrow().input.after_inserting_suggestion(&picked_suggestion, has_this)?; + let preview_ast = self.ide.parser().parse_line_ast(preview_change.new_input).ok(); + let expression = self.get_expression(preview_ast.as_ref()); + { // This block serves to limit the borrow of `self.data`. - let current_fragments = &self.data.borrow().fragments_added_by_picking; - let fragments_added_by_picking = - current_fragments.iter().chain(iter::once(&picked_completion)); - self.add_required_imports(fragments_added_by_picking, false)?; + let data = self.data.borrow(); + let requirements = data.picked_suggestions.iter().filter_map(|ps| ps.import.clone()); + let all_requirements = requirements.chain(preview_change.import.iter().cloned()); + self.add_required_imports(all_requirements, false)?; } - let new_expression_chain = self.create_new_expression_chain(added_ast, pattern_offset); - let expression = self.get_expression(Some(new_expression_chain)); - let intended_method = self.intended_method(); - - self.graph.graph().module.with_node_metadata( - self.mode.node_id(), - Box::new(|md| md.intended_method = intended_method), - )?; - debug!("Previewing expression: \"{:?}\".", expression); self.graph.graph().set_expression(self.mode.node_id(), expression)?; Ok(()) @@ -943,7 +730,6 @@ impl Searcher { /// Preview the action in the searcher. #[profile(Task)] pub fn preview_action_by_index(&self, index: usize) -> FallibleResult<()> { - //TODO[MM] the actual functionality here will be implemented as part of task #182634050. let error = || NoSuchAction { index }; let action = { let data = self.data.borrow(); @@ -954,19 +740,6 @@ impl Searcher { Ok(()) } - /// Check if the first fragment in the input (i.e. the one representing the called function) - /// is still unmodified. - /// - /// False if it was modified after picking or if it wasn't picked at all. - pub fn is_function_fragment_unmodified(&self) -> bool { - with(self.data.borrow(), |data| { - data.fragments_added_by_picking.first().contains_if(|frag| { - let is_function = frag.id == CompletedFragmentId::Function; - is_function && frag.is_still_unmodified(&data.input) - }) - }) - } - /// Commit the current input as a new node expression. /// /// If the searcher was brought by editing existing node, the input is set as a new node @@ -976,25 +749,22 @@ impl Searcher { pub fn commit_node(&self) -> FallibleResult { let _transaction_guard = self.graph.get_or_open_transaction("Commit node"); self.clear_temporary_imports(); + // We add the required imports before we edit its content. This way, we avoid an // intermediate state where imports would already be in use but not yet available. - let data_borrowed = self.data.borrow(); - let fragments = data_borrowed.fragments_added_by_picking.iter(); - self.add_required_imports(fragments, true)?; + { + let data = self.data.borrow(); + let requirements = data.picked_suggestions.iter().filter_map(|ps| ps.import.clone()); + self.add_required_imports(requirements, true)?; + } let node_id = self.mode.node_id(); - let input_chain = self.data.borrow().input.as_prefix_chain(self.ide.parser()); - let expression = self.get_expression(input_chain); - let intended_method = self.intended_method(); - self.graph.graph().set_expression(node_id, expression)?; - if let Mode::NewNode { .. } = self.mode.as_ref() { - self.graph.graph().introduce_name_on(node_id)?; - } - self.graph - .graph() - .module - .with_node_metadata(node_id, Box::new(|md| md.intended_method = intended_method))?; + let expression = self.get_expression(self.data.borrow().input.ast()); let graph = self.graph.graph(); + graph.set_expression(node_id, expression)?; + if let Mode::NewNode { .. } = *self.mode { + graph.introduce_name_on(node_id)?; + } if let Some(this) = self.this_arg.deref().as_ref() { this.introduce_pattern(graph.clone_ref())?; } @@ -1006,11 +776,10 @@ impl Searcher { Ok(node_id) } - fn get_expression(&self, input_chain: Option>) -> String { - let expression = match (self.this_var(), input_chain) { - (Some(this_var), Some(input)) => - apply_this_argument(this_var, &input.wrapped.into_ast()).repr(), - (None, Some(input)) => input.wrapped.into_ast().repr(), + fn get_expression(&self, input: Option<&Ast>) -> String { + let expression = match (self.this_var(), input) { + (Some(this_var), Some(input)) => apply_this_argument(this_var, input).repr(), + (None, Some(input)) => input.repr(), (_, None) => "".to_owned(), }; expression @@ -1066,22 +835,31 @@ impl Searcher { } #[profile(Debug)] - fn invalidate_fragments_added_by_picking(&self) { + fn invalidate_picked_suggestions(&self) { let mut data = self.data.borrow_mut(); - let data = data.deref_mut(); + let data = &mut *data; let input = &data.input; - data.fragments_added_by_picking.drain_filter(|frag| !frag.is_still_unmodified(input)); + data.picked_suggestions.drain_filter(|frag| !frag.is_still_unmodified(input)); } #[profile(Debug)] fn add_required_imports<'a>( &self, - fragments: impl Iterator, + import_requirements: impl Iterator, permanent: bool, ) -> FallibleResult { - let imports = fragments.flat_map(|frag| { - frag.required_imports(&self.database, self.module_qualified_name().as_ref()) - }); + let imports = import_requirements + .filter_map(|requirement| match requirement { + RequiredImport::Entry(entry) => Some( + entry.required_imports(&self.database, self.module_qualified_name().as_ref()), + ), + RequiredImport::Name(name) => { + let (_id, entry) = self.database.lookup_by_qualified_name(&name)?; + let defined_in = self.module_qualified_name(); + Some(entry.required_imports(&self.database, defined_in.as_ref())) + } + }) + .flatten(); let mut module = self.module(); // TODO[ao] this is a temporary workaround. See [`Searcher::add_enso_project_entries`] // documentation. @@ -1146,14 +924,17 @@ impl Searcher { /// list - once it be retrieved, the new list will be set and notification will be emitted. #[profile(Debug)] pub fn reload_list(&self) { - let this_type = self.this_arg_type_for_next_completion(); - let return_types = match self.data.borrow().input.next_completion_id() { - CompletedFragmentId::Function => vec![], - CompletedFragmentId::Argument { index } => - self.return_types_for_argument_completion(index), - }; - self.gather_actions_from_engine(this_type, return_types, None); - self.data.borrow_mut().actions = Actions::Loading; + let edited_literal = self.data.borrow().input.edited_literal().cloned(); + if let Some(literal) = edited_literal { + let components = component_list_for_literal(&literal, &self.database); + let mut data = self.data.borrow_mut(); + data.components = components; + data.actions = Actions::Loaded { list: default() }; + } else { + let this_type = self.this_arg_type_for_next_completion(); + self.gather_actions_from_engine(this_type, None); + self.data.borrow_mut().actions = Actions::Loading; + } executor::global::spawn(self.notifier.publish(Notification::NewActionList)); } @@ -1161,87 +942,58 @@ impl Searcher { /// type information might not have came yet from the Language Server. #[profile(Debug)] fn this_arg_type_for_next_completion(&self) -> impl Future> { - let next_id = self.data.borrow().input.next_completion_id(); + let replaced_range = { + let input = &self.data.borrow().input; + let default_range = (input.cursor_position..input.cursor_position).into(); + input.edited_name_range().unwrap_or(default_range) + }; + let is_first_function = replaced_range.start == Byte(0); let graph = self.graph.clone_ref(); let this = self.this_arg.clone_ref(); async move { - let is_function_fragment = next_id == CompletedFragmentId::Function; - if !is_function_fragment { - return None; + if is_first_function { + let ThisNode { id, .. } = this.deref().as_ref()?; + let opt_type = graph.expression_type(*id).await.map(Into::into); + opt_type.map_none(move || error!("Failed to obtain type for this node.")) + } else { + None } - let ThisNode { id, .. } = this.deref().as_ref()?; - let opt_type = graph.expression_type(*id).await.map(Into::into); - opt_type.map_none(move || error!("Failed to obtain type for this node.")) } } - /// Get the type that suggestions for the next completion should return. - /// - /// Generally this corresponds to the type of the currently filled function argument. Returns - /// empty list if no type could be determined. - fn return_types_for_argument_completion(&self, arg_index: usize) -> Vec { - let suggestions = if let Some(intended) = self.intended_function_suggestion() { - std::iter::once(intended).collect() - } else { - self.possible_function_calls() - }; - suggestions - .into_iter() - .filter_map(|suggestion| { - let arg_index = arg_index + if self.this_arg.is_some() { 1 } else { 0 }; - let arg_index = arg_index + if self.has_this_argument() { 1 } else { 0 }; - let parameter = suggestion.argument_types().into_iter().nth(arg_index)?; - Some(parameter) - }) - .collect() - } - fn gather_actions_from_engine( &self, this_type: impl Future> + 'static, - return_types: impl IntoIterator, tags: Option>, ) { let ls = self.language_server.clone_ref(); let graph = self.graph.graph(); let position = self.my_utf16_location().span.into(); let this = self.clone_ref(); - let return_types = return_types.into_iter().collect_vec(); - let return_types_for_engine = if return_types.is_empty() { - vec![None] - } else { - return_types.iter().cloned().map(Some).collect() - }; executor::global::spawn(async move { let this_type = this_type.await; info!("Requesting new suggestion list. Type of `self` is {this_type:?}."); - let requests = return_types_for_engine.into_iter().map(|return_type| { - info!("Requesting suggestions for returnType {return_type:?}."); - let file = graph.module.path().file_path(); - ls.completion(file, &position, &this_type, &return_type, &tags) - }); - let responses: Result, _> = - futures::future::join_all(requests).await.into_iter().collect(); - match responses { - Ok(responses) => { + let file = graph.module.path().file_path(); + let response = ls.completion(file, &position, &this_type, &None, &tags).await; + match response { + Ok(response) => { info!("Received suggestions from Language Server."); - let list = this.make_action_list(responses.iter()); + let list = this.make_action_list(&response); let mut data = this.data.borrow_mut(); - list.update_filtering(&data.input.pattern); + let filter = data.input.filter(); + list.update_filtering(filter.pattern.clone_ref()); data.actions = Actions::Loaded { list: Rc::new(list) }; - let completions = responses.iter().flat_map(|r| r.results.iter().cloned()); - data.components = - this.make_component_list(completions, &this_type, &return_types); - data.components.update_filtering(&data.input.pattern); + let completions = response.results; + data.components = this.make_component_list(completions, &this_type); + data.components.update_filtering(filter); } Err(err) => { let msg = "Request for completions to the Language Server returned error"; error!("{msg}: {err}"); let mut data = this.data.borrow_mut(); data.actions = Actions::Error(Rc::new(err.into())); - data.components = - this.make_component_list(this.database.keys(), &this_type, &return_types); - data.components.update_filtering(&data.input.pattern); + data.components = this.make_component_list(this.database.keys(), &this_type); + data.components.update_filtering(data.input.filter()); } } this.notifier.publish(Notification::NewActionList).await; @@ -1250,9 +1002,9 @@ impl Searcher { /// Process multiple completion responses from the engine into a single list of suggestion. #[profile(Debug)] - fn make_action_list<'a>( + fn make_action_list( &self, - completion_responses: impl IntoIterator, + completion_response: &language_server::response::Completion, ) -> action::List { let creating_new_node = matches!(self.mode.deref(), Mode::NewNode { .. }); let should_add_additional_entries = creating_new_node && self.this_arg.is_none(); @@ -1277,20 +1029,18 @@ impl Searcher { if should_add_additional_entries { Self::add_enso_project_entries(&libraries_cat); } - for response in completion_responses { - let entries = response.results.iter().filter_map(|id| { - self.database - .lookup(*id) - .map(|entry| Action::Suggestion(action::Suggestion::FromDatabase(entry))) - .handle_err(|e| { - error!( - "Response provided a suggestion ID that cannot be \ - resolved: {e}." - ) - }) - }); - libraries_cat.extend(entries); - } + let entries = completion_response.results.iter().filter_map(|id| { + self.database + .lookup(*id) + .map(|entry| Action::Suggestion(action::Suggestion::FromDatabase(entry))) + .handle_err(|e| { + error!( + "Response provided a suggestion ID that cannot be \ + resolved: {e}." + ) + }) + }); + libraries_cat.extend(entries); actions.build() } @@ -1300,7 +1050,6 @@ impl Searcher { &self, entry_ids: impl IntoIterator, this_type: &Option, - return_types: &[String], ) -> component::List { let favorites = self.graph.component_groups(); let module_name = self.module_qualified_name(); @@ -1310,7 +1059,7 @@ impl Searcher { &*favorites, self.ide.are_component_browser_private_entries_visible(), ); - add_virtual_entries_to_builder(&mut builder, this_type, return_types); + add_virtual_entries_to_builder(&mut builder, this_type); builder.extend_list_and_allow_favorites_with_ids(&self.database, entry_ids); builder.build() } @@ -1334,77 +1083,6 @@ impl Searcher { self.location_to_utf16(location) } - fn possible_function_calls(&self) -> Vec { - let opt_result = || { - let call_ast = self.data.borrow().input.expression.as_ref()?.func.clone_ref(); - let call = SimpleFunctionCall::try_new(&call_ast)?; - if let Some(module) = self.module_whose_method_is_called(&call) { - let name = call.function_name; - let entry = self.database.lookup_module_method(name, &module); - Some(entry.into_iter().map(action::Suggestion::FromDatabase).collect()) - } else { - let name = &call.function_name; - let location = self.my_utf16_location(); - let entries = self.database.lookup_at(name, &location); - Some(entries.into_iter().map(action::Suggestion::FromDatabase).collect()) - } - }; - opt_result().unwrap_or_default() - } - - /// For the simple function call checks if the function is called on the module (if it can be - /// easily determined) and returns the module's qualified name if it is. - fn module_whose_method_is_called(&self, call: &SimpleFunctionCall) -> Option { - let location = self.my_utf16_location(); - let this_name = ast::identifier::name(call.this_argument.as_ref()?)?; - let matching_locals = self.database.lookup_locals_at(this_name, &location); - let module_name = location.module; - let not_local_name = matching_locals.is_empty(); - not_local_name.and_option_from(|| { - if this_name == module_name.name().deref() { - Some(module_name) - } else { - self.module().iter_imports().find_map(|import| { - import - .qualified_module_name() - .ok() - .filter(|module| module.name().deref() == this_name) - }) - } - }) - } - - /// Get the suggestion that was selected by the user into the function. - /// - /// This suggestion shall be used to request better suggestions from the engine. - pub fn intended_function_suggestion(&self) -> Option { - let id = CompletedFragmentId::Function; - let fragment = self.data.borrow().find_picked_fragment(id).cloned(); - fragment.map(|f| f.picked_suggestion.clone_ref()) - } - - /// Returns the Id of method user intends to be called in this node. - /// - /// The method may be picked by user from suggestion, but there are many methods with the same - /// name. - fn intended_method(&self) -> Option { - self.intended_function_suggestion()?.method_id() - } - - /// If the function fragment has been filled and also contains the initial "this" argument. - /// - /// While we maintain chain consisting of target and applied arguments, the target itself might - /// need to count for a one argument, as it may of form `this.method`. - fn has_this_argument(&self) -> bool { - self.data - .borrow() - .input - .expression - .as_ref() - .and_then(|expr| ast::opr::as_access_chain(&expr.func)) - .is_some() - } - fn module(&self) -> double_representation::module::Info { double_representation::module::Info { ast: self.graph.graph().module.ast() } } @@ -1413,11 +1091,6 @@ impl Searcher { self.graph.module_qualified_name(&*self.project) } - /// Get the user action basing of current input (see `UserAction` docs). - pub fn current_user_action(&self) -> UserAction { - self.data.borrow().input.user_action() - } - /// Add to the action list the special mocked entry of `Enso_Project.data`. /// /// This is a workaround for Engine bug https://github.com/enso-org/enso/issues/1605. @@ -1471,16 +1144,9 @@ fn component_list_builder_with_favorites<'a>( fn add_virtual_entries_to_builder( builder: &mut component::builder::List, this_type: &Option, - return_types: &[String], ) { if this_type.is_none() { - let snippets = if return_types.is_empty() { - component::hardcoded::INPUT_SNIPPETS.with(|s| s.clone()) - } else { - let parse_type_qn = |s| QualifiedName::from_text(s).ok(); - let rt_qns = return_types.iter().filter_map(parse_type_qn); - component::hardcoded::input_snippets_with_matching_return_type(rt_qns) - }; + let snippets = component::hardcoded::INPUT_SNIPPETS.with(|s| s.clone()); let group_name = component::hardcoded::INPUT_GROUP_NAME; let project = project::QualifiedName::standard_base_library(); builder.insert_virtual_components_in_favorites_group(group_name, project, snippets); @@ -1537,11 +1203,7 @@ impl EditGuard { module.with_node_metadata( self.node_id, Box::new(|metadata| { - let previous_intended_method = metadata.intended_method.clone(); - metadata.edit_status = Some(NodeEditStatus::Edited { - previous_expression, - previous_intended_method, - }); + metadata.edit_status = Some(NodeEditStatus::Edited { previous_expression }); }), ) } @@ -1581,20 +1243,13 @@ impl EditGuard { debug!("Deleting temporary node {} after aborting edit.", self.node_id); self.graph.graph().remove_node(self.node_id)?; } - Some(NodeEditStatus::Edited { previous_expression, previous_intended_method }) => { + Some(NodeEditStatus::Edited { previous_expression }) => { debug!( "Reverting expression of node {} to {} after aborting edit.", self.node_id, &previous_expression ); let graph = self.graph.graph(); graph.set_expression(self.node_id, previous_expression)?; - let module = &self.graph.graph().module; - module.with_node_metadata( - self.node_id, - Box::new(|metadata| { - metadata.intended_method = previous_intended_method; - }), - )?; } }; Ok(()) @@ -1621,30 +1276,7 @@ impl Drop for EditGuard { } - -// === SimpleFunctionCall === - -/// A simple function call is an AST where function is a single identifier with optional -/// argument applied by `ACCESS` operator (dot). -struct SimpleFunctionCall { - this_argument: Option, - function_name: String, -} - -impl SimpleFunctionCall { - fn try_new(call: &Ast) -> Option { - if let Some(name) = ast::identifier::name(call) { - Some(Self { this_argument: None, function_name: name.to_owned() }) - } else { - let infix = ast::opr::to_access(call)?; - let name = ast::identifier::name(&infix.rarg)?; - Some(Self { - this_argument: Some(infix.larg.clone_ref()), - function_name: name.to_owned(), - }) - } - } -} +// === Helpers === fn apply_this_argument(this_var: &str, ast: &Ast) -> Ast { if let Ok(opr) = ast::known::Opr::try_from(ast) { @@ -1672,6 +1304,20 @@ fn apply_this_argument(this_var: &str, ast: &Ast) -> Ast { } } +/// Build a component list with a single component, representing the given literal. When used as a +/// suggestion, a number literal will be inserted without changes, but a string literal will be +/// surrounded by quotation marks. +fn component_list_for_literal( + literal: &input::Literal, + db: &enso_suggestion_database::SuggestionDatabase, +) -> component::List { + let mut builder = component::builder::List::default(); + let project = project::QualifiedName::standard_base_library(); + let snippet = component::hardcoded::Snippet::from_literal(literal, db).into(); + builder.insert_virtual_components_in_favorites_group("Literals", project, vec![snippet]); + builder.build() +} + // ============= @@ -1694,6 +1340,7 @@ pub mod test { use enso_suggestion_database::mock_suggestion_database; use enso_suggestion_database::SuggestionDatabase; use json_rpc::expect_call; + use parser::Parser; use std::assert_matches::assert_matches; @@ -1739,7 +1386,6 @@ pub mod test { &self, client: &mut language_server::MockClient, self_type: Option<&str>, - return_type: Option<&str>, result: &[SuggestionId], ) { let completion_response = completion_response(result); @@ -1747,7 +1393,7 @@ pub mod test { module = self.graph.module.path.file_path().clone(), position = self.code_location, self_type = self_type.map(Into::into), - return_type = return_type.map(Into::into), + return_type = None, tag = None ) => Ok(completion_response)); } @@ -1756,18 +1402,16 @@ pub mod test { struct Fixture { #[allow(dead_code)] data: MockData, + database: Rc, test: TestWithLocalPoolExecutor, searcher: Searcher, - entry1: Rc, - entry2: Rc, - entry3: Rc, - entry4: Rc, - entry9: Rc, } impl Fixture { - fn new_custom(client_setup: F) -> Self - where F: FnOnce(&mut MockData, &mut language_server::MockClient) { + fn new_custom(database_setup: D, client_setup: C) -> Self + where + D: FnOnce(RangeInclusive>) -> Rc, + C: FnOnce(&mut MockData, &mut language_server::MockClient), { let test = TestWithLocalPoolExecutor::set_up(); let mut data = MockData::default(); let mut client = language_server::MockClient::default(); @@ -1782,8 +1426,7 @@ pub mod test { let searcher_target = graph.graph().nodes().unwrap().last().unwrap().id(); let this = ThisNode::new(node.info.id(), &graph.graph()); let this = data.selected_node.and_option(this); - let module_name = crate::test::mock::data::module_qualified_name(); - let database = suggestion_database_with_mock_entries(code_range); + let database = database_setup(code_range); let mut ide = controller::ide::MockAPI::new(); let mut project = model::project::MockAPI::new(); let project_qname = project_qualified_name(); @@ -1801,7 +1444,7 @@ pub mod test { let breadcrumbs = Breadcrumbs::new(); let searcher = Searcher { graph, - database, + database: database.clone_ref(), ide: Rc::new(ide), data: default(), breadcrumbs, @@ -1813,32 +1456,40 @@ pub mod test { project: project.clone_ref(), node_edit_guard: node_metadata_guard, }; - let (_, entry1) = searcher - .database - .lookup_by_qualified_name(&module_name.clone().new_child("testFunction1")) - .unwrap(); - let (_, entry2) = searcher - .database - .lookup_by_qualified_name(&module_name.clone().new_child("test_var_1")) - .unwrap(); - let (_, entry3) = searcher - .database - .lookup_by_qualified_name(&module_name.clone().new_child("test_method")) - .unwrap(); - let entry4 = searcher - .database - .lookup_by_qualified_name_str("test.Test.Test.test_method") - .unwrap(); - let (_, entry9) = searcher - .database - .lookup_by_qualified_name(&module_name.new_child("testFunction2")) - .unwrap(); - Fixture { data, test, searcher, entry1, entry2, entry3, entry4, entry9 } + Fixture { data, test, searcher, database } } fn new() -> Self { - Self::new_custom(|_, _| {}) + Self::new_custom(|_| default(), |_, _| {}) } + + fn lookup(&self, name: &QualifiedName) -> Rc { + self.database.lookup_by_qualified_name(name).expect("Database lookup failed.").1 + } + + fn lookup_str(&self, name: &str) -> Rc { + self.database.lookup_by_qualified_name_str(name).expect("Database lookup failed.") + } + } + + fn test_function_1_name() -> QualifiedName { + crate::test::mock::data::module_qualified_name().new_child("testFunction1") + } + + fn test_function_2_name() -> QualifiedName { + crate::test::mock::data::module_qualified_name().new_child("testFunction2") + } + + fn test_method_name() -> QualifiedName { + crate::test::mock::data::module_qualified_name().new_child("test_method") + } + + fn test_var_1_name() -> QualifiedName { + crate::test::mock::data::module_qualified_name().new_child("test_var_1") + } + + fn test_method_3_name() -> &'static str { + "test.Test.Test.test_method3" } fn suggestion_database_with_mock_entries( @@ -1847,7 +1498,7 @@ pub mod test { let database = mock_suggestion_database! { test.Test { mod Test { - static fn test_method(this: Standard.Base.Any, arg: Standard.Base.Text) -> Standard.Base.Text; + static fn test_method3(this: Standard.Base.Any, arg: Standard.Base.Text) -> Standard.Base.Text; } } mock_namespace.Mock_Project { @@ -1890,19 +1541,16 @@ pub mod test { Rc::new(database) } - /// Test checks that: /// 1) if the selected node is assigned to a single variable (or can be assigned), the list is /// not immediately presented; /// 2) instead the searcher model obtains the type information for the selected node and uses it /// to query Language Server for the suggestion list; /// 3) The query for argument type takes the this-argument presence into consideration. - #[wasm_bindgen_test] + #[test] fn loading_list_w_self() { let mock_type = crate::test::mock::data::TYPE_NAME; - /// The case is: `main` contains a single, selected node. Searcher is brought up. - /// Mock `entry1` suggestion is picked twice. struct Case { /// The single line of the initial `main` body. node_line: &'static str, @@ -1917,35 +1565,28 @@ pub mod test { ]; for case in &cases { - let Fixture { mut test, searcher, entry1, entry9, .. } = - Fixture::new_custom(|data, client| { + let mut fixture = + Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| { data.change_main_body(&[case.node_line]); data.selected_node = true; // We expect following calls: // 1) for the function - with the "this" filled (if the test case says so); // 2) for subsequent completions - without "self" - data.expect_completion(client, case.sets_this.as_some(mock_type), None, &[ - 1, 5, 9, - ]); - // If we are about to add the self type, then we expect the second argument - // first, and then none. Otherwise we expect all arguments - // starting from the first. - let expected_types = if case.sets_this { - [Some("Standard.Base.Number"), None] - } else { - [Some("Standard.Base.Text"), Some("Standard.Base.Number")] - }; + data.expect_completion(client, case.sets_this.as_some(mock_type), &[1, 5, 9]); - data.expect_completion(client, None, expected_types[0], &[1, 5, 9]); - data.expect_completion(client, None, expected_types[1], &[1, 5, 9]); + data.expect_completion(client, None, &[1, 5, 9]); + data.expect_completion(client, None, &[1, 5, 9]); }); + let test_function_1 = fixture.lookup(&test_function_1_name()); + let test_function_2 = fixture.lookup(&test_function_2_name()); + let searcher = &mut fixture.searcher; searcher.reload_list(); // The suggestion list should stall only if we actually use "this" argument. if case.sets_this { assert!(searcher.actions().is_loading()); - test.run_until_stalled(); + fixture.test.run_until_stalled(); // Nothing appeared, because we wait for type information for this node. assert!(searcher.actions().is_loading()); @@ -1955,130 +1596,97 @@ pub mod test { assert!(searcher.actions().is_loading()); } - test.run_until_stalled(); + fixture.test.run_until_stalled(); assert!(!searcher.actions().is_loading()); - searcher.use_suggestion(action::Suggestion::FromDatabase(entry9.clone_ref())).unwrap(); - searcher.use_suggestion(action::Suggestion::FromDatabase(entry1.clone_ref())).unwrap(); - let expected_input = format!("{} {} ", entry9.name, entry1.name); - assert_eq!(searcher.data.borrow().input.repr(), expected_input); + let suggestion2 = action::Suggestion::FromDatabase(test_function_2.clone_ref()); + searcher.use_suggestion(suggestion2).unwrap(); + let suggestion1 = action::Suggestion::FromDatabase(test_function_1.clone_ref()); + searcher.use_suggestion(suggestion1).unwrap(); + let expected_input = format!("{} {}", test_function_2.name, test_function_1.name); + assert_eq!(searcher.data.borrow().input.ast().repr(), expected_input); } } - #[wasm_bindgen_test] + #[test] fn arguments_suggestions_for_picked_method() { - let mut fixture = Fixture::new_custom(|data, client| { - data.expect_completion(client, None, Some("Standard.Base.Number"), &[20]); - }); - let Fixture { test, searcher, entry3, .. } = &mut fixture; - searcher.use_suggestion(action::Suggestion::FromDatabase(entry3.clone_ref())).unwrap(); + let mut fixture = + Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| { + data.expect_completion(client, None, &[20]); + }); + let test_method = fixture.lookup(&test_method_name()); + let Fixture { test, searcher, .. } = &mut fixture; + searcher.use_suggestion(action::Suggestion::FromDatabase(test_method.clone_ref())).unwrap(); assert!(searcher.actions().is_loading()); test.run_until_stalled(); assert!(!searcher.actions().is_loading()); } - #[wasm_bindgen_test] + #[test] fn arguments_suggestions_for_picked_function() { - let mut fixture = Fixture::new_custom(|data, client| { - data.expect_completion(client, None, Some("Standard.Base.Text"), &[]); // First arg suggestion. - data.expect_completion(client, None, Some("Standard.Base.Number"), &[]); // Second arg suggestion. - data.expect_completion(client, None, None, &[]); // Oversaturated arg position. - }); + let mut fixture = + Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| { + data.expect_completion(client, None, &[]); // Function suggestion. + }); - let Fixture { searcher, entry9, .. } = &mut fixture; - searcher.use_suggestion(action::Suggestion::FromDatabase(entry9.clone_ref())).unwrap(); - assert_eq!(searcher.data.borrow().input.repr(), "testFunction2 "); - searcher.set_input("testFunction2 'foo' ".to_owned()).unwrap(); - searcher.set_input("testFunction2 'foo' 10 ".to_owned()).unwrap(); + let test_function_2 = fixture.lookup(&test_function_2_name()); + let Fixture { searcher, .. } = &mut fixture; + searcher + .use_suggestion(action::Suggestion::FromDatabase(test_function_2.clone_ref())) + .unwrap(); + assert_eq!(searcher.data.borrow().input.ast().unwrap().repr(), "testFunction2"); + searcher.set_input("testFunction2 'foo' ".to_owned(), Byte(20)).unwrap(); + searcher.set_input("testFunction2 'foo' 10 ".to_owned(), Byte(23)).unwrap(); } - #[wasm_bindgen_test] + #[test] fn non_picked_function_arg_suggestions() { - let mut fixture = Fixture::new_custom(|data, client| { - data.graph.module.code.insert_str(0, "import test.Test.Test\n\n"); - data.code_location.line += 2; - data.expect_completion(client, None, Some("Standard.Base.Text"), &[1]); - data.expect_completion(client, None, Some("Standard.Base.Number"), &[]); - data.expect_completion(client, None, Some("Standard.Base.Number"), &[]); - data.expect_completion(client, None, None, &[1, 2, 3, 4, 9]); - }); + let mut fixture = + Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| { + data.graph.module.code.insert_str(0, "import test.Test.Test\n\n"); + data.code_location.line += 2; + data.expect_completion(client, None, &[1]); + data.expect_completion(client, None, &[]); + data.expect_completion(client, None, &[]); + }); let Fixture { searcher, .. } = &mut fixture; // Known functions cases - searcher.set_input("Test.test_method ".to_string()).unwrap(); - searcher.set_input(format!("{MODULE_NAME}.test_method ")).unwrap(); - searcher.set_input("testFunction2 \"str\" ".to_string()).unwrap(); + searcher.set_input("Test.test_method ".to_string(), Byte(16)).unwrap(); + searcher.set_input(format!("{MODULE_NAME}.test_method "), Byte(16)).unwrap(); + searcher.set_input("testFunction2 \"str\" ".to_string(), Byte(16)).unwrap(); // Unknown functions case - searcher.set_input("unknownFunction ".to_string()).unwrap(); + searcher.set_input("unknownFunction ".to_string(), Byte(14)).unwrap(); } - #[wasm_bindgen_test] - fn non_picked_function_arg_suggestion_ambiguous() { - fn run_case(input: impl Str, setup: impl FnOnce(&mut Fixture)) { - // In each case we expect that we can pick two methods with the same name, but different - // second argument, so the controller should call Engine for each type. - const EXPECTED_REQUESTS: usize = 2; - let requested_types: Rc>>> = default(); - let requested_types2 = requested_types.clone(); - let mut fixture = Fixture::new_custom(move |data, client| { - data.graph.module.code.insert_str(0, "import test.Test.Test\n\n"); - data.code_location.line += 2; - for _ in 0..EXPECTED_REQUESTS { - let requested_types = requested_types2.clone(); - client.expect.completion( - move |_path, _position, _self_type, return_type, _tags| { - requested_types.borrow_mut().insert(return_type.clone()); - Ok(completion_response(&[])) - }, - ); - } - }); - setup(&mut fixture); - let Fixture { test, searcher, .. } = &mut fixture; - searcher.set_input(input.into()).unwrap(); - test.run_until_stalled(); - assert_eq!(requested_types.borrow().len(), EXPECTED_REQUESTS); - assert!(requested_types.borrow().contains(&Some("Standard.Base.Number".to_string()))); - assert!(requested_types.borrow().contains(&Some("Standard.Base.Text".to_string()))); - } - - run_case("test_method (foo bar) ".to_string(), |_| {}); - run_case("(foo bar).test_method ".to_string(), |_| {}); - // Here the "Test" module is shadowed by local, so the call is ambiguous - run_case("Test.test_method ".to_string(), |fixture| { - let shadowing = model::suggestion_database::Entry { - name: "test".to_string(), - ..(*fixture.entry2).clone() - }; - fixture.searcher.database.put_entry(133, shadowing) - }); - } - - #[wasm_bindgen_test] + #[test] fn loading_list() { - let Fixture { mut test, searcher, entry1, entry9, .. } = - Fixture::new_custom(|data, client| { + let mut fixture = + Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| { // entry with id 99999 does not exist, so only two actions from suggestions db // should be displayed in searcher. - data.expect_completion(client, None, None, &[101, 99999, 103]); + data.expect_completion(client, None, &[101, 99999, 103]); }); + let test_function_1 = fixture.lookup(&test_function_1_name()); + let test_function_2 = fixture.lookup(&test_function_2_name()); + let searcher = &mut fixture.searcher; let mut subscriber = searcher.subscribe(); searcher.reload_list(); assert!(searcher.actions().is_loading()); - test.run_until_stalled(); + fixture.test.run_until_stalled(); let list = searcher.actions().list().unwrap().to_action_vec(); // There are 8 entries, because: 2 were returned from `completion` method, two are mocked, // and all of these are repeated in "All Search Result" category. assert_eq!(list.len(), 8); - assert_eq!(list[2], Action::Suggestion(action::Suggestion::FromDatabase(entry1))); - assert_eq!(list[3], Action::Suggestion(action::Suggestion::FromDatabase(entry9))); + assert_eq!(list[2], Action::Suggestion(action::Suggestion::FromDatabase(test_function_1))); + assert_eq!(list[3], Action::Suggestion(action::Suggestion::FromDatabase(test_function_2))); let notification = subscriber.next().boxed_local().expect_ready(); assert_eq!(notification, Some(Notification::NewActionList)); } - #[wasm_bindgen_test] + #[test] fn loading_components() { // Prepare a sample component group to be returned by a mock Language Server client. let module_qualified_name = crate::test::mock::data::module_qualified_name().to_string(); @@ -2099,25 +1707,33 @@ pub mod test { ], }; // Create a test fixture with mocked Engine responses. - let Fixture { mut test, searcher, entry3, entry9, .. } = - Fixture::new_custom(|data, client| { + let mut fixture = + Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| { // Entry with id 99999 does not exist, so only two actions from suggestions db // should be displayed in searcher. - data.expect_completion(client, None, None, &[5, 99999, 103]); + data.expect_completion(client, None, &[5, 99999, 103]); data.graph.ctx.component_groups = vec![sample_ls_component_group]; }); + let test_function_2 = fixture.lookup(&test_function_2_name()); + let test_method = fixture.lookup(&test_method_name()); + let searcher = &mut fixture.searcher; // Reload the components list in the Searcher. searcher.reload_list(); - test.run_until_stalled(); + fixture.test.run_until_stalled(); // Verify the contents of the components list loaded by the Searcher. let components = searcher.components(); if let [module_group] = &components.top_modules().next().unwrap()[..] { - let expected_group_name = - format!("{}.{}", entry3.defined_in.project().project, entry3.defined_in.name()); + let expected_group_name = format!( + "{}.{}", + test_method.defined_in.project().project, + test_method.defined_in.name() + ); assert_eq!(module_group.name, expected_group_name); let entries = module_group.entries.borrow(); - assert_matches!(entries.as_slice(), [e1, e2] if e1.name() == entry3.name && e2.name() - == entry9.name); + assert_matches!( + entries.as_slice(), + [e1, e2] if e1.name() == test_method.name && e2.name() == test_function_2.name + ); } else { panic!( "Wrong top modules in Component List: {:?}", @@ -2135,74 +1751,6 @@ pub mod test { assert_eq!(favorites_entries[0].id().unwrap(), 5); } - #[wasm_bindgen_test] - fn parsed_input() { - let parser = Parser::new(); - - fn args_reprs(prefix: &ast::prefix::Chain) -> Vec { - prefix.args.iter().map(|arg| arg.repr()).collect() - } - - let input = ""; - let parsed = ParsedInput::new(input.to_string(), &parser).unwrap(); - assert!(parsed.expression.is_none()); - assert_eq!(parsed.pattern.as_str(), ""); - - let input = "foo"; - let parsed = ParsedInput::new(input.to_string(), &parser).unwrap(); - assert!(parsed.expression.is_none()); - assert_eq!(parsed.pattern.as_str(), "foo"); - - let input = " foo"; - let parsed = ParsedInput::new(input.to_string(), &parser).unwrap(); - assert!(parsed.expression.is_none()); - assert_eq!(parsed.pattern_offset, 1); - assert_eq!(parsed.pattern.as_str(), "foo"); - - let input = "foo "; - let parsed = ParsedInput::new(input.to_string(), &parser).unwrap(); - let expression = parsed.expression.unwrap(); - assert_eq!(expression.off, 0); - assert_eq!(expression.func.repr(), "foo"); - assert_eq!(args_reprs(&expression), Vec::::new()); - assert_eq!(parsed.pattern_offset, 2); - assert_eq!(parsed.pattern.as_str(), ""); - - let input = "foo bar"; - let parsed = ParsedInput::new(input.to_string(), &parser).unwrap(); - let expression = parsed.expression.unwrap(); - assert_eq!(expression.off, 0); - assert_eq!(expression.func.repr(), "foo"); - assert_eq!(args_reprs(&expression), Vec::::new()); - assert_eq!(parsed.pattern_offset, 1); - assert_eq!(parsed.pattern.as_str(), "bar"); - - let input = "foo bar baz"; - let parsed = ParsedInput::new(input.to_string(), &parser).unwrap(); - let expression = parsed.expression.unwrap(); - assert_eq!(expression.off, 0); - assert_eq!(expression.func.repr(), "foo"); - assert_eq!(args_reprs(&expression), vec![" bar".to_string()]); - assert_eq!(parsed.pattern_offset, 2); - assert_eq!(parsed.pattern.as_str(), "baz"); - - let input = " foo bar baz "; - let parsed = ParsedInput::new(input.to_string(), &parser).unwrap(); - let expression = parsed.expression.unwrap(); - assert_eq!(expression.off, 2); - assert_eq!(expression.func.repr(), "foo"); - assert_eq!(args_reprs(&expression), vec![" bar".to_string(), " baz".to_string()]); - assert_eq!(parsed.pattern_offset, 1); - assert_eq!(parsed.pattern.as_str(), ""); - - let input = "foo bar (baz "; - let parsed = ParsedInput::new(input.to_string(), &parser).unwrap(); - let expression = parsed.expression.unwrap(); - assert_eq!(expression.off, 0); - assert_eq!(expression.func.repr(), "foo"); - assert_eq!(args_reprs(&expression), vec![" bar".to_string(), " (baz".to_string()]); - } - fn are_same( action: &action::Suggestion, entry: &Rc, @@ -2213,64 +1761,43 @@ pub mod test { } } - #[wasm_bindgen_test] + #[test] fn picked_completions_list_maintaining() { - let Fixture { test: _test, searcher, entry1, entry2, .. } = - Fixture::new_custom(|data, client| { - data.expect_completion(client, None, None, &[]); - data.expect_completion(client, None, None, &[]); - data.expect_completion(client, None, None, &[]); - data.expect_completion(client, None, None, &[]); - data.expect_completion(client, None, None, &[]); - data.expect_completion(client, None, None, &[]); - }); - let frags_borrow = || Ref::map(searcher.data.borrow(), |d| &d.fragments_added_by_picking); + let fixture = Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| { + data.expect_completion(client, None, &[]); + data.expect_completion(client, None, &[]); + data.expect_completion(client, None, &[]); + }); + let test_function_1 = fixture.lookup(&test_function_1_name()); + let test_var_1 = fixture.lookup(&test_var_1_name()); + let Fixture { test: _test, searcher, .. } = fixture; + let picked_suggestions = || Ref::map(searcher.data.borrow(), |d| &d.picked_suggestions); // Picking first suggestion. - let new_input = - searcher.use_suggestion(action::Suggestion::FromDatabase(entry1.clone_ref())).unwrap(); - assert_eq!(new_input, "testFunction1 "); - let (func,) = frags_borrow().iter().cloned().expect_tuple(); - assert_eq!(func.id, CompletedFragmentId::Function); - assert!(are_same(&func.picked_suggestion, &entry1)); + let suggestion = action::Suggestion::FromDatabase(test_function_1.clone_ref()); + let new_input = searcher.use_suggestion(suggestion).unwrap(); + assert_eq!(new_input.text, "testFunction1 "); + let (func,) = picked_suggestions().iter().cloned().expect_tuple(); + assert!(are_same(&func.entry, &test_function_1)); // Typing more args by hand. - searcher.set_input("testFunction1 some_arg pat".to_string()).unwrap(); - let (func,) = frags_borrow().iter().cloned().expect_tuple(); - assert_eq!(func.id, CompletedFragmentId::Function); - assert!(are_same(&func.picked_suggestion, &entry1)); + searcher.set_input("testFunction1 some_arg pat".to_string(), Byte(26)).unwrap(); + let (func,) = picked_suggestions().iter().cloned().expect_tuple(); + assert!(are_same(&func.entry, &test_function_1)); // Picking argument's suggestion. - let new_input = - searcher.use_suggestion(action::Suggestion::FromDatabase(entry2.clone_ref())).unwrap(); - assert_eq!(new_input, "testFunction1 some_arg test_var_1 "); - let new_input = - searcher.use_suggestion(action::Suggestion::FromDatabase(entry2.clone_ref())).unwrap(); - assert_eq!(new_input, "testFunction1 some_arg test_var_1 test_var_1 "); - let (function, arg1, arg2) = frags_borrow().iter().cloned().expect_tuple(); - assert_eq!(function.id, CompletedFragmentId::Function); - assert!(are_same(&function.picked_suggestion, &entry1)); - assert_eq!(arg1.id, CompletedFragmentId::Argument { index: 1 }); - assert!(are_same(&arg1.picked_suggestion, &entry2)); - assert_eq!(arg2.id, CompletedFragmentId::Argument { index: 2 }); - assert!(are_same(&arg2.picked_suggestion, &entry2)); - - // Backspacing back to the second arg. - searcher.set_input("testFunction1 some_arg test_var_1 test_v".to_string()).unwrap(); - let (picked, arg) = frags_borrow().iter().cloned().expect_tuple(); - assert_eq!(picked.id, CompletedFragmentId::Function); - assert!(are_same(&picked.picked_suggestion, &entry1)); - assert_eq!(arg.id, CompletedFragmentId::Argument { index: 1 }); - assert!(are_same(&arg.picked_suggestion, &entry2)); - - // Editing the picked function. - searcher.set_input("testFunction2 some_arg test_var_1 test_v".to_string()).unwrap(); - let (arg,) = frags_borrow().iter().cloned().expect_tuple(); - assert_eq!(arg.id, CompletedFragmentId::Argument { index: 1 }); - assert!(are_same(&arg.picked_suggestion, &entry2)); + let suggestion1 = action::Suggestion::FromDatabase(test_var_1.clone_ref()); + let new_input = searcher.use_suggestion(suggestion1.clone()).unwrap(); + assert_eq!(new_input.text, "test_var_1 "); + let new_input = searcher.use_suggestion(suggestion1).unwrap(); + assert_eq!(new_input.text, "test_var_1 "); + let (function, arg1, arg2) = picked_suggestions().iter().cloned().expect_tuple(); + assert!(are_same(&function.entry, &test_function_1)); + assert!(are_same(&arg1.entry, &test_var_1)); + assert!(are_same(&arg2.entry, &test_var_1)); } - #[wasm_bindgen_test] + #[test] fn applying_this_var() { #[derive(Copy, Clone, Debug)] struct Case { @@ -2308,22 +1835,25 @@ pub mod test { } } - #[wasm_bindgen_test] + #[test] fn adding_node_introducing_this_var() { struct Case { - line: &'static str, - result: String, - run: Box, + line: &'static str, + result: String, + expect_completion: bool, + run: Box, } impl Case { fn new( line: &'static str, result: &[&str], - run: impl FnOnce(&mut Fixture) + 'static, + expect_completion: bool, + run: impl FnOnce(&mut Fixture, action::Suggestion) + 'static, ) -> Self { Case { line, + expect_completion, result: crate::test::mock::main_from_lines(result), run: Box::new(run), } @@ -2332,74 +1862,95 @@ pub mod test { let cases = vec![ // Completion was picked. - Case::new("2 + 2", &["sum1 = 2 + 2", "operator1 = sum1.testFunction1"], |f| { - f.searcher - .use_suggestion(action::Suggestion::FromDatabase(f.entry1.clone())) - .unwrap(); - }), + Case::new( + "2 + 2", + &["sum1 = 2 + 2", "operator1 = sum1.testFunction1"], + true, + |f, s| { + f.searcher.use_suggestion(s).unwrap(); + }, + ), // The input was manually written (not picked). - Case::new("2 + 2", &["sum1 = 2 + 2", "operator1 = sum1.testFunction1"], |f| { - f.searcher.set_input("testFunction1 ".to_owned()).unwrap(); - }), + Case::new( + "2 + 2", + &["sum1 = 2 + 2", "operator1 = sum1.testFunction1"], + false, + |f, _| { + f.searcher.set_input("testFunction1".to_owned(), Byte(13)).unwrap(); + }, + ), // Completion was picked and edited. - Case::new("2 + 2", &["sum1 = 2 + 2", "operator1 = sum1.var.testFunction1"], |f| { - f.searcher - .use_suggestion(action::Suggestion::FromDatabase(f.entry1.clone())) - .unwrap(); - let new_parsed_input = - ParsedInput::new("var.testFunction1", f.searcher.ide.parser()); - f.searcher.data.borrow_mut().input = new_parsed_input.unwrap(); - }), + Case::new( + "2 + 2", + &["sum1 = 2 + 2", "operator1 = sum1.var.testFunction1"], + true, + |f, s| { + f.searcher.use_suggestion(s).unwrap(); + let parser = f.searcher.ide.parser(); + let expr = "var.testFunction1"; + let new_parsed_input = input::Input::parse(parser, expr, Byte(expr.len())); + f.searcher.data.borrow_mut().input = new_parsed_input; + }, + ), // Variable name already present, need to use it. And not break it. Case::new( "my_var = 2 + 2", &["my_var = 2 + 2", "operator1 = my_var.testFunction1"], - |f| { - f.searcher - .use_suggestion(action::Suggestion::FromDatabase(f.entry1.clone())) - .unwrap(); + true, + |f, s| { + f.searcher.use_suggestion(s).unwrap(); }, ), // Variable names unusable (subpatterns are not yet supported). // Don't use "this" argument adjustments at all. - Case::new("[x,y] = 2 + 2", &["[x,y] = 2 + 2", "testfunction11 = testFunction1"], |f| { - f.searcher - .use_suggestion(action::Suggestion::FromDatabase(f.entry1.clone())) - .unwrap(); - }), + Case::new( + "[x,y] = 2 + 2", + &["[x,y] = 2 + 2", "testfunction11 = testFunction1"], + true, + |f, s| { + f.searcher.use_suggestion(s).unwrap(); + }, + ), ]; for case in cases.into_iter() { - let mut fixture = Fixture::new_custom(|data, client| { - data.selected_node = true; - // The last node will be used as searcher target. - data.change_main_body(&[case.line, "Nothing"]); - data.expect_completion(client, None, None, &[]); - }); - (case.run)(&mut fixture); + let mut fixture = + Fixture::new_custom(suggestion_database_with_mock_entries, |data, client| { + data.selected_node = true; + // The last node will be used as searcher target. + data.change_main_body(&[case.line, "Nothing"]); + if case.expect_completion { + data.expect_completion(client, None, &[]); + } + }); + let entry = fixture.lookup(&test_function_1_name()); + let suggestion = action::Suggestion::FromDatabase(entry); + (case.run)(&mut fixture, suggestion); fixture.searcher.commit_node().unwrap(); let updated_def = fixture.searcher.graph.graph().definition().unwrap().item; assert_eq!(updated_def.ast.repr(), case.result); } } - #[wasm_bindgen_test] + #[test] fn adding_imports_with_nodes() { fn expect_inserted_import_for( entry: &Rc, expected_import: Vec<&QualifiedName>, ) { - let Fixture { test: _test, mut searcher, .. } = Fixture::new(); + let Fixture { test: _test, mut searcher, .. } = + Fixture::new_custom(suggestion_database_with_mock_entries, |_, _| {}); let module = searcher.graph.graph().module.clone_ref(); let parser = searcher.ide.parser().clone_ref(); - let picked_method = FragmentAddedByPickingSuggestion { - id: CompletedFragmentId::Function, - picked_suggestion: action::Suggestion::FromDatabase(entry.clone_ref()), + let picked_method = PickedSuggestion { + entry: action::Suggestion::FromDatabase(entry.clone_ref()), + inserted_code: default(), + import: Some(RequiredImport::Entry(entry.clone_ref())), }; with(searcher.data.borrow_mut(), |mut data| { - data.fragments_added_by_picking.push(picked_method); - data.input = ParsedInput::new(entry.name.to_string(), &parser).unwrap(); + data.picked_suggestions.push(picked_method); + data.input = input::Input::parse(&parser, entry.name.to_string(), default()); }); // Add new node. @@ -2417,33 +1968,40 @@ pub mod test { assert_eq!(imported_names, expected_import); } - let Fixture { entry1, entry2, entry3, entry4, .. } = Fixture::new(); - expect_inserted_import_for(&entry1, vec![]); - expect_inserted_import_for(&entry2, vec![]); - expect_inserted_import_for(&entry3, vec![]); - expect_inserted_import_for(&entry4, vec![&entry4.defined_in]); + let fixture = Fixture::new_custom(suggestion_database_with_mock_entries, |_, _| {}); + let test_function_1 = fixture.lookup(&test_function_1_name()); + let test_var_1 = fixture.lookup(&test_var_1_name()); + let test_method = fixture.lookup(&test_function_1_name()); + let test_method_3 = fixture.lookup_str(test_method_3_name()); + expect_inserted_import_for(&test_function_1, vec![]); + expect_inserted_import_for(&test_var_1, vec![]); + expect_inserted_import_for(&test_method, vec![]); + expect_inserted_import_for(&test_method_3, vec![&test_method_3.defined_in]); } - #[wasm_bindgen_test] + #[test] fn committing_node() { - let Fixture { test: _test, mut searcher, entry4, .. } = - Fixture::new_custom(|data, _client| { + let mut fixture = + Fixture::new_custom(suggestion_database_with_mock_entries, |data, _client| { data.change_main_body(&["2 + 2", "Nothing"]); // The last node will be used as // searcher target. }); + let test_method_3 = fixture.lookup_str(test_method_3_name()); + let searcher = &mut fixture.searcher; let (node1, searcher_target) = searcher.graph.graph().nodes().unwrap().expect_tuple(); let module = searcher.graph.graph().module.clone_ref(); // Setup searcher. let parser = Parser::new(); - let picked_method = FragmentAddedByPickingSuggestion { - id: CompletedFragmentId::Function, - picked_suggestion: action::Suggestion::FromDatabase(entry4), + let picked_method = PickedSuggestion { + entry: action::Suggestion::FromDatabase(test_method_3.clone_ref()), + inserted_code: String::from("Test.test_method"), + import: Some(RequiredImport::Entry(test_method_3.clone_ref())), }; with(searcher.data.borrow_mut(), |mut data| { - data.fragments_added_by_picking.push(picked_method); - data.input = ParsedInput::new("Test.test_method".to_string(), &parser).unwrap(); + data.picked_suggestions.push(picked_method); + data.input = input::Input::parse(&parser, "Test.test_method".to_string(), Byte(16)); }); // Add new node. @@ -2454,99 +2012,16 @@ pub mod test { let expected_code = "import test.Test.Test\nmain =\n 2 + 2\n operator1 = Test.test_method"; assert_eq!(module.ast().repr(), expected_code); - let expected_intended_method = Some(MethodId { - module: "test.Test.Test".to_string().try_into().unwrap(), - defined_on_type: "test.Test.Test".to_string().try_into().unwrap(), - name: "test_method".to_string(), - }); - let (_, searcher_target) = searcher.graph.graph().nodes().unwrap().expect_tuple(); - assert_eq!(searcher_target.metadata.unwrap().intended_method, expected_intended_method); // Edit existing node. searcher.mode = Immutable(Mode::EditNode { node_id: node1.info.id() }); searcher.commit_node().unwrap(); let expected_code = "import test.Test.Test\nmain =\n Test.test_method\n operator1 = Test.test_method"; - let (node1, _) = searcher.graph.graph().nodes().unwrap().expect_tuple(); - assert_eq!(node1.metadata.unwrap().intended_method, expected_intended_method); assert_eq!(module.ast().repr(), expected_code); } - #[wasm_bindgen_test] - fn initialized_data_when_editing_node() { - let Fixture { test: _test, searcher, entry4, .. } = Fixture::new(); - - let graph = searcher.graph.graph(); - let (node,) = graph.nodes().unwrap().expect_tuple(); - let node_id = node.info.id(); - let database = searcher.database; - - // Node had not intended method. - let searcher_data = Data::new_with_edited_node(&graph, &database, node_id).unwrap(); - assert_eq!(searcher_data.input.repr(), node.info.expression().repr()); - assert!(searcher_data.fragments_added_by_picking.is_empty()); - assert!(searcher_data.actions.is_loading()); - - // Node had intended method, but it's outdated. - let intended_method = MethodId { - module: "test.Test.Test".to_string().try_into().unwrap(), - defined_on_type: "test.Test.Test".to_string().try_into().unwrap(), - name: "test_method".to_string(), - }; - graph - .module - .with_node_metadata( - node_id, - Box::new(|md| { - md.intended_method = Some(intended_method); - }), - ) - .unwrap(); - let searcher_data = Data::new_with_edited_node(&graph, &database, node_id).unwrap(); - assert_eq!(searcher_data.input.repr(), node.info.expression().repr()); - assert!(searcher_data.fragments_added_by_picking.is_empty()); - assert!(searcher_data.actions.is_loading()); - - // Node had up-to-date intended method. - graph.set_expression(node_id, "Test.test_method 12").unwrap(); - // We set metadata in previous section. - let searcher_data = Data::new_with_edited_node(&graph, &database, node_id).unwrap(); - assert_eq!(searcher_data.input.repr(), "Test.test_method 12"); - assert!(searcher_data.actions.is_loading()); - let (initial_fragment,) = searcher_data.fragments_added_by_picking.expect_tuple(); - assert!(are_same(&initial_fragment.picked_suggestion, &entry4)) - } - - #[wasm_bindgen_test] - fn simple_function_call_parsing() { - let parser = Parser::new(); - - let ast = parser.parse_line_ast("foo").unwrap(); - let call = SimpleFunctionCall::try_new(&ast).expect("Returned None for \"foo\""); - assert!(call.this_argument.is_none()); - assert_eq!(call.function_name, "foo"); - - let ast = parser.parse_line_ast("Main.foo").unwrap(); - let call = SimpleFunctionCall::try_new(&ast).expect("Returned None for \"Main.foo\""); - assert_eq!(call.this_argument.unwrap().repr(), "Main"); - assert_eq!(call.function_name, "foo"); - - let ast = parser.parse_line_ast("(2 + 3).foo").unwrap(); - let call = SimpleFunctionCall::try_new(&ast).expect("Returned None for \"(2 + 3).foo\""); - assert_eq!(call.this_argument.unwrap().repr(), "(2 + 3)"); - assert_eq!(call.function_name, "foo"); - - let ast = parser.parse_line_ast("foo + 3").unwrap(); - assert!(SimpleFunctionCall::try_new(&ast).is_none()); - - let ast = parser.parse_line_ast("foo bar baz").unwrap(); - assert!(SimpleFunctionCall::try_new(&ast).is_none()); - - let ast = parser.parse_line_ast("Main . (foo bar)").unwrap(); - assert!(SimpleFunctionCall::try_new(&ast).is_none()); - } - - #[wasm_bindgen_test] + #[test] fn adding_example() { let Fixture { test: _test, searcher, .. } = Fixture::new(); let module = searcher.graph.graph().module.clone_ref(); @@ -2562,7 +2037,7 @@ pub mod test { assert_eq!(module.ast().repr(), expected_code); } - #[wasm_bindgen_test] + #[test] fn adding_example_twice() { let Fixture { test: _test, searcher, .. } = Fixture::new(); let module = searcher.graph.graph().module.clone_ref(); @@ -2581,7 +2056,7 @@ pub mod test { assert_eq!(module.ast().repr(), expected_code); } - #[wasm_bindgen_test] + #[test] fn edit_guard() { let Fixture { test: _test, mut searcher, .. } = Fixture::new(); let graph = searcher.graph.graph(); @@ -2604,8 +2079,7 @@ pub mod test { assert_eq!( metadata.edit_status, Some(NodeEditStatus::Edited { - previous_expression: node.info.expression().to_string(), - previous_intended_method: None, + previous_expression: node.info.expression().to_string(), }) ); }), @@ -2629,7 +2103,7 @@ pub mod test { assert_eq!(initial_node_expression.to_string(), final_node_expression.to_string()); } - #[wasm_bindgen_test] + #[test] fn edit_guard_no_revert() { let Fixture { test: _test, mut searcher, .. } = Fixture::new(); let graph = searcher.graph.graph(); @@ -2651,4 +2125,134 @@ pub mod test { let final_node_expression = node.main_line.expression(); assert_eq!(final_node_expression.to_string(), new_expression); } + + /// Test recognition of qualified names in the searcher's input. + #[test] + fn recognize_qualified_names() { + fn database() -> Rc { + mock_suggestion_database! { + mock_namespace.MockProject { + mod Foo { + mod Bar { + type Baz { + fn baz_method() -> Standard.Base.Number; + } + + static fn bar_method() -> Standard.Base.Number; + } + } + + mod Table { + mod Data { + mod Table { + type Table { + static fn new() -> mock_namespace.MockProject.Table.Data.Table.Table; + } + } + } + } + + static fn project_method() -> Standard.Base.Number; + } + } + .into() + } + + #[derive(Debug)] + struct Case { + entry: String, + input: String, + expected_code: String, + expected_imports: Vec, + } + + let cases = vec![ + Case { + entry: "mock_namespace.MockProject.Foo.Bar.Baz.baz_method".to_string(), + input: "Foo.Bar.".to_string(), + expected_code: "operator1 = Foo.Bar.Baz.baz_method".to_string(), + expected_imports: vec!["import mock_namespace.MockProject.Foo".to_string()], + }, + Case { + entry: "mock_namespace.MockProject.Foo.Bar.Baz.baz_method".to_string(), + input: "Bar.".to_string(), + expected_code: "operator1 = Bar.Baz.baz_method".to_string(), + expected_imports: vec!["import mock_namespace.MockProject.Foo.Bar".to_string()], + }, + Case { + entry: "mock_namespace.MockProject.Foo.Bar.bar_method".to_string(), + input: "Bar.".to_string(), + expected_code: "operator1 = Bar.bar_method".to_string(), + expected_imports: vec!["import mock_namespace.MockProject.Foo.Bar".to_string()], + }, + Case { + entry: "mock_namespace.MockProject.Foo.Bar.bar_method".to_string(), + input: "Foo.Gee.".to_string(), + expected_code: "operator1 = Bar.bar_method".to_string(), + expected_imports: vec!["import mock_namespace.MockProject.Foo.Bar".to_string()], + }, + Case { + entry: "mock_namespace.MockProject.Foo.Bar.bar_method".to_string(), + input: "mock_namespace.MockProject.Foo.Bar.".to_string(), + expected_code: "operator1 = mock_namespace.MockProject.Foo.Bar.bar_method" + .to_string(), + expected_imports: vec![], + }, + Case { + entry: "mock_namespace.MockProject.Table.Data.Table.Table.new" + .to_string(), + input: "Table.".to_string(), + expected_code: "operator1 = Table.new".to_string(), + expected_imports: vec![ + "from mock_namespace.MockProject.Table.Data.Table import Table".to_string(), + ], + }, + Case { + entry: "mock_namespace.MockProject.project_method".to_string(), + input: "mock_namespace.MockProject.".to_string(), + expected_code: "operator1 = mock_namespace.MockProject.project_method" + .to_string(), + expected_imports: vec![], + }, + Case { + entry: "mock_namespace.MockProject.project_method".to_string(), + input: "MockProject.".to_string(), + expected_code: "operator1 = MockProject.project_method".to_string(), + expected_imports: vec!["import mock_namespace.MockProject".to_string()], + }, + Case { + entry: "mock_namespace.MockProject.Foo.Bar.bar_method".to_string(), + input: "MockProject.".to_string(), + expected_code: "operator1 = MockProject.Foo.Bar.bar_method".to_string(), + expected_imports: vec!["import mock_namespace.MockProject".to_string()], + }, + ]; + + for case in cases { + let mut fixture = Fixture::new_custom( + |_| database(), + |data, client| { + // data.selected_node = true; + data.change_main_body(&["Nothing"]); + data.expect_completion(client, None, &[]); + data.expect_completion(client, None, &[]); + }, + ); + let entry = fixture.lookup_str(&case.entry); + let searcher = &mut fixture.searcher; + + searcher.set_input(case.input.clone(), Byte(case.input.len())).unwrap(); + let suggestion = action::Suggestion::FromDatabase(entry.clone_ref()); + searcher.preview_suggestion(suggestion.clone()).unwrap(); + searcher.use_suggestion(suggestion.clone()).unwrap(); + searcher.commit_node().unwrap(); + let updated_def = searcher.graph.graph().definition().unwrap().item; + let expected = crate::test::mock::main_from_lines(&[case.expected_code.clone()]); + assert_eq!(updated_def.ast.repr(), expected, "{case:?}"); + let module_info = &searcher.graph.graph().module.info(); + let imports = module_info.iter_imports(); + let imports = imports.map(|i| i.to_string()).collect_vec(); + assert_eq!(imports, case.expected_imports, "{case:?}"); + } + } } diff --git a/app/gui/src/controller/searcher/action.rs b/app/gui/src/controller/searcher/action.rs index ee4224ae98b..1cb848f360e 100644 --- a/app/gui/src/controller/searcher/action.rs +++ b/app/gui/src/controller/searcher/action.rs @@ -2,10 +2,7 @@ use crate::prelude::*; -use crate::model::SuggestionDatabase; - use double_representation::module::MethodId; -use double_representation::name::QualifiedNameRef; use ordered_float::OrderedFloat; @@ -21,7 +18,7 @@ pub mod hardcoded; // === Action === // ============== -#[derive(Clone, CloneRef, Debug, Eq, PartialEq)] +#[derive(Clone, CloneRef, Debug, PartialEq)] /// Suggestion for code completion: possible functions, arguments, etc. pub enum Suggestion { /// The suggestion from Suggestion Database received from the Engine. @@ -35,18 +32,7 @@ impl Suggestion { pub fn code_to_insert(&self, generate_this: bool) -> Cow { match self { Suggestion::FromDatabase(s) => s.code_to_insert(generate_this), - Suggestion::Hardcoded(s) => s.code.into(), - } - } - - pub(crate) fn required_imports( - &self, - db: &SuggestionDatabase, - current_module: QualifiedNameRef, - ) -> impl IntoIterator { - match self { - Suggestion::FromDatabase(s) => s.required_imports(db, current_module), - Suggestion::Hardcoded(_) => default(), + Suggestion::Hardcoded(s) => s.code.as_str().into(), } } @@ -89,7 +75,7 @@ pub enum ProjectManagement { } /// A single action on the Searcher list. See also `controller::searcher::Searcher` docs. -#[derive(Clone, CloneRef, Debug, Eq, PartialEq)] +#[derive(Clone, CloneRef, Debug, PartialEq)] pub enum Action { /// Add to the searcher input a suggested code and commit editing (new node is inserted or /// existing is modified). This action can be also used to complete searcher input without @@ -215,7 +201,7 @@ impl Eq for MatchInfo {} /// The single list entry. #[allow(missing_docs)] -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct ListEntry { pub category: CategoryId, pub match_info: MatchInfo, diff --git a/app/gui/src/controller/searcher/component.rs b/app/gui/src/controller/searcher/component.rs index 5c07c0149dc..51aba8d6eca 100644 --- a/app/gui/src/controller/searcher/component.rs +++ b/app/gui/src/controller/searcher/component.rs @@ -7,6 +7,7 @@ use crate::prelude::*; use crate::model::suggestion_database; use controller::searcher::action::MatchKind; +use controller::searcher::Filter; use convert_case::Case; use convert_case::Casing; use double_representation::name::QualifiedName; @@ -145,7 +146,7 @@ impl Component { pub fn name(&self) -> &str { match &self.data { Data::FromDatabase { entry, .. } => entry.name.as_str(), - Data::Virtual { snippet } => snippet.name, + Data::Virtual { snippet } => snippet.name.as_str(), } } @@ -174,13 +175,13 @@ impl Component { /// Update matching info. /// /// It should be called each time the filtering pattern changes. - pub fn update_matching_info(&self, pattern: impl Str) { + pub fn update_matching_info(&self, filter: Filter) { // Match the input pattern to the component label. let label = self.to_string(); - let label_matches = fuzzly::matches(&label, pattern.as_ref()); + let label_matches = fuzzly::matches(&label, filter.pattern.clone_ref()); let label_subsequence = label_matches.and_option_from(|| { let metric = fuzzly::metric::default(); - fuzzly::find_best_subsequence(label, pattern.as_ref(), metric) + fuzzly::find_best_subsequence(label, filter.pattern.clone_ref(), metric) }); let label_match_info = label_subsequence .map(|subsequence| MatchInfo::Matches { subsequence, kind: MatchKind::Label }); @@ -190,10 +191,10 @@ impl Component { Data::FromDatabase { entry, .. } => entry.code_to_insert(true).to_string(), Data::Virtual { snippet } => snippet.code.to_string(), }; - let code_matches = fuzzly::matches(&code, pattern.as_ref()); + let code_matches = fuzzly::matches(&code, filter.pattern.clone_ref()); let code_subsequence = code_matches.and_option_from(|| { let metric = fuzzly::metric::default(); - fuzzly::find_best_subsequence(code, pattern.as_ref(), metric) + fuzzly::find_best_subsequence(code, filter.pattern.clone_ref(), metric) }); let code_match_info = code_subsequence.map(|subsequence| { let subsequence = fuzzly::Subsequence { indices: Vec::new(), ..subsequence }; @@ -202,9 +203,10 @@ impl Component { // Match the input pattern to an entry's aliases and select the best alias match. let alias_matches = self.aliases().filter_map(|alias| { - if fuzzly::matches(alias, pattern.as_ref()) { + if fuzzly::matches(alias, filter.pattern.clone_ref()) { let metric = fuzzly::metric::default(); - let subsequence = fuzzly::find_best_subsequence(alias, pattern.as_ref(), metric); + let subsequence = + fuzzly::find_best_subsequence(alias, filter.pattern.clone_ref(), metric); subsequence.map(|subsequence| (subsequence, alias)) } else { None @@ -223,6 +225,18 @@ impl Component { let match_info_iter = [alias_match_info, code_match_info, label_match_info].into_iter(); let best_match_info = match_info_iter.flatten().max_by(|lhs, rhs| lhs.cmp(rhs)); *self.match_info.borrow_mut() = best_match_info.unwrap_or(MatchInfo::DoesNotMatch); + + // Filter out components with FQN not matching the context. + if let Some(context) = filter.context { + if let Data::FromDatabase { entry, .. } = &self.data { + if !entry.qualified_name().to_string().contains(context.as_str()) { + *self.match_info.borrow_mut() = MatchInfo::DoesNotMatch; + } + } else { + // Remove virtual entries if the context is present. + *self.match_info.borrow_mut() = MatchInfo::DoesNotMatch; + } + } } /// Check whether the component contains the "PRIVATE" tag. @@ -382,22 +396,22 @@ impl List { } /// Update matching info in all components according to the new filtering pattern. - pub fn update_filtering(&self, pattern: impl AsRef) { - let pattern = pattern.as_ref(); + pub fn update_filtering(&self, filter: Filter) { + let pattern = &filter.pattern; for component in &*self.all_components { - component.update_matching_info(pattern) + component.update_matching_info(filter.clone_ref()) } - let pattern_not_empty = !pattern.is_empty(); + let filtering_enabled = !pattern.is_empty() || filter.context.is_some(); let submodules_order = - if pattern_not_empty { Order::ByMatch } else { Order::ByNameNonModulesThenModules }; - let favorites_order = if pattern_not_empty { Order::ByMatch } else { Order::Initial }; + if filtering_enabled { Order::ByMatch } else { Order::ByNameNonModulesThenModules }; + let favorites_order = if filtering_enabled { Order::ByMatch } else { Order::Initial }; for group in self.all_groups_not_in_favorites() { group.update_match_info_and_sorting(submodules_order); } for group in self.favorites.iter() { group.update_match_info_and_sorting(favorites_order); } - self.filtered.set(pattern_not_empty); + self.filtered.set(filtering_enabled); } /// All groups from [`List`] without the groups found in [`List::favorites`]. @@ -517,7 +531,7 @@ pub(crate) mod tests { builder.extend_list_and_allow_favorites_with_ids(&suggestion_db, 0..=4); let list = builder.build(); - list.update_filtering("fu"); + list.update_filtering(Filter { pattern: "fu".into(), context: None }); let match_infos = list.top_modules().next().unwrap()[0] .entries .borrow() @@ -529,22 +543,22 @@ pub(crate) mod tests { assert_ids_of_matches_entries(&list.favorites[0], &[4, 2]); assert_ids_of_matches_entries(&list.local_scope, &[2]); - list.update_filtering("x"); + list.update_filtering(Filter { pattern: "x".into(), context: None }); assert_ids_of_matches_entries(&list.top_modules().next().unwrap()[0], &[4]); assert_ids_of_matches_entries(&list.favorites[0], &[4]); assert_ids_of_matches_entries(&list.local_scope, &[]); - list.update_filtering("Sub"); + list.update_filtering(Filter { pattern: "Sub".into(), context: None }); assert_ids_of_matches_entries(&list.top_modules().next().unwrap()[0], &[3]); assert_ids_of_matches_entries(&list.favorites[0], &[]); assert_ids_of_matches_entries(&list.local_scope, &[]); - list.update_filtering("y"); + list.update_filtering(Filter { pattern: "y".into(), context: None }); assert_ids_of_matches_entries(&list.top_modules().next().unwrap()[0], &[]); assert_ids_of_matches_entries(&list.favorites[0], &[]); assert_ids_of_matches_entries(&list.local_scope, &[]); - list.update_filtering(""); + list.update_filtering(Filter { pattern: "".into(), context: None }); assert_ids_of_matches_entries(&list.top_modules().next().unwrap()[0], &[2, 3]); assert_ids_of_matches_entries(&list.favorites[0], &[4, 2]); assert_ids_of_matches_entries(&list.local_scope, &[2]); diff --git a/app/gui/src/controller/searcher/component/builder.rs b/app/gui/src/controller/searcher/component/builder.rs index 16d59d0452a..fc4ee8e404f 100644 --- a/app/gui/src/controller/searcher/component/builder.rs +++ b/app/gui/src/controller/searcher/component/builder.rs @@ -580,7 +580,7 @@ mod tests { components: vec![qn_of_db_entry_1.clone()], }]; builder.set_grouping_and_order_of_favorites(&db, &groups); - let snippet = component::hardcoded::Snippet { name: "test snippet", ..default() }; + let snippet = component::hardcoded::Snippet { name: "test snippet".into(), ..default() }; let snippet_iter = std::iter::once(Rc::new(snippet)); builder.insert_virtual_components_in_favorites_group(GROUP_NAME, project, snippet_iter); builder.extend_list_and_allow_favorites_with_ids(&db, std::iter::once(1)); @@ -607,7 +607,7 @@ mod tests { components: vec![qn_of_db_entry_1.clone()], }]; builder.set_grouping_and_order_of_favorites(&db, &groups); - let snippet = component::hardcoded::Snippet { name: "test snippet", ..default() }; + let snippet = component::hardcoded::Snippet { name: "test snippet".into(), ..default() }; let snippet_iter = std::iter::once(Rc::new(snippet)); const GROUP_2_NAME: &str = "Group 2"; builder.insert_virtual_components_in_favorites_group(GROUP_2_NAME, project, snippet_iter); diff --git a/app/gui/src/controller/searcher/component/hardcoded.rs b/app/gui/src/controller/searcher/component/hardcoded.rs index ff94aa03db0..20d49480647 100644 --- a/app/gui/src/controller/searcher/component/hardcoded.rs +++ b/app/gui/src/controller/searcher/component/hardcoded.rs @@ -7,8 +7,11 @@ use crate::prelude::*; +use crate::controller::searcher::input; + use double_representation::name::QualifiedName; -use enso_doc_parser::DocSection; +use enso_suggestion_database::documentation_ir::EntryDocumentation; +use enso_suggestion_database::SuggestionDatabase; use ide_view::component_browser::component_list_panel::grid::entry::icon::Id as IconId; @@ -20,6 +23,10 @@ use ide_view::component_browser::component_list_panel::grid::entry::icon::Id as /// Name of the favorites component group in the `Standard.Base` library where virtual components /// created from the [`INPUT_SNIPPETS`] should be added. pub const INPUT_GROUP_NAME: &str = "Input"; +/// Qualified name of the `Text` type. +const TEXT_ENTRY: &str = "Standard.Base.Main.Data.Text.Text"; +/// Qualified name of the `Number` type. +const NUMBER_ENTRY: &str = "Standard.Base.Main.Data.Numbers.Number"; thread_local! { /// Snippets describing virtual components displayed in the [`INPUT_GROUP_NAME`] favorites @@ -29,7 +36,7 @@ thread_local! { pub static INPUT_SNIPPETS: Vec> = vec![ Snippet::new("text input", "\"\"", IconId::TextInput) .with_return_types(["Standard.Base.Data.Text.Text"]) - .with_documentation( + .with_documentation_str( "A text input node.\n\n\ An empty text. The value can be edited and used as an input for other nodes.", ) @@ -40,7 +47,7 @@ thread_local! { "Standard.Base.Data.Numbers.Decimal", "Standard.Base.Data.Numbers.Integer", ]) - .with_documentation( + .with_documentation_str( "A number input node.\n\n\ A zero number. The value can be edited and used as an input for other nodes.", ) @@ -49,39 +56,24 @@ thread_local! { } -// === Filtering by Return Type === - -/// Return a filtered copy of [`INPUT_SNIPPETS`] containing only snippets which have at least one -/// of their return types on the given list of return types. -pub fn input_snippets_with_matching_return_type( - return_types: impl IntoIterator, -) -> Vec> { - let rt_set: HashSet<_> = return_types.into_iter().collect(); - let rt_of_snippet_is_in_set = - |s: &&Rc| s.return_types.iter().any(|rt| rt_set.contains(rt)); - INPUT_SNIPPETS - .with(|snippets| snippets.iter().filter(rt_of_snippet_is_in_set).cloned().collect_vec()) -} - - // =============== // === Snippet === // =============== /// A hardcoded snippet of code with a description and syntactic metadata. -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Snippet { /// The name displayed in the [Component Browser](crate::controller::searcher). - pub name: &'static str, + pub name: ImString, /// The code inserted when picking the snippet. - pub code: &'static str, + pub code: ImString, /// A list of types that the return value of this snippet's code typechecks as. Used by the /// [Component Browser](crate::controller::searcher) to decide whether to display the /// snippet when filtering components by return type. pub return_types: Vec, /// The documentation bound to the snippet. - pub documentation: Option>, + pub documentation: Option, /// The ID of the icon bound to this snippet's entry in the [Component /// Browser](crate::controller::searcher). pub icon: IconId, @@ -89,8 +81,31 @@ pub struct Snippet { impl Snippet { /// Construct a hardcoded snippet with given name, code, and icon. - fn new(name: &'static str, code: &'static str, icon: IconId) -> Self { - Self { name, code, icon, ..default() } + fn new(name: &str, code: &str, icon: IconId) -> Self { + Self { name: name.into(), code: code.into(), icon, ..default() } + } + + /// Construct a hardcoded snippet for a single literal. + pub fn from_literal(literal: &input::Literal, db: &SuggestionDatabase) -> Self { + use input::Literal::*; + let text_repr = literal.to_string(); + let snippet = match literal { + Text { closing_delimiter: missing_quotation_mark, .. } => { + let missing_quote = missing_quotation_mark.as_ref(); + let code = &missing_quote.map(ToString::to_string).unwrap_or_default(); + Self::new(&text_repr, code, IconId::TextInput) + } + Number(_) => Self::new(&text_repr, "", IconId::NumberInput), + }; + let entry_path = match literal { + Text { .. } => TEXT_ENTRY, + Number(_) => NUMBER_ENTRY, + }; + // `unwrap()` is safe here, because we test the validity of `entry_path` in the tests. + let qualified_name = QualifiedName::from_text(entry_path).unwrap(); + let entry = db.lookup_by_qualified_name(&qualified_name); + let docs = entry.map(|(entry_id, _)| db.documentation_for_entry(entry_id)); + snippet.with_documentation(docs.unwrap_or_default()) } /// Returns a modified suggestion with [`Snippet::return_types`] field set. This method is only @@ -104,8 +119,34 @@ impl Snippet { /// Returns a modified suggestion with the [`Snippet::documentation`] field set. This method /// is only intended to be used when defining hardcoded suggestions. - fn with_documentation(mut self, documentation: &str) -> Self { - self.documentation = Some(enso_doc_parser::parse(documentation)); + fn with_documentation_str(mut self, documentation: &str) -> Self { + let docs = EntryDocumentation::builtin(&enso_doc_parser::parse(documentation)); + self.documentation = Some(docs); + self + } + + /// Returns a modified suggestion with the [`Snippet::documentation`] field set. + fn with_documentation(mut self, documentation: EntryDocumentation) -> Self { + self.documentation = Some(documentation); self } } + + + +// ============= +// === Tests === +// ============= + +#[cfg(test)] +mod tests { + use super::*; + + /// Test that the qualified names used for hardcoded snippets can be constructed. We don't check + /// if the entries are actually available in the suggestion database. + #[test] + fn test_qualified_names_construction() { + QualifiedName::from_text(TEXT_ENTRY).unwrap(); + QualifiedName::from_text(NUMBER_ENTRY).unwrap(); + } +} diff --git a/app/gui/src/controller/searcher/input.rs b/app/gui/src/controller/searcher/input.rs new file mode 100644 index 00000000000..e64f9a8c379 --- /dev/null +++ b/app/gui/src/controller/searcher/input.rs @@ -0,0 +1,700 @@ +//! A module containing structures keeping and interpreting the input of the searcher controller. + +use crate::prelude::*; + +use crate::controller::searcher::action; +use crate::controller::searcher::Filter; +use crate::controller::searcher::RequiredImport; + +use ast::HasTokens; +use double_representation::name::QualifiedName; +use enso_text as text; +use parser::Parser; + + + +// ============== +// === Errors === +// ============== + +#[derive(Clone, Debug, Fail)] +#[allow(missing_docs)] +#[fail(display = "Not a char boundary: index {} at '{}'.", index, string)] +pub struct NotACharBoundary { + index: usize, + string: String, +} + + + +// =============== +// === Literal === +// =============== + +/// A text or number literal. +#[derive(Clone, Debug, PartialEq)] +pub enum Literal { + /// A literal of a number type. + Number(ast::known::Number), + /// A literal of a text type. + Text { + /// The string representation of the finished text literal. Includes the closing + /// delimiter even if the user didn't type it yet. + text: ImString, + /// The missing closing delimiter if the user didn't type it yet. + closing_delimiter: Option<&'static str>, + }, +} + +impl Display for Literal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Text { text, .. } => write!(f, "{text}"), + Self::Number(number) => write!(f, "{}", number.repr()), + } + } +} + +impl Literal { + /// Construct a string literal. Returns `None` if the given string is not starting with a + /// quotation mark. + fn try_new_text(text: &str) -> Option { + for delimiter in ast::repr::STRING_DELIMITERS { + if text.starts_with(delimiter) { + let delimiter_only = text == *delimiter; + let closing_delimiter = if text.ends_with(delimiter) && !delimiter_only { + None + } else { + Some(*delimiter) + }; + let text = if let Some(delimiter) = closing_delimiter { + let mut text = text.to_owned(); + text.push_str(delimiter); + text.into() + } else { + text.into() + }; + return Some(Self::Text { text, closing_delimiter }); + } + } + None + } +} + + + +// ==================== +// === AstWithRange === +// ==================== + +/// A helper structure binding given AST node to the range of Component Browser input. +#[derive(Clone, Debug)] +pub struct AstWithRange { + ast: A, + range: text::Range, +} + + + +// ========================= +// === AstPositionFinder === +// ========================= + +/// A structure locating what Ast overlaps with given text offset. +#[derive(Clone, Debug, Default)] +struct AstAtPositionFinder { + current_offset: text::Byte, + target_offset: text::Byte, + result: Vec, +} + +impl AstAtPositionFinder { + fn new(target_offset: text::Byte) -> Self { + Self { target_offset, ..default() } + } + + /// Find all AST nodes containing the character in the `target` offset. The most nested AST + /// nodes will be last on the returned list. + fn find(ast: &Ast, target: text::Byte) -> Vec { + let mut finder = Self::new(target); + ast.feed_to(&mut finder); + finder.result + } +} + +impl ast::TokenConsumer for AstAtPositionFinder { + fn feed(&mut self, token: ast::Token) { + match token { + ast::Token::Ast(ast) if self.current_offset <= self.target_offset => { + let start = self.current_offset; + ast.shape().feed_to(self); + let end = self.current_offset; + if end > self.target_offset { + self.result + .push(AstWithRange { ast: ast.clone_ref(), range: (start..end).into() }); + } + } + other => self.current_offset += other.len(), + } + } +} + + + +// ================= +// === EditedAst === +// ================= + +/// Information about the AST node which is currently modified in the Searcher Input's expression. +#[derive(Clone, Debug, Default)] +pub struct EditedAst { + /// The edited name. The left side of the name should be used as searching pattern, and picked + /// suggestion should replace the entire name. `None` if no name is edited - in that case + /// suggestion should be inserted at the cursor position. + pub edited_name: Option, + /// Accessor chain (like `Foo.Bar.buz`) which is currently edited. + pub edited_accessor_chain: Option>, + /// The currently edited literal. + pub edited_literal: Option, +} + +impl EditedAst { + /// Create EditedAst structure basing on the result of [`AstAtPositionFinder::find`]. + fn new(stack: Vec) -> Self { + let mut stack = stack.into_iter(); + if let Some(leaf) = stack.next() { + let leaf_parent = stack.next(); + let leaf_id = leaf.ast.id; + let to_accessor_chain = |a: AstWithRange| { + let ast = ast::opr::Chain::try_new_of(&a.ast, ast::opr::predefined::ACCESS)?; + let target_id = ast.target.as_ref().map(|arg| arg.arg.id); + let leaf_is_not_target = !target_id.contains(&leaf_id); + leaf_is_not_target.then_some(AstWithRange { ast, range: a.range }) + }; + match leaf.ast.shape() { + ast::Shape::Var(_) | ast::Shape::Cons(_) => Self { + edited_name: Some(leaf), + edited_accessor_chain: leaf_parent.and_then(to_accessor_chain), + edited_literal: None, + }, + ast::Shape::Opr(_) => Self { + edited_name: None, + edited_accessor_chain: leaf_parent.and_then(to_accessor_chain), + edited_literal: None, + }, + ast::Shape::Infix(_) => Self { + edited_name: None, + edited_accessor_chain: to_accessor_chain(leaf), + edited_literal: None, + }, + ast::Shape::Number(_) => { + // Unwrapping is safe here, because we know that the shape is a number. + let ast = ast::known::Number::try_from(leaf.ast).unwrap(); + Self { edited_literal: Some(Literal::Number(ast)), ..default() } + } + ast::Shape::Tree(tree) => + if let Some(text) = tree.leaf_info.as_ref() { + let edited_literal = Literal::try_new_text(text); + Self { edited_literal, ..default() } + } else { + default() + }, + _ => default(), + } + } else { + default() + } + } +} + + + +// ================ +// === InputAst === +// ================ + +/// Searcher input content parsed to [`ast::BlockLine`] node if possible. +#[allow(missing_docs)] +#[derive(Clone, Debug)] +pub enum InputAst { + Line(ast::BlockLine), + Invalid(String), +} + +impl Default for InputAst { + fn default() -> Self { + Self::Invalid(default()) + } +} + +impl Display for InputAst { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InputAst::Line(ast) => write!(f, "{}", ast.repr()), + InputAst::Invalid(string) => write!(f, "{string}"), + } + } +} + + + +// ============= +// === Input === +// ============= + +/// The Component Browser Input. +/// +/// This structure is responsible for keeping and interpreting the searcher input - it provides +/// the information: +/// * [what part of the input is a filtering pattern](Input::pattern), and +/// * [what part should be replaced when inserting a suggestion](Input::after_inserting_suggestion). +/// +/// Both information are deduced from the _edited name_. The edited name is a concrete +/// identifier which we deduce the user is currently editing - we assume this is an identifier where +/// the text cursor is positioned (including the "end" of the identifier, like `foo|`). +#[allow(missing_docs)] +#[derive(Clone, Debug, Default)] +pub struct Input { + /// The input in the AST form. + pub ast: InputAst, + /// The current cursor position in the input. + pub cursor_position: text::Byte, + /// The edited part of the input: the name being edited and it's context. See [`EditedAst`] for + /// details. + pub edited_ast: EditedAst, +} + +impl Input { + /// Create the structure from the code already parsed to AST. + pub fn new(ast: ast::BlockLine, cursor_position: text::Byte) -> Self { + // We primarily check the character on the left of the cursor (`cursor_position_before`), + // to properly handle the situation when the cursor is at end of the edited name. + // But if there is no identifier on the left then we check the right side. + let cursor_position_before = + (cursor_position > text::Byte(0)).then(|| cursor_position - text::ByteDiff(1)); + let ast_stack_on_left = + cursor_position_before.map(|cp| AstAtPositionFinder::find(&ast.elem, cp)); + let edited_ast_on_left = ast_stack_on_left.map_or_default(EditedAst::new); + let edited_ast = if edited_ast_on_left.edited_name.is_none() { + let ast_stack_on_right = AstAtPositionFinder::find(&ast.elem, cursor_position); + let edited_ast_on_right = EditedAst::new(ast_stack_on_right); + if edited_ast_on_right.edited_name.is_some() { + edited_ast_on_right + } else { + edited_ast_on_left + } + } else { + edited_ast_on_left + }; + let ast = InputAst::Line(ast); + Self { ast, cursor_position, edited_ast } + } + + /// Create the structure parsing the string. + pub fn parse(parser: &Parser, expression: impl Str, cursor_position: text::Byte) -> Self { + match parser.parse_line(expression.as_ref()) { + Ok(ast) => Self::new(ast, cursor_position), + Err(_) => Self { + ast: InputAst::Invalid(expression.into()), + cursor_position, + edited_ast: default(), + }, + } + } + + /// Return the filtering pattern for the input. + pub fn filter(&self) -> Filter { + let pattern = if let Some(edited) = &self.edited_ast.edited_name { + let cursor_position_in_name = self.cursor_position - edited.range.start; + let name = ast::identifier::name(&edited.ast); + name.map_or_default(|name| { + let range = ..cursor_position_in_name.value as usize; + name.get(range).unwrap_or_default().into() + }) + } else { + default() + }; + let context = self.context().map(|c| c.into_ast().repr().to_im_string()); + Filter { pattern, context } + } + + /// Return the accessor chain being the context of the edited name, i.e. the preceding fully + /// qualified name parts or this argument. + pub fn context(&self) -> Option { + self.edited_ast.edited_accessor_chain.clone().map(|mut chain| { + chain.ast.args.pop(); + chain.ast + }) + } + + /// The range of the text which contains the currently edited name, if any. + pub fn edited_name_range(&self) -> Option> { + self.edited_ast.edited_name.as_ref().map(|name| name.range) + } + + /// The range of the text which contains the currently edited accessor chain, if any. + pub fn accessor_chain_range(&self) -> Option> { + self.edited_ast.edited_accessor_chain.as_ref().map(|chain| chain.range) + } + + /// The input as AST node. Returns [`None`] if the input is not a valid AST. + pub fn ast(&self) -> Option<&Ast> { + match &self.ast { + InputAst::Line(ast) => Some(&ast.elem), + InputAst::Invalid(_) => None, + } + } + + /// Currently edited literal, if any. + pub fn edited_literal(&self) -> Option<&Literal> { + self.edited_ast.edited_literal.as_ref() + } +} + + + +// ============================ +// === Inserting Suggestion === +// ============================ + +/// The description of the results of inserting suggestion. +#[derive(Clone, Debug)] +pub struct InsertedSuggestion { + /// An entire input after inserting the suggestion. + pub new_input: String, + /// The replaced range in the old input. + pub replaced: text::Range, + /// The range of the inserted code in the new input. It does not contain any additional spaces + /// (in contrary to [`inserted_text`] field. + pub inserted_code: text::Range, + /// The range of the entire inserted text in the new input. It contains code and all additional + /// spaces. + pub inserted_text: text::Range, + /// An import that needs to be added when applying the suggestion. + pub import: Option, +} + + +impl InsertedSuggestion { + /// The text change resulting from this insertion. + pub fn input_change(&self) -> text::Change { + let to_insert = self.new_input[self.inserted_text].to_owned(); + text::Change { range: self.replaced, text: to_insert } + } +} + + +// === Insert Context === + +/// A helper abstraction responsible for generating proper code for the suggestion. +/// +/// If the context contains a qualified name, we want to reuse it in the generated code sample. +/// For example, if the user types `Foo.Bar.` and accepts a method `Foo.Bar.baz` we insert +/// `Foo.Bar.baz` with `Foo` import instead of inserting `Bar.baz` with `Bar` import. +struct InsertContext<'a> { + suggestion: &'a action::Suggestion, + context: Option, + generate_this: bool, +} + +impl InsertContext<'_> { + /// Whether the context of the edited expression contains a qualified name. Qualified name is a + /// infix chain of `ACCESS` operators with identifiers. + fn has_qualified_name(&self) -> bool { + if let Some(ref context) = self.context { + let every_operand_is_name = context.enumerate_operands().flatten().all(|opr| { + matches!(opr.item.arg.shape(), ast::Shape::Cons(_) | ast::Shape::Var(_)) + }); + let every_operator_is_access = context + .enumerate_operators() + .all(|opr| opr.item.ast().repr() == ast::opr::predefined::ACCESS); + every_operand_is_name && every_operator_is_access + } else { + false + } + } + + /// All the segments of the qualified name already written by the user. + fn qualified_name_segments(&self) -> Option> { + if self.has_qualified_name() { + // Unwrap is safe here, because `has_qualified_name` would not return true in case of no + // context. + let context = self.context.as_ref().unwrap(); + let name_segments = context + .enumerate_operands() + .flatten() + .filter_map(|opr| ast::identifier::name(&opr.item.arg).map(ImString::new)) + .collect_vec(); + Some(name_segments) + } else { + None + } + } + + /// A list of name segments to replace the user input, and also a required import for the + /// final expression. The returned list of segments contains both the segments already entered + /// by the user and the segments that need to be added. + fn segments_to_replace(&self) -> Option<(Vec, Option)> { + if let Some(existing_segments) = self.qualified_name_segments() { + if let action::Suggestion::FromDatabase(entry) = self.suggestion { + let name = entry.qualified_name(); + let all_segments = name.segments().cloned().collect_vec(); + // A list of search windows is reversed, because we want to look from the end, as it + // gives the shortest possible name. + let window_size = existing_segments.len(); + let mut windows = all_segments.windows(window_size).rev(); + let window_position_from_end = windows.position(|w| w == existing_segments)?; + let pos = all_segments.len().saturating_sub(window_position_from_end + window_size); + let name_segments = all_segments.get(pos..).unwrap_or(&[]).to_vec(); + let import_segments = all_segments.get(..=pos).unwrap_or(&[]).to_vec(); + // Valid qualified name requires at least 2 segments (namespace and project name). + // Not enough segments to build a qualified name is not a mistake here – it just + // means we don't need any import, as the entry is already in scope. + let minimal_count_of_segments = 2; + let import = if import_segments.len() < minimal_count_of_segments { + None + } else if let Ok(import) = QualifiedName::from_all_segments(import_segments.clone()) + { + Some(RequiredImport::Name(import)) + } else { + error!("Invalid import formed in `segments_to_replace`: {import_segments:?}."); + None + }; + Some((name_segments, import)) + } else { + None + } + } else { + None + } + } + + fn code_to_insert(&self) -> (Cow, Option) { + match self.suggestion { + action::Suggestion::FromDatabase(entry) => { + if let Some((segments, import)) = self.segments_to_replace() { + (Cow::from(segments.iter().join(ast::opr::predefined::ACCESS)), import) + } else { + let code = entry.code_to_insert(self.generate_this); + let import = Some(RequiredImport::Entry(entry.clone_ref())); + (code, import) + } + } + action::Suggestion::Hardcoded(snippet) => (Cow::from(snippet.code.as_str()), None), + } + } +} + +// === Input === + +impl Input { + /// Return an information about input change after inserting given suggestion. + pub fn after_inserting_suggestion( + &self, + suggestion: &action::Suggestion, + has_this: bool, + ) -> FallibleResult { + let context = self.context(); + let generate_this = !has_this; + let context = InsertContext { suggestion, context, generate_this }; + let default_range = (self.cursor_position..self.cursor_position).into(); + let replaced = if context.has_qualified_name() { + self.accessor_chain_range().unwrap_or(default_range) + } else { + self.edited_name_range().unwrap_or(default_range) + }; + let (code_to_insert, import) = context.code_to_insert(); + debug!("Code to insert: \"{code_to_insert}\""); + let end_of_inserted_code = replaced.start + text::Bytes(code_to_insert.len()); + let end_of_inserted_text = end_of_inserted_code + text::Bytes(1); + let mut new_input = self.ast.to_string(); + let raw_range = replaced.start.value..replaced.end.value; + Self::ensure_on_char_boundary(&new_input, raw_range.start)?; + Self::ensure_on_char_boundary(&new_input, raw_range.end)?; + new_input.replace_range(raw_range, &code_to_insert); + new_input.insert(end_of_inserted_code.value, ' '); + Ok(InsertedSuggestion { + new_input, + replaced, + inserted_code: (replaced.start..end_of_inserted_code).into(), + inserted_text: (replaced.start..end_of_inserted_text).into(), + import, + }) + } + + /// Check if the `index` is at a char boundary inside `string` and can be used as a range + /// boundary. Otherwise, return an error. + fn ensure_on_char_boundary(string: &str, index: usize) -> FallibleResult<()> { + if !string.is_char_boundary(index) { + Err(NotACharBoundary { index, string: string.into() }.into()) + } else { + Ok(()) + } + } +} + + + +// ============ +// === Test === +// ============ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn edited_literal() { + struct Case { + input: &'static str, + expected: Option, + } + + let cases: Vec = vec![ + Case { input: "", expected: None }, + Case { + input: "'", + expected: Some(Literal::Text { + text: "''".into(), + closing_delimiter: Some("'"), + }), + }, + Case { + input: "\"\"\"", + expected: Some(Literal::Text { + text: "\"\"\"\"\"\"".into(), + closing_delimiter: Some("\"\"\""), + }), + }, + Case { + input: "\"text", + expected: Some(Literal::Text { + text: "\"text\"".into(), + closing_delimiter: Some("\""), + }), + }, + Case { + input: "\"text\"", + expected: Some(Literal::Text { + text: "\"text\"".into(), + closing_delimiter: None, + }), + }, + Case { + input: "'text", + expected: Some(Literal::Text { + text: "'text'".into(), + closing_delimiter: Some("'"), + }), + }, + Case { + input: "'text'", + expected: Some(Literal::Text { + text: "'text'".into(), + closing_delimiter: None, + }), + }, + Case { + input: "x = 'text", + expected: Some(Literal::Text { + text: "'text'".into(), + closing_delimiter: Some("'"), + }), + }, + Case { + input: "x = \"\"\"text", + expected: Some(Literal::Text { + text: "\"\"\"text\"\"\"".into(), + closing_delimiter: Some("\"\"\""), + }), + }, + ]; + let parser = Parser::new(); + + let input = Input::parse(&parser, "123", text::Byte(3)); + assert!(matches!(input.edited_literal(), Some(Literal::Number(n)) if n.repr() == "123")); + let input = Input::parse(&parser, "x = 123", text::Byte(7)); + assert!(matches!(input.edited_literal(), Some(Literal::Number(n)) if n.repr() == "123")); + + for case in cases { + let input = Input::parse(&parser, case.input, text::Byte(case.input.len())); + assert_eq!(input.edited_literal(), case.expected.as_ref()); + } + } + + #[test] + fn parsing_input() { + #[derive(Debug, Default)] + struct Case { + input: String, + cursor_position: text::Byte, + expected_accessor_chain_range: Option>, + expected_name_range: Option>, + expected_pattern: String, + } + + impl Case { + fn new( + input: impl Into, + cursor_position: impl Into, + expected_accessor_chain_range: Option>>, + expected_name_range: Option>>, + expected_pattern: impl Into, + ) -> Self { + Self { + input: input.into(), + cursor_position: cursor_position.into(), + expected_accessor_chain_range: expected_accessor_chain_range.map(Into::into), + expected_name_range: expected_name_range.map(Into::into), + expected_pattern: expected_pattern.into(), + } + } + + fn run(self, parser: &Parser) { + debug!("Running case {} cursor position {}", self.input, self.cursor_position); + let input = Input::parse(parser, self.input, self.cursor_position); + let pattern = input.filter().pattern.clone_ref(); + assert_eq!(input.cursor_position, self.cursor_position); + assert_eq!(input.edited_ast.edited_name.map(|a| a.range), self.expected_name_range); + assert_eq!( + input.edited_ast.edited_accessor_chain.map(|a| a.range), + self.expected_accessor_chain_range + ); + assert_eq!(pattern, self.expected_pattern); + } + } + + let none_range: Option> = None; + let cases = [ + Case::new("foo", 3, none_range, Some(0..3), "foo"), + Case::new("foo", 1, none_range, Some(0..3), "f"), + Case::new("foo", 0, none_range, Some(0..3), ""), + Case::new("(2 + foo) * bar", 2, none_range, none_range, ""), + Case::new("(2 + foo)*bar", 5, none_range, Some(5..8), ""), + Case::new("(2 + foo)*bar", 7, none_range, Some(5..8), "fo"), + Case::new("(2 + foo)*bar", 8, none_range, Some(5..8), "foo"), + Case::new("(2 + foo)*bar", 9, none_range, none_range, ""), + Case::new("(2 + foo)*b", 10, none_range, Some(10..11), ""), + Case::new("(2 + foo)*b", 11, none_range, Some(10..11), "b"), + Case::new("Standard.Base.foo", 0, none_range, Some(0..8), ""), + Case::new("Standard.Base.foo", 4, none_range, Some(0..8), "Stan"), + Case::new("Standard.Base.foo", 8, none_range, Some(0..8), "Standard"), + Case::new("Standard.Base.foo", 9, Some(0..13), Some(9..13), ""), + Case::new("Standard.Base.foo", 11, Some(0..13), Some(9..13), "Ba"), + Case::new("Standard.Base.foo", 13, Some(0..13), Some(9..13), "Base"), + Case::new("Standard.Base.foo", 14, Some(0..17), Some(14..17), ""), + Case::new("Standard.Base.foo", 15, Some(0..17), Some(14..17), "f"), + Case::new("Standard.Base.foo", 17, Some(0..17), Some(14..17), "foo"), + Case::new("Standard . Base . foo", 17, Some(0..21), none_range, ""), + Case::new("Standard . Base.foo", 16, Some(11..19), Some(16..19), ""), + Case::new("Standard.Base.", 14, Some(0..14), none_range, ""), + Case::new("(2 + Standard.Base.foo)*b", 21, Some(5..22), Some(19..22), "fo"), + Case::new("(2 + Standard.Base.)*b", 19, Some(5..19), none_range, ""), + ]; + + let parser = Parser::new(); + for case in cases { + case.run(&parser); + } + } +} diff --git a/app/gui/src/model/module.rs b/app/gui/src/model/module.rs index 6b98bd17937..144b55be03b 100644 --- a/app/gui/src/model/module.rs +++ b/app/gui/src/model/module.rs @@ -392,9 +392,7 @@ pub enum NodeEditStatus { /// The node was edited and had a previous expression. Edited { /// Expression of the node before the edit was started. - previous_expression: String, - /// Intended method of the node before editing (if known). - previous_intended_method: Option, + previous_expression: String, }, /// The node was created and did not previously exist. Created, @@ -411,6 +409,9 @@ pub struct NodeMetadata { /// /// The methods may be defined for different types, so the name alone don't specify them. #[serde(default, deserialize_with = "enso_prelude::deserialize_or_default")] + #[deprecated( + note = "No longer used anywhere, but is kept for compatibility with old projects" + )] pub intended_method: Option, /// Information about uploading file. /// @@ -803,7 +804,6 @@ main = 5 let id = ast::Id::from_str("1d6660c6-a70b-4eeb-b5f7-82f05a51df25").unwrap(); let node = file.metadata.ide.node.get(&id).unwrap(); assert_eq!(node.position, Some(Position::new(-75.5, 52.0))); - assert_eq!(node.intended_method, None); assert_eq!(file.metadata.rest, serde_json::Value::Object(default())); } } diff --git a/app/gui/src/model/module/plain.rs b/app/gui/src/model/module/plain.rs index 5d443329743..4bc74485bb2 100644 --- a/app/gui/src/model/module/plain.rs +++ b/app/gui/src/model/module/plain.rs @@ -323,13 +323,12 @@ fn restore_edited_node_in_graph( graph.remove_node(node_id)?; md_entry.remove(); } - Some(NodeEditStatus::Edited { previous_expression, previous_intended_method }) => { + Some(NodeEditStatus::Edited { previous_expression }) => { debug!( "Restoring edited node {node_id} to original expression \ \"{previous_expression}\"." ); graph.edit_node(node_id, Parser::new().parse_line_ast(previous_expression)?)?; - md_entry.get_mut().intended_method = previous_intended_method; } None => {} } diff --git a/app/gui/src/model/project/synchronized.rs b/app/gui/src/model/project/synchronized.rs index 9e060ed9e9e..74408a2d198 100644 --- a/app/gui/src/model/project/synchronized.rs +++ b/app/gui/src/model/project/synchronized.rs @@ -315,7 +315,7 @@ impl Project { let content_roots = ContentRoots::new_from_connection(language_server); let content_roots = Rc::new(content_roots); let notifications = notification::Publisher::default(); - let urm = Rc::new(model::undo_redo::Manager::new()); + let urm = default(); let properties = Rc::new(RefCell::new(properties)); let ret = Project { diff --git a/app/gui/src/model/undo_redo.rs b/app/gui/src/model/undo_redo.rs index f4be92bd37d..7751e949639 100644 --- a/app/gui/src/model/undo_redo.rs +++ b/app/gui/src/model/undo_redo.rs @@ -343,7 +343,6 @@ impl Repository { /// Owns [`Repository`] and keeps track of open modules. #[derive(Debug, Default)] pub struct Manager { - #[allow(missing_docs)] /// Repository with undo and redo stacks. pub repository: Rc, /// Currently available modules. @@ -535,7 +534,8 @@ main = assert_eq!(sum_node.expression().to_string(), "2 + 2"); assert_eq!(product_node.expression().to_string(), "5 * 5"); - let sum_tree = SpanTree::<()>::new(&sum_node.expression(), graph).unwrap(); + let context = &span_tree::generate::context::Empty; + let sum_tree = SpanTree::<()>::new(&sum_node.expression(), context).unwrap(); let sum_input = sum_tree.root_ref().leaf_iter().find(|n| n.is_argument()).unwrap().crumbs; let connection = controller::graph::Connection { @@ -543,7 +543,7 @@ main = destination: controller::graph::Endpoint::new(sum_node.id(), sum_input), }; - graph.connect(&connection, &span_tree::generate::context::Empty).unwrap(); + graph.connect(&connection, context).unwrap(); }); } diff --git a/app/gui/src/presenter/searcher.rs b/app/gui/src/presenter/searcher.rs index d837ca99b60..1cf9a283421 100644 --- a/app/gui/src/presenter/searcher.rs +++ b/app/gui/src/presenter/searcher.rs @@ -19,11 +19,11 @@ use crate::presenter::searcher::provider::ControllerComponentsProviderExt; use enso_frp as frp; use enso_suggestion_database::documentation_ir::EntryDocumentation; use enso_suggestion_database::documentation_ir::Placeholder; +use enso_text as text; use ide_view as view; use ide_view::component_browser::component_list_panel::grid as component_grid; use ide_view::component_browser::component_list_panel::BreadcrumbId; use ide_view::component_browser::component_list_panel::SECTION_NAME_CRUMB_INDEX; -use ide_view::graph_editor::component::node as node_view; use ide_view::graph_editor::GraphEditor; use ide_view::project::SearcherParams; @@ -96,8 +96,8 @@ impl Model { } #[profile(Debug)] - fn input_changed(&self, new_input: &str) { - if let Err(err) = self.controller.set_input(new_input.to_owned()) { + fn input_changed(&self, new_input: &str, cursor_position: text::Byte) { + if let Err(err) = self.controller.set_input(new_input.to_owned(), cursor_position) { error!("Error while setting new searcher input: {err}."); } } @@ -143,7 +143,7 @@ impl Model { fn suggestion_accepted( &self, id: component_grid::GroupEntryId, - ) -> Option<(ViewNodeId, node_view::Expression)> { + ) -> Option<(ViewNodeId, text::Range, ImString)> { let provider = self.controller.provider(); let component: FallibleResult<_> = provider.component_by_view_id(id).ok_or_else(|| NoSuchComponent(id).into()); @@ -156,10 +156,9 @@ impl Model { self.controller.use_suggestion(suggestion) }); match new_code { - Ok(new_code) => { + Ok(text::Change { range, text }) => { self.update_breadcrumbs(); - let new_code_and_trees = node_view::Expression::new_plain(new_code); - Some((self.input_view, new_code_and_trees)) + Some((self.input_view, range, text.into())) } Err(err) => { error!("Error while applying suggestion: {err}."); @@ -248,7 +247,7 @@ impl Model { component::Data::FromDatabase { id, .. } => self.controller.documentation_for_entry(*id), component::Data::Virtual { snippet } => - snippet.documentation.as_ref().map_or_default(EntryDocumentation::builtin), + snippet.documentation.clone().unwrap_or_default(), } } else { default() @@ -292,9 +291,13 @@ impl Searcher { let network = frp::Network::new("presenter::Searcher"); let graph = &model.view.graph().frp; + let browser = model.view.searcher(); frp::extend! { network - eval model.view.searcher_input_changed ((expr) model.input_changed(expr)); + eval model.view.searcher_input_changed ([model]((expr, selections)) { + let cursor_position = selections.last().map(|sel| sel.end).unwrap_or_default(); + model.input_changed(expr, cursor_position); + }); action_list_changed <- source::<()>(); @@ -302,7 +305,6 @@ impl Searcher { model.controller.reload_list()); } - let browser = model.view.searcher(); let grid = &browser.model().list.model().grid; let navigator = &browser.model().list.model().section_navigator; let breadcrumbs = &browser.model().list.model().breadcrumbs; @@ -316,8 +318,8 @@ impl Searcher { let provider = provider::Component::provide_new_list(controller_provider, &grid); *model.provider.borrow_mut() = Some(provider); }); - new_input <- grid.suggestion_accepted.filter_map(f!((e) model.suggestion_accepted(*e))); - graph.set_node_expression <+ new_input; + input_edit <- grid.suggestion_accepted.filter_map(f!((e) model.suggestion_accepted(*e))); + graph.edit_node_expression <+ input_edit; entry_selected <- grid.active.filter_map(|&s| s?.as_entry_id()); selected_entry_changed <- entry_selected.on_change().constant(()); @@ -380,7 +382,7 @@ impl Searcher { /// The expression to be used for newly created nodes when initialising the searcher without /// an existing node. const DEFAULT_INPUT_EXPRESSION: &str = "Nothing"; - let SearcherParams { input, source_node } = parameters; + let SearcherParams { input, source_node, .. } = parameters; let view_data = graph_editor.model.nodes.get_cloned_ref(&input); @@ -443,6 +445,10 @@ impl Searcher { view: view::project::View, parameters: SearcherParams, ) -> FallibleResult { + // We get the position for searcher before initializing the input node, because the + // added node will affect the AST, and the position will become incorrect. + let position_in_code = graph_controller.graph().definition_end_location()?; + let mode = Self::init_input_node( parameters, graph_presenter, @@ -455,13 +461,15 @@ impl Searcher { &project_controller.model, graph_controller, mode, + parameters.cursor_position, + position_in_code, )?; // Clear input on a new node. By default this will be set to whatever is used as the default // content of the new node. if let Mode::NewNode { source_node, .. } = mode { if source_node.is_none() { - if let Err(e) = searcher_controller.set_input("".to_string()) { + if let Err(e) = searcher_controller.set_input("".to_string(), text::Byte(0)) { error!("Failed to clear input when creating searcher for a new node: {e:?}."); } } diff --git a/app/gui/src/presenter/searcher/provider.rs b/app/gui/src/presenter/searcher/provider.rs index ba70c156cae..bf5ea8ab628 100644 --- a/app/gui/src/presenter/searcher/provider.rs +++ b/app/gui/src/presenter/searcher/provider.rs @@ -33,9 +33,7 @@ pub fn create_providers_from_controller(controller: &controller::Searcher) -> An match controller.actions() { Actions::Loading => as_any(Rc::new(list_view::entry::EmptyProvider)), Actions::Loaded { list } => { - let user_action = controller.current_user_action(); - let intended_function = controller.intended_function_suggestion(); - let provider = Action { actions: list, user_action, intended_function }; + let provider = Action { actions: list }; as_any(Rc::new(provider)) } Actions::Error(err) => { @@ -61,9 +59,7 @@ where P: list_view::entry::ModelProvider /// An searcher actions provider, based on the action list retrieved from the searcher controller. #[derive(Clone, Debug)] pub struct Action { - actions: Rc, - user_action: controller::searcher::UserAction, - intended_function: Option, + actions: Rc, } impl Action { @@ -126,11 +122,7 @@ impl list_view::entry::ModelProvider for Action { impl ide_view::searcher::DocumentationProvider for Action { fn get(&self) -> Option { - use controller::searcher::UserAction::*; - self.intended_function.as_ref().and_then(|function| match self.user_action { - StartingTypingArgument => function.documentation_html().map(ToOwned::to_owned), - _ => None, - }) + None } fn get_for_entry(&self, id: usize) -> Option { diff --git a/app/gui/src/test.rs b/app/gui/src/test.rs index 8eae31e04e4..9fee40d904c 100644 --- a/app/gui/src/test.rs +++ b/app/gui/src/test.rs @@ -210,12 +210,12 @@ pub mod mock { &self, module: model::Module, db: Rc, - ) -> crate::controller::Graph { + ) -> controller::Graph { let parser = self.parser.clone_ref(); let method = self.method_pointer(); let definition = module.lookup_method(self.project_name.clone(), &method).expect("Lookup failed."); - crate::controller::Graph::new(module, db, parser, definition) + controller::Graph::new(module, db, parser, definition) .expect("Graph could not be created") } @@ -291,11 +291,14 @@ pub mod mock { let data = self.clone(); let searcher_target = executed_graph.graph().nodes().unwrap().last().unwrap().id(); let searcher_mode = controller::searcher::Mode::EditNode { node_id: searcher_target }; + let position_in_code = executed_graph.graph().definition_end_location().unwrap(); let searcher = controller::Searcher::new_from_graph_controller( ide.clone_ref(), &project, executed_graph.clone_ref(), searcher_mode, + enso_text::Byte(0), + position_in_code, ) .unwrap(); executor.run_until_stalled(); diff --git a/app/gui/src/tests.rs b/app/gui/src/tests.rs index 96c2f8d3716..8dcf7ab9193 100644 --- a/app/gui/src/tests.rs +++ b/app/gui/src/tests.rs @@ -1,6 +1,5 @@ use super::prelude::*; -use crate::controller::graph::NodeTrees; use crate::ide; use crate::transport::test_utils::TestWithMockedTransport; @@ -50,112 +49,3 @@ fn failure_to_open_project_is_reported() { // Further investigation needed, tracked by https://github.com/enso-org/ide/issues/1575 fixture.run_until_stalled(); } - - -// ==================================== -// === SpanTree in Graph Controller === -// ==================================== - -#[wasm_bindgen_test] -fn span_tree_args() { - use crate::test::mock::*; - use span_tree::Node; - - let data = Unified::new(); - let fixture = data.fixture_customize(|_, json_client, _| { - // The searcher requests for completion when we clear the input. - controller::searcher::test::expect_completion(json_client, &[1]); - // Additional completion request happens after picking completion. - controller::searcher::test::expect_completion(json_client, &[1]); - }); - let Fixture { graph, executed_graph, searcher, suggestion_db, .. } = &fixture; - let entry = suggestion_db.lookup(1).unwrap(); - - searcher.set_input("".into()).unwrap(); - searcher - .use_suggestion(controller::searcher::action::Suggestion::FromDatabase(entry.clone_ref())) - .unwrap(); - - let id = searcher.commit_node().unwrap(); - - let get_node = || graph.node(id).unwrap(); - let get_inputs = || NodeTrees::new(&get_node().info, executed_graph).unwrap().inputs; - let get_param = |n| { - let inputs = get_inputs(); - let mut args = inputs.root_ref().leaf_iter().filter(|n| n.is_function_parameter()); - args.nth(n).and_then(|node| node.argument_info()) - }; - - let parser = executed_graph.parser(); - let mut invocation_info = entry.invocation_info(suggestion_db, &parser); - let expected_this_param = invocation_info.parameters.remove(0).with_call_id(Some(id)); - let expected_arg1_param = invocation_info.parameters.remove(0).with_call_id(Some(id)); - - // === Method notation, without prefix application === - assert_eq!(get_node().info.expression().repr(), "Base.foo"); - match get_inputs().root.children.as_slice() { - // The tree here should have two nodes under root - one with given Ast and second for - // an additional prefix application argument. - [_, second] => { - let Node { children, kind, .. } = &second.node; - assert!(children.is_empty()); - assert_eq!(kind.argument_info().as_ref(), Some(&expected_arg1_param)); - } - _ => panic!("Expected only two children in the span tree's root"), - }; - - - // === Method notation, with prefix application === - graph.set_expression(id, "Base.foo 50").unwrap(); - match get_inputs().root.children.as_slice() { - // The tree here should have two nodes under root - one with given Ast and second for - // an additional prefix application argument. - [_target, argument] => { - let Node { children, kind, .. } = &argument.node; - assert!(children.is_empty()); - assert_eq!(kind.argument_info().as_ref(), Some(&expected_arg1_param)); - } - inputs => - panic!("Expected two children in the span tree's root but got {:?}", inputs.len()), - }; - - - // === Function notation, without prefix application === - assert_eq!(entry.name, "foo"); - graph.set_expression(id, "foo").unwrap(); - assert_eq!(get_param(0).as_ref(), Some(&expected_this_param)); - assert_eq!(get_param(1).as_ref(), Some(&expected_arg1_param)); - assert_eq!(get_param(2).as_ref(), None); - - - // === Function notation, with prefix application === - graph.set_expression(id, "foo Base").unwrap(); - assert_eq!(get_param(0).as_ref(), Some(&expected_this_param)); - assert_eq!(get_param(1).as_ref(), Some(&expected_arg1_param)); - assert_eq!(get_param(2).as_ref(), None); - - - // === Changed function name, should not have known parameters === - graph.set_expression(id, "bar").unwrap(); - assert_eq!(get_param(0), None); - assert_eq!(get_param(1), None); - assert_eq!(get_param(2), None); - - graph.set_expression(id, "bar Base").unwrap(); - assert_eq!(get_param(0), Some(default())); - assert_eq!(get_param(1), None); - assert_eq!(get_param(2), None); - - graph.set_expression(id, "Base.bar").unwrap(); - assert_eq!(get_param(0), Some(default())); - assert_eq!(get_param(1), Some(default())); - assert_eq!(get_param(2), None); - - // === Oversaturated call === - graph.set_expression(id, "foo Base 10 20 30").unwrap(); - assert_eq!(get_param(0).as_ref(), Some(&expected_this_param)); - assert_eq!(get_param(1).as_ref(), Some(&expected_arg1_param)); - assert_eq!(get_param(2).as_ref(), Some(&default())); - assert_eq!(get_param(3).as_ref(), Some(&default())); - assert_eq!(get_param(4).as_ref(), None); -} diff --git a/app/gui/suggestion-database/src/entry.rs b/app/gui/suggestion-database/src/entry.rs index a31fdf0a98a..39d3a4b52fa 100644 --- a/app/gui/suggestion-database/src/entry.rs +++ b/app/gui/suggestion-database/src/entry.rs @@ -452,7 +452,13 @@ impl Entry { let is_extension_method = self_type_module.map_or(false, |stm| *stm != self.defined_in); let extension_method_import = is_extension_method.and_option_from(|| { - self.defined_in_entry(db).map(|e| e.required_imports(db, in_module.clone_ref())) + self.defined_in_entry(db).map(|e| { + if e.kind == Kind::Module && e.defined_in == in_module { + default() + } else { + e.required_imports(db, in_module.clone_ref()) + } + }) }); let self_type_import = self .is_static @@ -469,7 +475,7 @@ impl Entry { .into_iter() .flatten() .collect(), - Kind::Module | Kind::Type if defined_in_same_module => default(), + Kind::Type if defined_in_same_module => default(), Kind::Module => { let import = if let Some(reexport) = &self.reexported_in { Import::Unqualified { @@ -1125,8 +1131,12 @@ mod test { } expect_imports(&static_method, ¤t_modules[0], &[]); - expect_imports(&module_method, ¤t_modules[0], &[]); - expect_imports(&submodule, ¤t_modules[0], &[]); + // The following asserts are disabled, because we actually add import for the module even if + // we're inside it. It is necessary, because otherwise `Project.foo` expressions + // currently do not work without importing `local.Project` into the scope. + // See https://github.com/enso-org/enso/issues/5616. + // expect_imports(&module_method, ¤t_modules[0], &[]); + // expect_imports(&submodule, ¤t_modules[0], &[]); expect_imports(&extension_method, ¤t_modules[0], &[ "from Standard.Base import Number", ]); diff --git a/app/gui/view/examples/icons/src/lib.rs b/app/gui/view/examples/icons/src/lib.rs index 9654cf694d6..a570e7f0bee 100644 --- a/app/gui/view/examples/icons/src/lib.rs +++ b/app/gui/view/examples/icons/src/lib.rs @@ -21,6 +21,7 @@ use ide_view_component_list_panel_icons::SIZE; use ide_view_graph_editor::component::node::action_bar; + // ============= // === Frame === // ============= diff --git a/app/gui/view/graph-editor/src/component/node.rs b/app/gui/view/graph-editor/src/component/node.rs index 659fbe9d011..34daa83aa92 100644 --- a/app/gui/view/graph-editor/src/component/node.rs +++ b/app/gui/view/graph-editor/src/component/node.rs @@ -299,6 +299,7 @@ ensogl::define_endpoints_2! { set_disabled (bool), set_input_connected (span_tree::Crumbs,Option,bool), set_expression (Expression), + edit_expression (text::Range, ImString), set_skip_macro (bool), set_freeze_macro (bool), set_comment (Comment), @@ -750,6 +751,7 @@ impl Node { ); eval filtered_usage_type (((a,b)) model.set_expression_usage_type(a,b)); eval input.set_expression ((a) model.set_expression(a)); + model.input.edit_expression <+ input.edit_expression; out.expression <+ model.input.frp.expression; out.expression_span <+ model.input.frp.on_port_code_update; out.requested_widgets <+ model.input.frp.requested_widgets; diff --git a/app/gui/view/graph-editor/src/component/node/action_bar.rs b/app/gui/view/graph-editor/src/component/node/action_bar.rs index 913fd5b475c..1a900a5a7ca 100644 --- a/app/gui/view/graph-editor/src/component/node/action_bar.rs +++ b/app/gui/view/graph-editor/src/component/node/action_bar.rs @@ -1,22 +1,25 @@ //! Definition of the `ActionBar` component for the `visualization::Container`. - - -pub mod icon; - use crate::prelude::*; +use ensogl::display::shape::*; use enso_config::ARGS; use enso_frp as frp; use ensogl::application::Application; use ensogl::display; -use ensogl::display::shape::*; use ensogl_component::toggle_button; use ensogl_component::toggle_button::ColorableShape; use ensogl_component::toggle_button::ToggleButton; use ensogl_hardcoded_theme as theme; +// ============== +// === Export === +// ============== + +pub mod icon; + + // ================== // === Constants === diff --git a/app/gui/view/graph-editor/src/component/node/input/area.rs b/app/gui/view/graph-editor/src/component/node/input/area.rs index 37fbcf2066f..0131cac65e2 100644 --- a/app/gui/view/graph-editor/src/component/node/input/area.rs +++ b/app/gui/view/graph-editor/src/component/node/input/area.rs @@ -23,6 +23,8 @@ use ensogl::display; use ensogl::gui::cursor; use ensogl::Animation; use ensogl_component::text; +use ensogl_component::text::buffer::selection::Selection; +use ensogl_component::text::FromInContextSnapped; use ensogl_hardcoded_theme as theme; @@ -852,6 +854,12 @@ ensogl::define_endpoints! { /// Set the node expression. set_expression (node::Expression), + /// Edit the node expression: if the node is currently edited, the given range will be + /// replaced with the string, and the text cursor will be placed after the inserted string. + /// + /// If the node is **not** edited, nothing changes. + edit_expression (text::Range, ImString), + /// Set the mode in which the cursor will indicate that editing of the node is possible. set_edit_ready_mode (bool), @@ -893,6 +901,8 @@ ensogl::define_endpoints! { pointer_style (cursor::Style), width (f32), expression (ImString), + expression_edit (ImString, Vec>), + editing (bool), ports_visible (bool), body_hover (bool), @@ -1017,9 +1027,21 @@ impl Area { model.set_expression(expr, *is_editing, &frp_endpoints) ) ); + legit_edit <- frp.input.edit_expression.gate(&frp.input.set_editing); + model.label.select <+ legit_edit.map(|(range, _)| (range.start.into(), range.end.into())); + model.label.insert <+ legit_edit._1(); frp.output.source.expression <+ expression.map(|e| e.code.clone_ref()); expression_changed_by_user <- model.label.content.gate(&frp.input.set_editing); frp.output.source.expression <+ expression_changed_by_user.ref_into(); + frp.output.source.expression_edit <+ model.label.selections.map2( + &expression_changed_by_user, + f!([model](selection, full_content) { + let full_content = full_content.into(); + let to_byte = |loc| text::Byte::from_in_context_snapped(&model.label, loc); + let selections = selection.iter().map(|sel| sel.map(to_byte)).collect_vec(); + (full_content, selections) + }) + ); // === Expression Type === diff --git a/app/gui/view/graph-editor/src/lib.rs b/app/gui/view/graph-editor/src/lib.rs index 1668d58d8bf..ccd6ff67121 100644 --- a/app/gui/view/graph-editor/src/lib.rs +++ b/app/gui/view/graph-editor/src/lib.rs @@ -72,6 +72,8 @@ use ensogl::system::web::traits::*; use ensogl::Animation; use ensogl::DEPRECATED_Animation; use ensogl::Easing; +use ensogl_component::text; +use ensogl_component::text::buffer::selection::Selection; use ensogl_component::tooltip::Tooltip; use ensogl_hardcoded_theme as theme; @@ -590,13 +592,13 @@ ensogl::define_endpoints_2! { // === VCS Status === - set_node_vcs_status ((NodeId,Option)), + set_node_vcs_status ((NodeId, Option)), set_detached_edge_targets (EdgeEndpoint), set_detached_edge_sources (EdgeEndpoint), - set_edge_source ((EdgeId,EdgeEndpoint)), - set_edge_target ((EdgeId,EdgeEndpoint)), + set_edge_source ((EdgeId, EdgeEndpoint)), + set_edge_target ((EdgeId, EdgeEndpoint)), unset_edge_source (EdgeId), unset_edge_target (EdgeId), connect_nodes ((EdgeEndpoint,EdgeEndpoint)), @@ -611,6 +613,7 @@ ensogl::define_endpoints_2! { edit_node (NodeId), collapse_nodes ((Vec,NodeId)), set_node_expression ((NodeId,node::Expression)), + edit_node_expression ((NodeId, text::Range, ImString)), set_node_skip ((NodeId,bool)), set_node_freeze ((NodeId,bool)), set_node_comment ((NodeId,node::Comment)), @@ -618,10 +621,10 @@ ensogl::define_endpoints_2! { set_expression_usage_type ((NodeId,ast::Id,Option)), update_node_widgets ((NodeId,WidgetUpdates)), cycle_visualization (NodeId), - set_visualization ((NodeId,Option)), + set_visualization ((NodeId, Option)), register_visualization (Option), - set_visualization_data ((NodeId,visualization::Data)), - set_error_visualization_data ((NodeId,visualization::Data)), + set_visualization_data ((NodeId, visualization::Data)), + set_error_visualization_data ((NodeId, visualization::Data)), enable_visualization (NodeId), disable_visualization (NodeId), @@ -693,11 +696,12 @@ ensogl::define_endpoints_2! { node_hovered (Option>), node_selected (NodeId), node_deselected (NodeId), - node_position_set ((NodeId, Vector2)), - node_position_set_batched ((NodeId, Vector2)), - node_expression_set ((NodeId, ImString)), + node_position_set ((NodeId,Vector2)), + node_position_set_batched ((NodeId,Vector2)), + node_expression_set ((NodeId,ImString)), node_expression_span_set ((NodeId, span_tree::Crumbs, ImString)), - node_comment_set ((NodeId, String)), + node_expression_edited ((NodeId,ImString,Vec>)), + node_comment_set ((NodeId,String)), node_entered (NodeId), node_exited (), node_editing_started (NodeId), @@ -1588,6 +1592,7 @@ impl GraphEditorModelWithNetwork { )); eval node.expression((t) model.frp.private.output.node_expression_set.emit((node_id,t.into()))); + eval node.expression_span([model]((crumbs,code)) { let args = (node_id, crumbs.clone(), code.clone()); model.frp.private.output.node_expression_span_set.emit(args) @@ -1598,6 +1603,10 @@ impl GraphEditorModelWithNetwork { model.frp.private.output.widgets_requested.emit(args) }); + let node_expression_edit = node.model().input.expression_edit.clone_ref(); + model.frp.private.output.node_expression_edited <+ node_expression_edit.map( + move |(expr, selection)| (node_id, expr.clone_ref(), selection.clone()) + ); model.frp.private.output.request_import <+ node.request_import; @@ -1947,6 +1956,20 @@ impl GraphEditorModel { } } + fn edit_node_expression( + &self, + node_id: impl Into, + range: impl Into>, + inserted_str: impl Into, + ) { + let node_id = node_id.into(); + let range = range.into(); + let inserted_str = inserted_str.into(); + if let Some(node) = self.nodes.get_cloned_ref(&node_id) { + node.edit_expression(range, inserted_str); + } + } + fn set_node_skip(&self, node_id: impl Into, skip: &bool) { let node_id = node_id.into(); if let Some(node) = self.nodes.get_cloned_ref(&node_id) { @@ -2809,8 +2832,6 @@ fn new_graph_editor(app: &Application) -> GraphEditor { let node_input_touch = TouchNetwork::::new(network,mouse); let node_output_touch = TouchNetwork::::new(network,mouse); - node_expression_set <- source(); - out.node_expression_set <+ node_expression_set; on_output_connect_drag_mode <- node_output_touch.down.constant(true); on_output_connect_follow_mode <- node_output_touch.selected.constant(false); @@ -3147,15 +3168,6 @@ fn new_graph_editor(app: &Application) -> GraphEditor { } - // === Set Node Expression === - frp::extend! { network - - set_node_expression_string <- inputs.set_node_expression.map(|(id,expr)| (*id,expr.code.clone())); - out.node_expression_set <+ set_node_expression_string; - - } - - // === Set Node SKIP and FREEZE macros === frp::extend! { network @@ -3642,8 +3654,9 @@ fn new_graph_editor(app: &Application) -> GraphEditor { model.profiling_statuses.remove <+ out.node_removed; out.on_visualization_select <+ out.node_removed.map(|&id| Switch::Off(id)); - eval inputs.set_node_expression (((id,expr)) model.set_node_expression(id,expr)); - port_to_refresh <= inputs.set_node_expression.map(f!(((id,_))model.node_in_edges(id))); + eval inputs.set_node_expression (((id, expr)) model.set_node_expression(id, expr)); + eval inputs.edit_node_expression (((id, range, ins)) model.edit_node_expression(id, range, ins)); + port_to_refresh <= inputs.set_node_expression.map(f!(((id, _))model.node_in_edges(id))); eval port_to_refresh ((id) model.set_edge_target_connection_status(*id,true)); // === Remove implementation === diff --git a/app/gui/view/src/project.rs b/app/gui/view/src/project.rs index 319f8cf5e8d..a50a208a8da 100644 --- a/app/gui/view/src/project.rs +++ b/app/gui/view/src/project.rs @@ -25,6 +25,8 @@ use ensogl::display; use ensogl::system::web; use ensogl::system::web::dom; use ensogl::DEPRECATED_Animation; +use ensogl_component::text; +use ensogl_component::text::selection::Selection; use ensogl_hardcoded_theme::Theme; use ide_view_graph_editor::NodeSource; @@ -49,19 +51,21 @@ const INPUT_CHANGE_DELAY_MS: i32 = 200; #[derive(Clone, Copy, Debug, Default)] pub struct SearcherParams { /// The node being an Expression Input. - pub input: NodeId, + pub input: NodeId, /// The node being a source for the edited node data - usually it's output shall be a `this` /// port for inserted expression. - pub source_node: Option, + pub source_node: Option, + /// A position of the cursor in the input node. + pub cursor_position: text::Byte, } impl SearcherParams { fn new_for_new_node(node_id: NodeId, source_node: Option) -> Self { - Self { input: node_id, source_node } + Self { input: node_id, source_node, cursor_position: default() } } - fn new_for_edited_node(node_id: NodeId) -> Self { - Self { input: node_id, source_node: None } + fn new_for_edited_node(node_id: NodeId, cursor_position: text::Byte) -> Self { + Self { input: node_id, source_node: None, cursor_position } } } @@ -107,7 +111,7 @@ ensogl::define_endpoints! { /// Is **not** emitted with every graph's node expression change, only when /// [`INPUT_CHANGE_DELAY_MS`] passes since last change, so we won't needlessly update the /// Component Browser when user is quickly typing in the input. - searcher_input_changed (ImString), + searcher_input_changed (ImString, Vec>), is_searcher_opened (bool), adding_new_node (bool), old_expression_of_edited_node (Expression), @@ -367,6 +371,7 @@ impl View { // TODO[WD]: This should not be needed after the theme switching issue is implemented. // See: https://github.com/enso-org/ide/issues/795 let input_change_delay = frp::io::timer::Timeout::new(network); + let searcher_open_delay = frp::io::timer::Timeout::new(network); if let Some(window_control_buttons) = &*model.window_control_buttons { let initial_size = &window_control_buttons.size.value(); @@ -456,20 +461,47 @@ impl View { // === Editing === - existing_node_edited <- graph.node_being_edited.filter_map(|x| *x).gate_not(&frp.adding_new_node); - frp.source.searcher <+ existing_node_edited.map( - |&node| Some(SearcherParams::new_for_edited_node(node)) - ); - searcher_input_change_opt <- graph.node_expression_set.map2(&frp.searcher, |(node_id, expr), searcher| { - (searcher.as_ref()?.input == *node_id).then(|| expr.clone_ref()) + node_edited_by_user <- graph.node_being_edited.gate_not(&frp.adding_new_node); + existing_node_edited <- graph.node_expression_edited.gate_not(&frp.is_searcher_opened); + open_searcher <- existing_node_edited.map2(&node_edited_by_user, + |(id, _, _), edited| edited.map_or(false, |edited| *id == edited) + ).on_true(); + searcher_open_delay.restart <+ open_searcher.constant(0); + cursor_position <- existing_node_edited.map2( + &node_edited_by_user, + |(node_id, _, selections), edited| { + edited.map_or(None, |edited| { + let position = || selections.last().map(|sel| sel.end).unwrap_or_default(); + (*node_id == edited).then(position) + }) + } + ).filter_map(|pos| *pos); + edited_node <- node_edited_by_user.filter_map(|node| *node); + position_and_edited_node <- cursor_position.map2(&edited_node, |pos, id| (*pos, *id)); + prepare_params <- position_and_edited_node.sample(&searcher_open_delay.on_expired); + frp.source.searcher <+ prepare_params.map(|(pos, node_id)| { + Some(SearcherParams::new_for_edited_node(*node_id, *pos)) }); - searcher_input_change <- searcher_input_change_opt.filter_map(|expr| expr.clone()); + searcher_input_change_opt <- graph.node_expression_edited.map2(&frp.searcher, + |(node_id, expr, selections), searcher| { + let input_change = || (*node_id, expr.clone_ref(), selections.clone()); + (searcher.as_ref()?.input == *node_id).then(input_change) + } + ); + searcher_input_change <- searcher_input_change_opt.filter_map(|change| change.clone()); input_change_delay.restart <+ searcher_input_change.constant(INPUT_CHANGE_DELAY_MS); update_searcher_input_on_commit <- frp.output.editing_committed.constant(()); input_change_delay.cancel <+ update_searcher_input_on_commit; update_searcher_input <- any(&input_change_delay.on_expired, &update_searcher_input_on_commit); - frp.source.searcher_input_changed <+ searcher_input_change.sample(&update_searcher_input); - + input_change_and_searcher <- map2(&searcher_input_change, &frp.searcher, + |c, s| (c.clone(), *s) + ); + updated_input <- input_change_and_searcher.sample(&update_searcher_input); + input_changed <- updated_input.filter_map(|((node_id, expr, selections), searcher)| { + let input_change = || (expr.clone_ref(), selections.clone()); + (searcher.as_ref()?.input == *node_id).then(input_change) + }); + frp.source.searcher_input_changed <+ input_changed; // === Adding Node === diff --git a/lib/rust/ensogl/component/text/src/buffer.rs b/lib/rust/ensogl/component/text/src/buffer.rs index 368b80167a8..30d133bdc80 100644 --- a/lib/rust/ensogl/component/text/src/buffer.rs +++ b/lib/rust/ensogl/component/text/src/buffer.rs @@ -130,6 +130,7 @@ ensogl_core::define_endpoints! { cursors_select (Option), set_cursor (Location), add_cursor (Location), + set_single_selection (selection::Shape), set_newest_selection_end (Location), set_oldest_selection_end (Location), insert (ImString), @@ -220,6 +221,9 @@ impl Buffer { sel_on_set_cursor <- input.set_cursor.map(f!((t) m.set_cursor(*t))); sel_on_add_cursor <- input.add_cursor.map(f!((t) m.add_cursor(*t))); + sel_on_set_single_selection <- input.set_single_selection.map( + f!((t) m.set_single_selection(*t)) + ); sel_on_set_newest_end <- input.set_newest_selection_end.map (f!((t) m.set_newest_selection_end(*t))); sel_on_set_oldest_end <- input.set_oldest_selection_end.map @@ -247,6 +251,7 @@ impl Buffer { output.source.selection_non_edit_mode <+ sel_on_keep_oldest_cursor; output.source.selection_non_edit_mode <+ sel_on_set_cursor; output.source.selection_non_edit_mode <+ sel_on_add_cursor; + output.source.selection_non_edit_mode <+ sel_on_set_single_selection; output.source.selection_non_edit_mode <+ sel_on_set_newest_end; output.source.selection_non_edit_mode <+ sel_on_set_oldest_end; output.source.selection_non_edit_mode <+ sel_on_remove_all; @@ -446,10 +451,14 @@ impl BufferModel { self.oldest_selection().snap_selections_to_start() } - fn new_cursor(&self, location: Location) -> Selection { + fn new_selection(&self, shape: selection::Shape) -> Selection { let id = self.next_selection_id.get(); self.next_selection_id.set(selection::Id { value: id.value + 1 }); - Selection::new_cursor(location, id) + Selection { shape, id } + } + + fn new_cursor(&self, location: Location) -> Selection { + self.new_selection(selection::Shape::new_cursor(location)) } /// Returns the last used selection or a new one if no active selection exists. This allows for @@ -467,6 +476,12 @@ impl BufferModel { selection } + fn set_single_selection(&self, shape: selection::Shape) -> selection::Group { + let last_selection = self.selection.borrow().last().cloned(); + let opt_existing = last_selection.map(|t| t.with_shape(shape)); + opt_existing.unwrap_or_else(|| self.new_selection(shape)).into() + } + fn set_newest_selection_end(&self, location: Location) -> selection::Group { let mut group = self.selection.borrow().clone(); group.newest_mut().for_each(|s| s.end = location); @@ -813,6 +828,7 @@ pub enum LocationLike { LocationUBytesLine(Location), LocationColumnViewLine(Location), LocationUBytesViewLine(Location), + Byte(Byte), } impl Default for LocationLike { @@ -831,6 +847,7 @@ impl LocationLike { Location::from_in_context_snapped(buffer, loc), LocationLike::LocationUBytesViewLine(loc) => Location::from_in_context_snapped(buffer, loc), + LocationLike::Byte(byte) => Location::from_in_context_snapped(buffer, byte), } } } diff --git a/lib/rust/ensogl/component/text/src/component/text.rs b/lib/rust/ensogl/component/text/src/component/text.rs index 756fdf01714..a57d16ea892 100644 --- a/lib/rust/ensogl/component/text/src/component/text.rs +++ b/lib/rust/ensogl/component/text/src/component/text.rs @@ -303,6 +303,7 @@ ensogl_core::define_endpoints_2! { set_cursor (LocationLike), add_cursor (LocationLike), + select (LocationLike, LocationLike), paste_string (ImString), insert (ImString), set_property (RangeLike, Option), @@ -341,6 +342,7 @@ ensogl_core::define_endpoints_2! { width (f32), height (f32), changed (Rc>), + selections (buffer::selection::Group), content (Rope), hovered (bool), selection_color (color::Lch), @@ -408,6 +410,9 @@ impl Text { loc_on_set <- input.set_cursor.map(f!([m](t) t.expand(&m))); loc_on_add <- input.add_cursor.map(f!([m](t) t.expand(&m))); + shape_on_select <- input.select.map( + f!([m]((s, e)) buffer::selection::Shape(s.expand(&m), e.expand(&m))) + ); mouse_on_set <- mouse.position.sample(&input.set_cursor_at_mouse_position); mouse_on_add <- mouse.position.sample(&input.add_cursor_at_mouse_position); @@ -422,8 +427,9 @@ impl Text { loc_on_set <- any(loc_on_set,loc_on_mouse_set,loc_on_set_at_front,loc_on_set_at_end); loc_on_add <- any(loc_on_add,loc_on_mouse_add,loc_on_add_at_front,loc_on_add_at_end); - eval loc_on_set ((loc) m.buffer.frp.set_cursor(loc)); - eval loc_on_add ((loc) m.buffer.frp.add_cursor(loc)); + m.buffer.frp.set_cursor <+ loc_on_set; + m.buffer.frp.add_cursor <+ loc_on_add; + m.buffer.frp.set_single_selection <+ shape_on_select; // === Cursor Transformations === @@ -579,6 +585,8 @@ impl Text { // read the new content, so it should be up-to-date. out.content <+ m.buffer.frp.text_change.map(f_!(m.buffer.text())); out.changed <+ m.buffer.frp.text_change; + out.selections <+ m.buffer.frp.selection_non_edit_mode; + out.selections <+ m.buffer.frp.selection_edit_mode.map(|m| m.selection_group.clone()); // === Text Width And Height Updates === @@ -1914,6 +1922,14 @@ where T: for<'t> FromInContextSnapped<&'t buffer::Buffer, S> } } +impl FromInContextSnapped<&Text, S> for T +where T: for<'t> FromInContextSnapped<&'t TextModel, S> +{ + fn from_in_context_snapped(context: &Text, arg: S) -> Self { + T::from_in_context_snapped(&context.data, arg) + } +} + impl display::Object for TextModel { fn display_object(&self) -> &display::object::Instance { &self.display_object