mirror of
https://github.com/enso-org/enso.git
synced 2024-12-20 16:41:48 +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::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)]
|
||||||
|
@ -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"]);
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)?;
|
||||||
|
@ -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)]
|
||||||
|
Loading…
Reference in New Issue
Block a user