From 3ba80bbe65b21b14c8c3c54b9293db961a5e67e1 Mon Sep 17 00:00:00 2001 From: Adam Obuchowicz Date: Thu, 16 Jul 2020 16:54:47 +0200 Subject: [PATCH] Applying suggestion in Searcher (https://github.com/enso-org/ide/pull/661) Original commit: https://github.com/enso-org/ide/commit/010e10fe30ddab9c689dbc77b4b690079bc15ce1 --- gui/src/rust/ide/lib/ast/impl/src/prefix.rs | 14 +- gui/src/rust/ide/lib/parser/tests/ast.rs | 1 + gui/src/rust/ide/src/controller/searcher.rs | 444 +++++++++++++++--- .../ide/src/double_representation/module.rs | 63 ++- gui/src/rust/ide/src/model/module.rs | 3 +- .../rust/ide/src/model/suggestion_database.rs | 110 ++++- gui/src/rust/ide/src/view/node_editor.rs | 2 +- gui/src/rust/lib/data/src/text.rs | 2 +- 8 files changed, 540 insertions(+), 99 deletions(-) diff --git a/gui/src/rust/ide/lib/ast/impl/src/prefix.rs b/gui/src/rust/ide/lib/ast/impl/src/prefix.rs index 5fb0cfdbc2d..6e813af8dbf 100644 --- a/gui/src/rust/ide/lib/ast/impl/src/prefix.rs +++ b/gui/src/rust/ide/lib/ast/impl/src/prefix.rs @@ -2,7 +2,7 @@ use crate::prelude::*; -use crate::{Ast, TokenConsumer}; +use crate::Ast; use crate::Id; use crate::crumbs::Located; use crate::crumbs::PrefixCrumb; @@ -10,6 +10,8 @@ use crate::HasTokens; use crate::known; use crate::Prefix; use crate::Shifted; +use crate::Token; +use crate::TokenConsumer; use utils::vec::VecExt; @@ -144,6 +146,16 @@ impl Chain { } } +impl HasTokens for Chain { + fn feed_to(&self, consumer: &mut impl TokenConsumer) { + self.func.feed_to(consumer); + for arg in &self.args { + consumer.feed(Token::Off(arg.sast.off)); + arg.sast.wrapped.feed_to(consumer); + } + } +} + #[cfg(test)] diff --git a/gui/src/rust/ide/lib/parser/tests/ast.rs b/gui/src/rust/ide/lib/parser/tests/ast.rs index 342244f5221..662cc4249e0 100644 --- a/gui/src/rust/ide/lib/parser/tests/ast.rs +++ b/gui/src/rust/ide/lib/parser/tests/ast.rs @@ -83,6 +83,7 @@ pub fn flatten_prefix_test() { let ast = ast::test_utils::expect_single_line(&ast); let flattened = prefix::Chain::new_non_strict(&ast); expect_pieces(&flattened,expected_pieces); + assert_eq!(flattened.repr(), code); }; case("a", vec!["a"]); diff --git a/gui/src/rust/ide/src/controller/searcher.rs b/gui/src/rust/ide/src/controller/searcher.rs index 17fa386949c..47d76c30391 100644 --- a/gui/src/rust/ide/src/controller/searcher.rs +++ b/gui/src/rust/ide/src/controller/searcher.rs @@ -7,6 +7,7 @@ use crate::notification; use data::text::TextLocation; use enso_protocol::language_server; use flo_stream::Subscriber; +use parser::Parser; @@ -14,11 +15,14 @@ use flo_stream::Subscriber; // === Suggestion List === // ======================= +/// Suggestion for input completion: possible functions, arguments, etc. +pub type CompletionSuggestion = Rc; + /// A single suggestion on the Searcher suggestion list. #[derive(Clone,CloneRef,Debug,Eq,PartialEq)] pub enum Suggestion { /// Suggestion for input completion: possible functions, arguments, etc. - Completion(Rc) + Completion(CompletionSuggestion) // In future, other suggestion types will be added (like suggestions of actions, etc.). } @@ -82,17 +86,133 @@ pub enum Notification { } +// =================== +// === Input Parts === +// =================== + +/// 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} +} + +/// 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 suggestions. + pub pattern : String, +} + +impl ParsedInput { + /// Contructor from the plain input. + fn new(mut input:String, parser:&Parser) -> FallibleResult { + 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(input.trim_start())?; + let mut prefix = ast::prefix::Chain::new_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 + }) + } + } + + /// 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()), + } + } +} + +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 + } +} + + // =========================== // === Searcher Controller === // =========================== -/// A controller state. Currently it caches the currently kept suggestions list and the current -/// searcher input. +/// A fragment filled by single picked completion suggestion. +/// +/// We store such information in Searcher to better suggest the potential arguments, and to know +/// what imports should be added when inserting node. +#[derive(Clone,Debug)] +#[allow(missing_docs)] +pub struct FragmentAddedByPickingSuggestion { + pub id : CompletedFragmentId, + pub picked_suggestion : CompletionSuggestion, +} + +impl FragmentAddedByPickingSuggestion { + /// Check if the picked fragment is still unmodified by user. + fn is_still_unmodified(&self, input:&ParsedInput) -> bool { + input.completed_fragment(self.id).contains(&self.picked_suggestion.code_to_insert()) + } +} + +/// A controller state. #[derive(Clone,Debug,Default)] -struct Data { - current_input : String, - current_list : Suggestions, +pub struct Data { + /// The current searcher's input. + pub input : ParsedInput, + /// The suggestion list which should be displayed. + pub suggestions : Suggestions, + /// 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, } /// Searcher Controller. @@ -109,6 +229,7 @@ pub struct Searcher { position : Immutable, database : Rc, language_server : Rc, + parser : Parser, } impl Searcher { @@ -127,6 +248,7 @@ impl Searcher { module : Rc::new(project.qualified_module_name(&module)), database : project.suggestion_db.clone_ref(), language_server : project.language_server_rpc.clone_ref(), + parser : project.parser.clone_ref(), }; this.reload_list(); this @@ -139,16 +261,68 @@ impl Searcher { /// Get the current suggestion list. pub fn suggestions(&self) -> Suggestions { - self.data.borrow().current_list.clone_ref() + self.data.borrow().suggestions.clone_ref() } /// Set the Searcher Input. /// /// This function should be called each time user modifies Searcher input in view. It may result /// in a new suggestion list (the aprriopriate notification will be emitted). - pub fn set_input(&self, new_input:String) { - self.data.borrow_mut().current_input = new_input; - //TODO[ao] here goes refreshing suggestion list after input change. + pub fn set_input(&self, new_input:String) -> FallibleResult<()> { + let parsed_input = ParsedInput::new(new_input,&self.parser)?; + let old_id = self.data.borrow().input.next_completion_id(); + let new_id = parsed_input.next_completion_id(); + + self.data.borrow_mut().input = parsed_input; + self.invalidate_fragments_added_by_picking(); + if old_id != new_id { + self.reload_list() + } + Ok(()) + } + + /// Pick a completion suggestion. + /// + /// This function should be called when user chooses some completion suggestion. The picked + /// suggestion will be remembered, and the searcher's input will be updated and returned by this + /// function. + pub fn pick_completion + (&self, picked_suggestion:CompletionSuggestion) -> FallibleResult { + let added_ast = self.parser.parse_line(&picked_suggestion.code_to_insert())?; + let id = self.data.borrow().input.next_completion_id(); + let picked_completion = FragmentAddedByPickingSuggestion {id,picked_suggestion}; + let pattern_offset = self.data.borrow().input.pattern_offset; + let new_expression = match self.data.borrow_mut().input.expression.take() { + None => { + let ast = ast::prefix::Chain::new_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,added_ast), + prefix_id : default(), + }; + expression.args.push(new_argument); + expression + } + }; + let new_parsed_input = ParsedInput { + expression : Some(new_expression), + pattern_offset : 1, + pattern : "".to_string() + }; + 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(); + Ok(new_input) + } + + fn invalidate_fragments_added_by_picking(&self) { + let mut data = self.data.borrow_mut(); + let data = data.deref_mut(); + let input = &data.input; + data.fragments_added_by_picking.drain_filter(|frag| !frag.is_still_unmodified(input)); } /// Reload Suggestion List. @@ -156,23 +330,33 @@ impl Searcher { /// The current list will be set as "Loading" and Language Server will be requested for a new /// list - once it be retrieved, the new list will be set and notification will be emitted. fn reload_list(&self) { + let next_completion = self.data.borrow().input.next_completion_id(); + let new_suggestions = if next_completion == CompletedFragmentId::Function { + self.get_suggestion_list_from_engine(None,None); + Suggestions::Loading + } else { + // TODO[ao] Requesting for argument. + Suggestions::Loaded {list:default()} + }; + self.data.borrow_mut().suggestions = new_suggestions; + } + + fn get_suggestion_list_from_engine + (&self, return_type:Option, tags:Option>) { + let ls = &self.language_server; let module = self.module.as_ref(); let self_type = None; - let return_type = None; - let tags = None; let position = self.position.deref().into(); - let request = self.language_server.completion(module,&position,&self_type,&return_type,&tags); + let request = ls.completion(module,&position,&self_type,&return_type,&tags); let data = self.data.clone_ref(); let database = self.database.clone_ref(); let logger = self.logger.clone_ref(); let notifier = self.notifier.clone_ref(); - - self.data.borrow_mut().current_list = Suggestions::Loading; executor::global::spawn(async move { info!(logger,"Requesting new suggestion list."); - let ls_response = request.await; + let response = request.await; info!(logger,"Received suggestions from Language Server."); - let new_list = match ls_response { + let new_suggestions = match response { Ok(list) => { let entry_ids = list.results.into_iter(); let entries = entry_ids.filter_map(|id| { @@ -187,7 +371,7 @@ impl Searcher { }, Err(error) => Suggestions::Error(Rc::new(error.into())) }; - data.borrow_mut().current_list = new_list; + data.borrow_mut().suggestions = new_suggestions; notifier.publish(Notification::NewSuggestionList).await; }); } @@ -208,59 +392,195 @@ mod test { use json_rpc::expect_call; use utils::test::traits::*; - #[test] - fn reloading_list() { - let mut test = TestWithLocalPoolExecutor::set_up(); - let client = language_server::MockClient::default(); - let module_path = Path::from_mock_module_name("Test"); + struct Fixture { + searcher : Searcher, + entry1 : CompletionSuggestion, + entry2 : CompletionSuggestion, + entry9 : CompletionSuggestion, + } - let completion_response = language_server::response::Completion { - results: vec![1,5,9], - current_version: default(), - }; - expect_call!(client.completion( - module = "Test.Test".to_string(), - position = TextLocation::at_document_begin().into(), - self_type = None, - return_type = None, - tag = None - ) => Ok(completion_response)); + impl Fixture { + fn new(client_setup:impl FnOnce(&mut language_server::MockClient)) -> Self { + let mut client = language_server::MockClient::default(); + let module_path = Path::from_mock_module_name("Test"); + client_setup(&mut client); + let searcher = Searcher { + logger : default(), + data : default(), + notifier : default(), + module : Rc::new(module_path.qualified_module_name("Test")), + position : Immutable(TextLocation::at_document_begin()), + database : default(), + language_server : language_server::Connection::new_mock_rc(client), + parser : Parser::new_or_panic(), + }; + let entry1 = model::suggestion_database::Entry { + name : "TestFunction1".to_string(), + kind : model::suggestion_database::EntryKind::Function, + module : "Test.Test".to_string().try_into().unwrap(), + arguments : vec![], + return_type : "Number".to_string(), + documentation : default(), + self_type : None + }; + let entry2 = model::suggestion_database::Entry { + name : "TestVar1".to_string(), + kind : model::suggestion_database::EntryKind::Local, + ..entry1.clone() + }; + let entry9 = entry1.clone().with_name("TestFunction2"); - let searcher = Searcher { - logger : default(), - data : default(), - notifier : default(), - module : Rc::new(module_path.qualified_module_name("Test")), - position : Immutable(TextLocation::at_document_begin()), - database : default(), - language_server : language_server::Connection::new_mock_rc(client), - }; - let entry1 = model::suggestion_database::Entry { - name : "TestFunction1".to_string(), - kind : model::suggestion_database::EntryKind::Function, - module : "Test.Test".to_string(), - arguments : vec![], - return_type : "Number".to_string(), - documentation : default(), - self_type : None - }; - let entry2 = model::suggestion_database::Entry { - name : "TestFunction2".to_string(), - ..entry1.clone() - }; - searcher.database.put_entry(1,entry1); - let entry1 = searcher.database.get(1).unwrap(); - searcher.database.put_entry(9,entry2); - let entry2 = searcher.database.get(9).unwrap(); + searcher.database.put_entry(1,entry1); + let entry1 = searcher.database.get(1).unwrap(); + searcher.database.put_entry(2,entry2); + let entry2 = searcher.database.get(2).unwrap(); + searcher.database.put_entry(9,entry9); + let entry9 = searcher.database.get(9).unwrap(); + Fixture{searcher,entry1,entry2,entry9} + } + } + + + #[wasm_bindgen_test] + fn loading_list() { + let mut test = TestWithLocalPoolExecutor::set_up(); + let Fixture{searcher,entry1,entry9,..} = Fixture::new(|client| { + let completion_response = language_server::response::Completion { + results: vec![1,5,9], + current_version: default(), + }; + expect_call!(client.completion( + module = "Test.Test".to_string(), + position = TextLocation::at_document_begin().into(), + self_type = None, + return_type = None, + tag = None + ) => Ok(completion_response)); + }); let mut subscriber = searcher.subscribe(); - searcher.reload_list(); assert!(searcher.suggestions().is_loading()); test.run_until_stalled(); - let expected_list = vec![Suggestion::Completion(entry1),Suggestion::Completion(entry2)]; + let expected_list = vec![Suggestion::Completion(entry1),Suggestion::Completion(entry9)]; assert_eq!(searcher.suggestions().list(), Some(&expected_list)); let notification = subscriber.next().boxed_local().expect_ready(); assert_eq!(notification, Some(Notification::NewSuggestionList)); } + + #[wasm_bindgen_test] + fn parsed_input() { + let parser = Parser::new_or_panic(); + + 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()]); + assert_eq!(parsed.pattern_offset, 1); + assert_eq!(parsed.pattern.as_str(), "(baz "); + } + + #[wasm_bindgen_test] + fn picked_completions_list_maintaining() { + let Fixture{searcher,entry1,entry2,..} = Fixture::new(|_|{}); + let frags_borrow = || Ref::map(searcher.data.borrow(),|d| &d.fragments_added_by_picking); + + // Picking first suggestion. + let new_input = searcher.pick_completion(entry1.clone_ref()).unwrap(); + assert_eq!(new_input, "TestFunction1 "); + let (func,) = frags_borrow().iter().cloned().expect_tuple(); + assert_eq!(func.id, CompletedFragmentId::Function); + assert!(Rc::ptr_eq(&func.picked_suggestion,&entry1)); + + // 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!(Rc::ptr_eq(&func.picked_suggestion,&entry1)); + + // Picking argument's suggestion. + let new_input = searcher.pick_completion(entry2.clone_ref()).unwrap(); + assert_eq!(new_input, "TestFunction1 some_arg TestVar1 "); + let new_input = searcher.pick_completion(entry2.clone_ref()).unwrap(); + assert_eq!(new_input, "TestFunction1 some_arg TestVar1 TestVar1 "); + let (function,arg1,arg2) = frags_borrow().iter().cloned().expect_tuple(); + assert_eq!(function.id, CompletedFragmentId::Function); + assert!(Rc::ptr_eq(&function.picked_suggestion,&entry1)); + assert_eq!(arg1.id, CompletedFragmentId::Argument {index:1}); + assert!(Rc::ptr_eq(&arg1.picked_suggestion,&entry2)); + assert_eq!(arg2.id, CompletedFragmentId::Argument {index:2}); + assert!(Rc::ptr_eq(&arg2.picked_suggestion,&entry2)); + + // Backspacing back to the second arg. + searcher.set_input("TestFunction1 some_arg TestVar1 TestV".to_string()).unwrap(); + let (picked,arg) = frags_borrow().iter().cloned().expect_tuple(); + assert_eq!(picked.id, CompletedFragmentId::Function); + assert!(Rc::ptr_eq(&picked.picked_suggestion,&entry1)); + assert_eq!(arg.id, CompletedFragmentId::Argument {index:1}); + assert!(Rc::ptr_eq(&arg.picked_suggestion,&entry2)); + + // Editing the picked function. + searcher.set_input("TestFunction2 some_arg TestVar1 TestV".to_string()).unwrap(); + let (arg,) = frags_borrow().iter().cloned().expect_tuple(); + assert_eq!(arg.id, CompletedFragmentId::Argument {index:1}); + assert!(Rc::ptr_eq(&arg.picked_suggestion,&entry2)); + } } diff --git a/gui/src/rust/ide/src/double_representation/module.rs b/gui/src/rust/ide/src/double_representation/module.rs index 503955ac3ae..da27761e2e6 100644 --- a/gui/src/rust/ide/src/double_representation/module.rs +++ b/gui/src/rust/ide/src/double_representation/module.rs @@ -10,13 +10,20 @@ use ast::crumbs::ModuleCrumb; use ast::known; use ast::BlockLine; use enso_protocol::language_server; - +use data::text::ByteIndex; // ===================== // === QualifiedName === // ===================== +#[allow(missing_docs)] +#[derive(Clone,Copy,Debug,Fail)] +pub enum InvalidQualifiedName { + #[fail(display="No module segment in qualified name.")] + NoModuleSegment, +} + /// Module's qualified name is used in some of the Language Server's APIs, like /// `VisualisationConfiguration`. /// @@ -25,8 +32,12 @@ use enso_protocol::language_server; /// /// See https://dev.enso.org/docs/distribution/packaging.html for more information about the /// package structure. -#[derive(Clone,Debug,Display,Shrinkwrap)] -pub struct QualifiedName(String); +#[derive(Clone,Debug,Shrinkwrap)] +pub struct QualifiedName { + #[shrinkwrap(main_field)] + text : String, + name_part : Range, +} impl QualifiedName { /// Build a module's full qualified name from its name segments and the project name. @@ -34,21 +45,50 @@ impl QualifiedName { /// ``` /// use ide::model::module::QualifiedName; /// - /// let name = QualifiedName::from_segments("Project",&["Main"]); + /// let name = QualifiedName::from_segments("Project",&["Main"]).unwrap(); /// assert_eq!(name.to_string(), "Project.Main"); /// ``` pub fn from_segments (project_name:impl Str, module_segments:impl IntoIterator>) - -> QualifiedName { + -> FallibleResult { let project_name = std::iter::once(project_name.into()); let module_segments = module_segments.into_iter(); let module_segments = module_segments.map(|segment| segment.as_ref().to_string()); let mut all_segments = project_name.chain(module_segments); - let name = all_segments.join("."); - QualifiedName(name) + let text = all_segments.join(ast::opr::predefined::ACCESS); + Ok(text.try_into()?) + } + + /// Get the unqualified name of the module. + pub fn name(&self) -> &str { + &self.text[self.name_part.start.value..self.name_part.end.value] } } +impl TryFrom for QualifiedName { + type Error = InvalidQualifiedName; + + fn try_from(text:String) -> Result { + let error = InvalidQualifiedName::NoModuleSegment; + let name_start = text.rfind(ast::opr::predefined::ACCESS).ok_or(error)? + 1; + let name_part = ByteIndex::new(name_start)..ByteIndex::new(text.len()); + Ok(QualifiedName {text,name_part}) + } +} + +impl Display for QualifiedName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.text,f) + } +} + +impl PartialEq for QualifiedName { + fn eq(&self,rhs:&QualifiedName) -> bool { + self.text == rhs.text + } +} + +impl Eq for QualifiedName {} // ================== @@ -63,6 +103,8 @@ impl QualifiedName { #[derive(Clone,Debug,PartialEq)] pub struct ImportInfo { /// The segments of the qualified name of the imported target. + /// + /// This field is not Qualified name to cover semantically illegal imports. pub target:Vec } @@ -84,8 +126,8 @@ impl ImportInfo { } /// Obtain the qualified name of the imported module. - pub fn qualified_name(&self) -> QualifiedName { - QualifiedName(self.target.join(ast::opr::predefined::ACCESS)) + pub fn qualified_name(&self) -> FallibleResult { + Ok(self.target.join(ast::opr::predefined::ACCESS).try_into()?) } /// Construct from an AST. Fails if the Ast is not an import declaration. @@ -104,7 +146,8 @@ impl ImportInfo { impl Display for ImportInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}",ast::macros::IMPORT_KEYWORD,self.qualified_name()) + let target = self.target.join(ast::opr::predefined::ACCESS); + write!(f, "{} {}",ast::macros::IMPORT_KEYWORD,target) } } diff --git a/gui/src/rust/ide/src/model/module.rs b/gui/src/rust/ide/src/model/module.rs index feec4366ae1..fdd87f248af 100644 --- a/gui/src/rust/ide/src/model/module.rs +++ b/gui/src/rust/ide/src/model/module.rs @@ -166,7 +166,8 @@ impl Path { let non_src_directories = non_src_directories.iter().map(|dirname| dirname.as_str()); let module_name = std::iter::once(self.module_name()); let module_segments = non_src_directories.chain(module_name); - QualifiedName::from_segments(project_name,module_segments) + // The module path during creation should be checked for at least one module segment. + QualifiedName::from_segments(project_name,module_segments).unwrap() } } diff --git a/gui/src/rust/ide/src/model/suggestion_database.rs b/gui/src/rust/ide/src/model/suggestion_database.rs index cee074eb72c..4ee12ffe14a 100644 --- a/gui/src/rust/ide/src/model/suggestion_database.rs +++ b/gui/src/rust/ide/src/model/suggestion_database.rs @@ -2,6 +2,8 @@ use crate::prelude::*; +use crate::double_representation::module::QualifiedName; + use enso_protocol::language_server; use language_server::types::SuggestionsDatabaseVersion; use language_server::types::SuggestionDatabaseUpdateEvent; @@ -11,7 +13,6 @@ pub use language_server::types::SuggestionEntryId as EntryId; pub use language_server::types::SuggestionsDatabaseUpdate as Update; - // ============= // === Entry === // ============= @@ -30,8 +31,8 @@ pub struct Entry { pub name : String, /// A type of suggestion. pub kind : EntryKind, - /// A module where the suggested object is defined. - pub module : String, + /// A module where the suggested object is defined, represented as vector of segments. + pub module : QualifiedName, /// Argument lists of suggested object (atom or function). If the object does not take any /// arguments, the list is empty. pub arguments : Vec, @@ -45,42 +46,63 @@ pub struct Entry { impl Entry { /// Create entry from the structure deserialized from the Language Server responses. - pub fn from_ls_entry(entry:language_server::types::SuggestionEntry) -> Self { + pub fn from_ls_entry(entry:language_server::types::SuggestionEntry) -> FallibleResult { use language_server::types::SuggestionEntry::*; - match entry { + let this = match entry { SuggestionEntryAtom {name,module,arguments,return_type,documentation} => Self { - name,module,arguments,return_type,documentation, - self_type : None, - kind : EntryKind::Atom, + name,arguments,return_type,documentation, + module : module.try_into()?, + self_type : None, + kind : EntryKind::Atom, }, SuggestionEntryMethod {name,module,arguments,self_type,return_type,documentation} => Self { - name,module,arguments,return_type,documentation, - self_type : Some(self_type), - kind : EntryKind::Method, + name,arguments,return_type,documentation, + module : module.try_into()?, + self_type : Some(self_type), + kind : EntryKind::Method, }, SuggestionEntryFunction {name,module,arguments,return_type,..} => Self { - name,module,arguments,return_type, + name,arguments,return_type, + module : module.try_into()?, self_type : None, documentation : default(), kind : EntryKind::Function, }, SuggestionEntryLocal {name,module,return_type,..} => Self { - name,module,return_type, + name,return_type, arguments : default(), + module : module.try_into()?, self_type : None, documentation : default(), kind : EntryKind::Local, }, + }; + Ok(this) + } + + /// Returns the code which should be inserted to Searcher input when suggestion is picked. + pub fn code_to_insert(&self) -> String { + let module = self.module.name(); + if self.self_type.as_ref().contains(&module) { + iformat!("{module}.{self.name}") + } else { + self.name.clone() } } + + /// Returns entry with the changed name. + pub fn with_name(self, name:impl Into) -> Self { + Self {name:name.into(),..self} + } } -impl From for Entry { - fn from(entry:language_server::types::SuggestionEntry) -> Self { +impl TryFrom for Entry { + type Error = failure::Error; + fn try_from(entry:language_server::types::SuggestionEntry) -> FallibleResult { Self::from_ls_entry(entry) } } @@ -99,6 +121,7 @@ impl From for Entry { /// argument names and types. #[derive(Clone,Debug,Default)] pub struct SuggestionDatabase { + logger : Logger, entries : RefCell>>, version : Cell, } @@ -113,11 +136,17 @@ impl SuggestionDatabase { /// Create a new database model from response received from the Language Server. fn from_ls_response(response:language_server::response::GetSuggestionDatabase) -> Self { + let logger = Logger::new("SuggestionDatabase"); let mut entries = HashMap::new(); - for entry in response.entries { - entries.insert(entry.id, Rc::new(Entry::from_ls_entry(entry.suggestion))); + for ls_entry in response.entries { + let id = ls_entry.id; + match Entry::from_ls_entry(ls_entry.suggestion) { + Ok(entry) => { entries.insert(id, Rc::new(entry)); }, + Err(err) => { error!(logger,"Discarded invalid entry {id}: {err}"); }, + } } Self { + logger, entries : RefCell::new(entries), version : Cell::new(response.current_version), } @@ -133,8 +162,11 @@ impl SuggestionDatabase { for update in event.updates { let mut entries = self.entries.borrow_mut(); match update { - Update::Add {id,entry} => entries.insert(id,Rc::new(entry.into())), - Update::Remove {id} => entries.remove(&id), + Update::Add {id,entry} => match entry.try_into() { + Ok(entry) => { entries.insert(id,Rc::new(entry)); }, + Err(err) => { error!(self.logger, "Discarding update for {id}: {err}") }, + }, + Update::Remove {id} => { entries.remove(&id); }, }; } self.version.set(event.current_version); @@ -163,8 +195,40 @@ impl From for SuggestionDataba #[cfg(test)] mod test { use super::*; + use enso_protocol::language_server::SuggestionsDatabaseEntry; + + + #[test] + fn code_from_entry() { + let module = QualifiedName::from_segments("Project",&["Main"]).unwrap(); + let atom_entry = Entry { + name : "Atom".to_string(), + kind : EntryKind::Atom, + module, + arguments : vec![], + return_type : "Number".to_string(), + documentation : None, + self_type : None + }; + let method_entry = Entry { + name : "method".to_string(), + kind : EntryKind::Method, + self_type : Some("Number".to_string()), + ..atom_entry.clone() + }; + let module_method_entry = Entry { + name : "moduleMethod".to_string(), + self_type : Some("Main".to_string()), + ..method_entry.clone() + }; + + assert_eq!(atom_entry.code_to_insert() , "Atom".to_string()); + assert_eq!(method_entry.code_to_insert() , "method".to_string()); + assert_eq!(module_method_entry.code_to_insert(), "Main.moduleMethod".to_string()); + } + #[test] fn initialize_database() { // Empty db @@ -179,7 +243,7 @@ mod test { // Non-empty db let entry = language_server::types::SuggestionEntry::SuggestionEntryAtom { name : "TextAtom".to_string(), - module : "TestModule".to_string(), + module : "TestProject.TestModule".to_string(), arguments : vec![], return_type : "TestAtom".to_string(), documentation : None @@ -199,21 +263,21 @@ mod test { fn applying_update() { let entry1 = language_server::types::SuggestionEntry::SuggestionEntryAtom { name : "Entry1".to_string(), - module : "TestModule".to_string(), + module : "TestProject.TestModule".to_string(), arguments : vec![], return_type : "TestAtom".to_string(), documentation : None }; let entry2 = language_server::types::SuggestionEntry::SuggestionEntryAtom { name : "Entry2".to_string(), - module : "TestModule".to_string(), + module : "TestProject.TestModule".to_string(), arguments : vec![], return_type : "TestAtom".to_string(), documentation : None }; let new_entry2 = language_server::types::SuggestionEntry::SuggestionEntryAtom { name : "NewEntry2".to_string(), - module : "TestModule".to_string(), + module : "TestProject.TestModule".to_string(), arguments : vec![], return_type : "TestAtom".to_string(), documentation : None diff --git a/gui/src/rust/ide/src/view/node_editor.rs b/gui/src/rust/ide/src/view/node_editor.rs index d69f4240a96..638ab6090d0 100644 --- a/gui/src/rust/ide/src/view/node_editor.rs +++ b/gui/src/rust/ide/src/view/node_editor.rs @@ -588,7 +588,7 @@ impl GraphEditorIntegratedWithControllerModel { // to the customised values. let project_name = self.project.name.as_ref(); let module_name = crate::view::project::INITIAL_MODULE_NAME; - let visualisation_module = QualifiedName::from_segments(project_name,&[module_name]); + let visualisation_module = QualifiedName::from_segments(project_name,&[module_name])?; let id = VisualizationId::new_v4(); let expression = crate::constants::SERIALIZE_TO_JSON_EXPRESSION.into(); let ast_id = self.get_controller_node_id(*node_id)?; diff --git a/gui/src/rust/lib/data/src/text.rs b/gui/src/rust/lib/data/src/text.rs index e5f80e61584..4a79ff5fb74 100644 --- a/gui/src/rust/lib/data/src/text.rs +++ b/gui/src/rust/lib/data/src/text.rs @@ -41,7 +41,7 @@ impl Index { /// Strongly typed index of byte in String (which may differ with analogous character index, /// because some chars takes more than one byte). -//TODO[ao] We should use structures from ensogl::,math::topology to represent different quantities +//TODO[ao] We should use structures from ensogl::math::topology to represent different quantities // and units. #[allow(missing_docs)] #[derive(Clone,Copy,Debug,Default,Hash,PartialEq,Eq,PartialOrd,Ord,Serialize,Deserialize)]