mirror of
https://github.com/enso-org/enso.git
synced 2024-12-20 11:21:57 +03:00
Applying suggestion in Searcher (https://github.com/enso-org/ide/pull/661)
Original commit: 010e10fe30
This commit is contained in:
parent
820f2c9553
commit
3ba80bbe65
@ -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)]
|
||||
|
@ -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"]);
|
||||
|
@ -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<model::suggestion_database::Entry>;
|
||||
|
||||
/// 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<model::suggestion_database::Entry>)
|
||||
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<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 ===
|
||||
// ===========================
|
||||
|
||||
/// 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<FragmentAddedByPickingSuggestion>,
|
||||
}
|
||||
|
||||
/// Searcher Controller.
|
||||
@ -109,6 +229,7 @@ pub struct Searcher {
|
||||
position : Immutable<TextLocation>,
|
||||
database : Rc<model::SuggestionDatabase>,
|
||||
language_server : Rc<language_server::Connection>,
|
||||
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<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.
|
||||
@ -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<String>, tags:Option<Vec<language_server::SuggestionEntryType>>) {
|
||||
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<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));
|
||||
}
|
||||
}
|
||||
|
@ -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<ByteIndex>,
|
||||
}
|
||||
|
||||
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<Item:AsRef<str>>)
|
||||
-> QualifiedName {
|
||||
-> FallibleResult<QualifiedName> {
|
||||
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<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)]
|
||||
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<String>
|
||||
}
|
||||
|
||||
@ -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<QualifiedName> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Argument>,
|
||||
@ -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<Self> {
|
||||
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<String>) -> Self {
|
||||
Self {name:name.into(),..self}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<language_server::types::SuggestionEntry> for Entry {
|
||||
fn from(entry:language_server::types::SuggestionEntry) -> Self {
|
||||
impl TryFrom<language_server::types::SuggestionEntry> for Entry {
|
||||
type Error = failure::Error;
|
||||
fn try_from(entry:language_server::types::SuggestionEntry) -> FallibleResult<Self> {
|
||||
Self::from_ls_entry(entry)
|
||||
}
|
||||
}
|
||||
@ -99,6 +121,7 @@ impl From<language_server::types::SuggestionEntry> for Entry {
|
||||
/// argument names and types.
|
||||
#[derive(Clone,Debug,Default)]
|
||||
pub struct SuggestionDatabase {
|
||||
logger : Logger,
|
||||
entries : RefCell<HashMap<EntryId,Rc<Entry>>>,
|
||||
version : Cell<SuggestionsDatabaseVersion>,
|
||||
}
|
||||
@ -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<language_server::response::GetSuggestionDatabase> 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
|
||||
|
@ -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)?;
|
||||
|
@ -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)]
|
||||
|
Loading…
Reference in New Issue
Block a user