Original commit: 010e10fe30
This commit is contained in:
Adam Obuchowicz 2020-07-16 16:54:47 +02:00 committed by GitHub
parent 820f2c9553
commit 3ba80bbe65
8 changed files with 540 additions and 99 deletions

View File

@ -2,7 +2,7 @@
use crate::prelude::*; use crate::prelude::*;
use crate::{Ast, TokenConsumer}; use crate::Ast;
use crate::Id; use crate::Id;
use crate::crumbs::Located; use crate::crumbs::Located;
use crate::crumbs::PrefixCrumb; use crate::crumbs::PrefixCrumb;
@ -10,6 +10,8 @@ use crate::HasTokens;
use crate::known; use crate::known;
use crate::Prefix; use crate::Prefix;
use crate::Shifted; use crate::Shifted;
use crate::Token;
use crate::TokenConsumer;
use utils::vec::VecExt; 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)] #[cfg(test)]

View File

@ -83,6 +83,7 @@ pub fn flatten_prefix_test() {
let ast = ast::test_utils::expect_single_line(&ast); let ast = ast::test_utils::expect_single_line(&ast);
let flattened = prefix::Chain::new_non_strict(&ast); let flattened = prefix::Chain::new_non_strict(&ast);
expect_pieces(&flattened,expected_pieces); expect_pieces(&flattened,expected_pieces);
assert_eq!(flattened.repr(), code);
}; };
case("a", vec!["a"]); case("a", vec!["a"]);

View File

@ -7,6 +7,7 @@ use crate::notification;
use data::text::TextLocation; use data::text::TextLocation;
use enso_protocol::language_server; use enso_protocol::language_server;
use flo_stream::Subscriber; use flo_stream::Subscriber;
use parser::Parser;
@ -14,11 +15,14 @@ use flo_stream::Subscriber;
// === Suggestion List === // === Suggestion List ===
// ======================= // =======================
/// Suggestion for input completion: possible functions, arguments, etc.
pub type CompletionSuggestion = Rc<model::suggestion_database::Entry>;
/// A single suggestion on the Searcher suggestion list. /// A single suggestion on the Searcher suggestion list.
#[derive(Clone,CloneRef,Debug,Eq,PartialEq)] #[derive(Clone,CloneRef,Debug,Eq,PartialEq)]
pub enum Suggestion { pub enum Suggestion {
/// Suggestion for input completion: possible functions, arguments, etc. /// Suggestion for input completion: possible functions, arguments, etc.
Completion(Rc<model::suggestion_database::Entry>) Completion(CompletionSuggestion)
// In future, other suggestion types will be added (like suggestions of actions, etc.). // 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<ast::Shifted<ast::prefix::Chain>>,
/// 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<Self> {
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<String> {
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 === // === Searcher Controller ===
// =========================== // ===========================
/// A controller state. Currently it caches the currently kept suggestions list and the current /// A fragment filled by single picked completion suggestion.
/// searcher input. ///
/// 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)] #[derive(Clone,Debug,Default)]
struct Data { pub struct Data {
current_input : String, /// The current searcher's input.
current_list : Suggestions, 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<FragmentAddedByPickingSuggestion>,
} }
/// Searcher Controller. /// Searcher Controller.
@ -109,6 +229,7 @@ pub struct Searcher {
position : Immutable<TextLocation>, position : Immutable<TextLocation>,
database : Rc<model::SuggestionDatabase>, database : Rc<model::SuggestionDatabase>,
language_server : Rc<language_server::Connection>, language_server : Rc<language_server::Connection>,
parser : Parser,
} }
impl Searcher { impl Searcher {
@ -127,6 +248,7 @@ impl Searcher {
module : Rc::new(project.qualified_module_name(&module)), module : Rc::new(project.qualified_module_name(&module)),
database : project.suggestion_db.clone_ref(), database : project.suggestion_db.clone_ref(),
language_server : project.language_server_rpc.clone_ref(), language_server : project.language_server_rpc.clone_ref(),
parser : project.parser.clone_ref(),
}; };
this.reload_list(); this.reload_list();
this this
@ -139,16 +261,68 @@ impl Searcher {
/// Get the current suggestion list. /// Get the current suggestion list.
pub fn suggestions(&self) -> Suggestions { pub fn suggestions(&self) -> Suggestions {
self.data.borrow().current_list.clone_ref() self.data.borrow().suggestions.clone_ref()
} }
/// Set the Searcher Input. /// Set the Searcher Input.
/// ///
/// This function should be called each time user modifies Searcher input in view. It may result /// 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). /// in a new suggestion list (the aprriopriate notification will be emitted).
pub fn set_input(&self, new_input:String) { pub fn set_input(&self, new_input:String) -> FallibleResult<()> {
self.data.borrow_mut().current_input = new_input; let parsed_input = ParsedInput::new(new_input,&self.parser)?;
//TODO[ao] here goes refreshing suggestion list after input change. 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<String> {
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. /// 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 /// 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. /// list - once it be retrieved, the new list will be set and notification will be emitted.
fn reload_list(&self) { 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<String>, tags:Option<Vec<language_server::SuggestionEntryType>>) {
let ls = &self.language_server;
let module = self.module.as_ref(); let module = self.module.as_ref();
let self_type = None; let self_type = None;
let return_type = None;
let tags = None;
let position = self.position.deref().into(); 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 data = self.data.clone_ref();
let database = self.database.clone_ref(); let database = self.database.clone_ref();
let logger = self.logger.clone_ref(); let logger = self.logger.clone_ref();
let notifier = self.notifier.clone_ref(); let notifier = self.notifier.clone_ref();
self.data.borrow_mut().current_list = Suggestions::Loading;
executor::global::spawn(async move { executor::global::spawn(async move {
info!(logger,"Requesting new suggestion list."); info!(logger,"Requesting new suggestion list.");
let ls_response = request.await; let response = request.await;
info!(logger,"Received suggestions from Language Server."); info!(logger,"Received suggestions from Language Server.");
let new_list = match ls_response { let new_suggestions = match response {
Ok(list) => { Ok(list) => {
let entry_ids = list.results.into_iter(); let entry_ids = list.results.into_iter();
let entries = entry_ids.filter_map(|id| { let entries = entry_ids.filter_map(|id| {
@ -187,7 +371,7 @@ impl Searcher {
}, },
Err(error) => Suggestions::Error(Rc::new(error.into())) 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; notifier.publish(Notification::NewSuggestionList).await;
}); });
} }
@ -208,59 +392,195 @@ mod test {
use json_rpc::expect_call; use json_rpc::expect_call;
use utils::test::traits::*; use utils::test::traits::*;
#[test] struct Fixture {
fn reloading_list() { searcher : Searcher,
let mut test = TestWithLocalPoolExecutor::set_up(); entry1 : CompletionSuggestion,
let client = language_server::MockClient::default(); entry2 : CompletionSuggestion,
let module_path = Path::from_mock_module_name("Test"); entry9 : CompletionSuggestion,
}
let completion_response = language_server::response::Completion { impl Fixture {
results: vec![1,5,9], fn new(client_setup:impl FnOnce(&mut language_server::MockClient)) -> Self {
current_version: default(), let mut client = language_server::MockClient::default();
}; let module_path = Path::from_mock_module_name("Test");
expect_call!(client.completion( client_setup(&mut client);
module = "Test.Test".to_string(), let searcher = Searcher {
position = TextLocation::at_document_begin().into(), logger : default(),
self_type = None, data : default(),
return_type = None, notifier : default(),
tag = None module : Rc::new(module_path.qualified_module_name("Test")),
) => Ok(completion_response)); 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 { searcher.database.put_entry(1,entry1);
logger : default(), let entry1 = searcher.database.get(1).unwrap();
data : default(), searcher.database.put_entry(2,entry2);
notifier : default(), let entry2 = searcher.database.get(2).unwrap();
module : Rc::new(module_path.qualified_module_name("Test")), searcher.database.put_entry(9,entry9);
position : Immutable(TextLocation::at_document_begin()), let entry9 = searcher.database.get(9).unwrap();
database : default(), Fixture{searcher,entry1,entry2,entry9}
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, #[wasm_bindgen_test]
module : "Test.Test".to_string(), fn loading_list() {
arguments : vec![], let mut test = TestWithLocalPoolExecutor::set_up();
return_type : "Number".to_string(), let Fixture{searcher,entry1,entry9,..} = Fixture::new(|client| {
documentation : default(), let completion_response = language_server::response::Completion {
self_type : None results: vec![1,5,9],
}; current_version: default(),
let entry2 = model::suggestion_database::Entry { };
name : "TestFunction2".to_string(), expect_call!(client.completion(
..entry1.clone() module = "Test.Test".to_string(),
}; position = TextLocation::at_document_begin().into(),
searcher.database.put_entry(1,entry1); self_type = None,
let entry1 = searcher.database.get(1).unwrap(); return_type = None,
searcher.database.put_entry(9,entry2); tag = None
let entry2 = searcher.database.get(9).unwrap(); ) => Ok(completion_response));
});
let mut subscriber = searcher.subscribe(); let mut subscriber = searcher.subscribe();
searcher.reload_list(); searcher.reload_list();
assert!(searcher.suggestions().is_loading()); assert!(searcher.suggestions().is_loading());
test.run_until_stalled(); 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)); assert_eq!(searcher.suggestions().list(), Some(&expected_list));
let notification = subscriber.next().boxed_local().expect_ready(); let notification = subscriber.next().boxed_local().expect_ready();
assert_eq!(notification, Some(Notification::NewSuggestionList)); 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<String> {
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::<String>::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::<String>::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));
}
} }

View File

@ -10,13 +10,20 @@ use ast::crumbs::ModuleCrumb;
use ast::known; use ast::known;
use ast::BlockLine; use ast::BlockLine;
use enso_protocol::language_server; use enso_protocol::language_server;
use data::text::ByteIndex;
// ===================== // =====================
// === QualifiedName === // === 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 /// Module's qualified name is used in some of the Language Server's APIs, like
/// `VisualisationConfiguration`. /// `VisualisationConfiguration`.
/// ///
@ -25,8 +32,12 @@ use enso_protocol::language_server;
/// ///
/// See https://dev.enso.org/docs/distribution/packaging.html for more information about the /// See https://dev.enso.org/docs/distribution/packaging.html for more information about the
/// package structure. /// package structure.
#[derive(Clone,Debug,Display,Shrinkwrap)] #[derive(Clone,Debug,Shrinkwrap)]
pub struct QualifiedName(String); pub struct QualifiedName {
#[shrinkwrap(main_field)]
text : String,
name_part : Range<ByteIndex>,
}
impl QualifiedName { impl QualifiedName {
/// Build a module's full qualified name from its name segments and the project name. /// 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; /// 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"); /// assert_eq!(name.to_string(), "Project.Main");
/// ``` /// ```
pub fn from_segments pub fn from_segments
(project_name:impl Str, module_segments:impl IntoIterator<Item:AsRef<str>>) (project_name:impl Str, module_segments:impl IntoIterator<Item:AsRef<str>>)
-> QualifiedName { -> FallibleResult<QualifiedName> {
let project_name = std::iter::once(project_name.into()); let project_name = std::iter::once(project_name.into());
let module_segments = module_segments.into_iter(); let module_segments = module_segments.into_iter();
let module_segments = module_segments.map(|segment| segment.as_ref().to_string()); let module_segments = module_segments.map(|segment| segment.as_ref().to_string());
let mut all_segments = project_name.chain(module_segments); let mut all_segments = project_name.chain(module_segments);
let name = all_segments.join("."); let text = all_segments.join(ast::opr::predefined::ACCESS);
QualifiedName(name) 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<String> for QualifiedName {
type Error = InvalidQualifiedName;
fn try_from(text:String) -> Result<Self,Self::Error> {
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<QualifiedName> 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)] #[derive(Clone,Debug,PartialEq)]
pub struct ImportInfo { pub struct ImportInfo {
/// The segments of the qualified name of the imported target. /// The segments of the qualified name of the imported target.
///
/// This field is not Qualified name to cover semantically illegal imports.
pub target:Vec<String> pub target:Vec<String>
} }
@ -84,8 +126,8 @@ impl ImportInfo {
} }
/// Obtain the qualified name of the imported module. /// Obtain the qualified name of the imported module.
pub fn qualified_name(&self) -> QualifiedName { pub fn qualified_name(&self) -> FallibleResult<QualifiedName> {
QualifiedName(self.target.join(ast::opr::predefined::ACCESS)) Ok(self.target.join(ast::opr::predefined::ACCESS).try_into()?)
} }
/// Construct from an AST. Fails if the Ast is not an import declaration. /// Construct from an AST. Fails if the Ast is not an import declaration.
@ -104,7 +146,8 @@ impl ImportInfo {
impl Display for ImportInfo { impl Display for ImportInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 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)
} }
} }

View File

@ -166,7 +166,8 @@ impl Path {
let non_src_directories = non_src_directories.iter().map(|dirname| dirname.as_str()); let non_src_directories = non_src_directories.iter().map(|dirname| dirname.as_str());
let module_name = std::iter::once(self.module_name()); let module_name = std::iter::once(self.module_name());
let module_segments = non_src_directories.chain(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()
} }
} }

View File

@ -2,6 +2,8 @@
use crate::prelude::*; use crate::prelude::*;
use crate::double_representation::module::QualifiedName;
use enso_protocol::language_server; use enso_protocol::language_server;
use language_server::types::SuggestionsDatabaseVersion; use language_server::types::SuggestionsDatabaseVersion;
use language_server::types::SuggestionDatabaseUpdateEvent; 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; pub use language_server::types::SuggestionsDatabaseUpdate as Update;
// ============= // =============
// === Entry === // === Entry ===
// ============= // =============
@ -30,8 +31,8 @@ pub struct Entry {
pub name : String, pub name : String,
/// A type of suggestion. /// A type of suggestion.
pub kind : EntryKind, pub kind : EntryKind,
/// A module where the suggested object is defined. /// A module where the suggested object is defined, represented as vector of segments.
pub module : String, pub module : QualifiedName,
/// Argument lists of suggested object (atom or function). If the object does not take any /// Argument lists of suggested object (atom or function). If the object does not take any
/// arguments, the list is empty. /// arguments, the list is empty.
pub arguments : Vec<Argument>, pub arguments : Vec<Argument>,
@ -45,42 +46,63 @@ pub struct Entry {
impl Entry { impl Entry {
/// Create entry from the structure deserialized from the Language Server responses. /// 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<Self> {
use language_server::types::SuggestionEntry::*; use language_server::types::SuggestionEntry::*;
match entry { let this = match entry {
SuggestionEntryAtom {name,module,arguments,return_type,documentation} => SuggestionEntryAtom {name,module,arguments,return_type,documentation} =>
Self { Self {
name,module,arguments,return_type,documentation, name,arguments,return_type,documentation,
self_type : None, module : module.try_into()?,
kind : EntryKind::Atom, self_type : None,
kind : EntryKind::Atom,
}, },
SuggestionEntryMethod {name,module,arguments,self_type,return_type,documentation} => SuggestionEntryMethod {name,module,arguments,self_type,return_type,documentation} =>
Self { Self {
name,module,arguments,return_type,documentation, name,arguments,return_type,documentation,
self_type : Some(self_type), module : module.try_into()?,
kind : EntryKind::Method, self_type : Some(self_type),
kind : EntryKind::Method,
}, },
SuggestionEntryFunction {name,module,arguments,return_type,..} => SuggestionEntryFunction {name,module,arguments,return_type,..} =>
Self { Self {
name,module,arguments,return_type, name,arguments,return_type,
module : module.try_into()?,
self_type : None, self_type : None,
documentation : default(), documentation : default(),
kind : EntryKind::Function, kind : EntryKind::Function,
}, },
SuggestionEntryLocal {name,module,return_type,..} => SuggestionEntryLocal {name,module,return_type,..} =>
Self { Self {
name,module,return_type, name,return_type,
arguments : default(), arguments : default(),
module : module.try_into()?,
self_type : None, self_type : None,
documentation : default(), documentation : default(),
kind : EntryKind::Local, 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<String>) -> Self {
Self {name:name.into(),..self}
}
} }
impl From<language_server::types::SuggestionEntry> for Entry { impl TryFrom<language_server::types::SuggestionEntry> for Entry {
fn from(entry:language_server::types::SuggestionEntry) -> Self { type Error = failure::Error;
fn try_from(entry:language_server::types::SuggestionEntry) -> FallibleResult<Self> {
Self::from_ls_entry(entry) Self::from_ls_entry(entry)
} }
} }
@ -99,6 +121,7 @@ impl From<language_server::types::SuggestionEntry> for Entry {
/// argument names and types. /// argument names and types.
#[derive(Clone,Debug,Default)] #[derive(Clone,Debug,Default)]
pub struct SuggestionDatabase { pub struct SuggestionDatabase {
logger : Logger,
entries : RefCell<HashMap<EntryId,Rc<Entry>>>, entries : RefCell<HashMap<EntryId,Rc<Entry>>>,
version : Cell<SuggestionsDatabaseVersion>, version : Cell<SuggestionsDatabaseVersion>,
} }
@ -113,11 +136,17 @@ impl SuggestionDatabase {
/// Create a new database model from response received from the Language Server. /// Create a new database model from response received from the Language Server.
fn from_ls_response(response:language_server::response::GetSuggestionDatabase) -> Self { fn from_ls_response(response:language_server::response::GetSuggestionDatabase) -> Self {
let logger = Logger::new("SuggestionDatabase");
let mut entries = HashMap::new(); let mut entries = HashMap::new();
for entry in response.entries { for ls_entry in response.entries {
entries.insert(entry.id, Rc::new(Entry::from_ls_entry(entry.suggestion))); 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 { Self {
logger,
entries : RefCell::new(entries), entries : RefCell::new(entries),
version : Cell::new(response.current_version), version : Cell::new(response.current_version),
} }
@ -133,8 +162,11 @@ impl SuggestionDatabase {
for update in event.updates { for update in event.updates {
let mut entries = self.entries.borrow_mut(); let mut entries = self.entries.borrow_mut();
match update { match update {
Update::Add {id,entry} => entries.insert(id,Rc::new(entry.into())), Update::Add {id,entry} => match entry.try_into() {
Update::Remove {id} => entries.remove(&id), 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); self.version.set(event.current_version);
@ -163,8 +195,40 @@ impl From<language_server::response::GetSuggestionDatabase> for SuggestionDataba
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use enso_protocol::language_server::SuggestionsDatabaseEntry; 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] #[test]
fn initialize_database() { fn initialize_database() {
// Empty db // Empty db
@ -179,7 +243,7 @@ mod test {
// Non-empty db // Non-empty db
let entry = language_server::types::SuggestionEntry::SuggestionEntryAtom { let entry = language_server::types::SuggestionEntry::SuggestionEntryAtom {
name : "TextAtom".to_string(), name : "TextAtom".to_string(),
module : "TestModule".to_string(), module : "TestProject.TestModule".to_string(),
arguments : vec![], arguments : vec![],
return_type : "TestAtom".to_string(), return_type : "TestAtom".to_string(),
documentation : None documentation : None
@ -199,21 +263,21 @@ mod test {
fn applying_update() { fn applying_update() {
let entry1 = language_server::types::SuggestionEntry::SuggestionEntryAtom { let entry1 = language_server::types::SuggestionEntry::SuggestionEntryAtom {
name : "Entry1".to_string(), name : "Entry1".to_string(),
module : "TestModule".to_string(), module : "TestProject.TestModule".to_string(),
arguments : vec![], arguments : vec![],
return_type : "TestAtom".to_string(), return_type : "TestAtom".to_string(),
documentation : None documentation : None
}; };
let entry2 = language_server::types::SuggestionEntry::SuggestionEntryAtom { let entry2 = language_server::types::SuggestionEntry::SuggestionEntryAtom {
name : "Entry2".to_string(), name : "Entry2".to_string(),
module : "TestModule".to_string(), module : "TestProject.TestModule".to_string(),
arguments : vec![], arguments : vec![],
return_type : "TestAtom".to_string(), return_type : "TestAtom".to_string(),
documentation : None documentation : None
}; };
let new_entry2 = language_server::types::SuggestionEntry::SuggestionEntryAtom { let new_entry2 = language_server::types::SuggestionEntry::SuggestionEntryAtom {
name : "NewEntry2".to_string(), name : "NewEntry2".to_string(),
module : "TestModule".to_string(), module : "TestProject.TestModule".to_string(),
arguments : vec![], arguments : vec![],
return_type : "TestAtom".to_string(), return_type : "TestAtom".to_string(),
documentation : None documentation : None

View File

@ -588,7 +588,7 @@ impl GraphEditorIntegratedWithControllerModel {
// to the customised values. // to the customised values.
let project_name = self.project.name.as_ref(); let project_name = self.project.name.as_ref();
let module_name = crate::view::project::INITIAL_MODULE_NAME; 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 id = VisualizationId::new_v4();
let expression = crate::constants::SERIALIZE_TO_JSON_EXPRESSION.into(); let expression = crate::constants::SERIALIZE_TO_JSON_EXPRESSION.into();
let ast_id = self.get_controller_node_id(*node_id)?; let ast_id = self.get_controller_node_id(*node_id)?;

View File

@ -41,7 +41,7 @@ impl Index {
/// Strongly typed index of byte in String (which may differ with analogous character index, /// Strongly typed index of byte in String (which may differ with analogous character index,
/// because some chars takes more than one byte). /// 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. // and units.
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Clone,Copy,Debug,Default,Hash,PartialEq,Eq,PartialOrd,Ord,Serialize,Deserialize)] #[derive(Clone,Copy,Debug,Default,Hash,PartialEq,Eq,PartialOrd,Ord,Serialize,Deserialize)]