mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 18:34:03 +03:00
Cursor aware Component Browser (#5770)
Closes #5220 This PR implements the cursor-aware behavior of the CB, as described in [pivotal issue](https://www.pivotaltracker.com/n/projects/2539304/stories/183521918) https://user-images.githubusercontent.com/6566674/221807206-39f93cb4-8253-421d-a33a-33ac0aa56e54.mp4 https://user-images.githubusercontent.com/6566674/223124947-259153ca-e656-4349-87b5-47c06fd21af2.mp4 # Important Notes - The `intended_method` of the node's metadata is marked as deprecated, and all usages are removed. - It seems *all usages of Scala parser are removed from IDE*. We no longer use it to parse documentation for snippets. This is how the snippets docs look now: <img width="410" alt="Screenshot 2023-02-28 at 13 18 11" src="https://user-images.githubusercontent.com/6566674/221808028-d69c54e4-2842-4f1c-aa16-781d3f7765a1.png">
This commit is contained in:
parent
e5fef2fef3
commit
1b30a5275f
@ -73,7 +73,8 @@ impl Info {
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtain the qualified name of the module.
|
||||
/// Return the module path as [`QualifiedName`]. Returns [`Err`] if the path is not a valid
|
||||
/// module name.
|
||||
pub fn qualified_module_name(&self) -> FallibleResult<QualifiedName> {
|
||||
QualifiedName::from_all_segments(&self.module)
|
||||
}
|
||||
@ -108,12 +109,6 @@ impl Info {
|
||||
self.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
/// Return the module path as [`QualifiedName`]. Returns [`Err`] if the path is not a valid
|
||||
/// module name.
|
||||
pub fn module_qualified_name(&self) -> FallibleResult<QualifiedName> {
|
||||
QualifiedName::from_all_segments(self.module.iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Info {
|
||||
|
@ -672,7 +672,7 @@ pub trait TraversableAst: Sized {
|
||||
/// Recursively traverses AST to retrieve AST node located by given crumbs sequence.
|
||||
fn get_traversing(&self, crumbs: &[Crumb]) -> FallibleResult<&Ast>;
|
||||
|
||||
/// Get the `Ast` node corresponging to `Self`.
|
||||
/// Get the `Ast` node corresponding to `Self`.
|
||||
fn my_ast(&self) -> FallibleResult<&Ast> {
|
||||
self.get_traversing(&[])
|
||||
}
|
||||
|
@ -511,10 +511,11 @@ impl Chain {
|
||||
while !self.args.is_empty() {
|
||||
self.fold_arg()
|
||||
}
|
||||
// TODO[ao] the only case when target is none is when chain have None target and empty
|
||||
// arguments list. Such Chain cannot be generated from Ast, but someone could think that
|
||||
// this is still a valid chain. To consider returning error here.
|
||||
self.target.unwrap().arg
|
||||
if let Some(target) = self.target {
|
||||
target.arg
|
||||
} else {
|
||||
SectionSides { opr: self.operator.into() }.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// True if all operands are set, i.e. there are no section shapes in this chain.
|
||||
|
@ -51,6 +51,9 @@ pub const RAW_BLOCK_QUOTES: &str = "\"\"\"";
|
||||
/// Quotes opening block of the formatted text.
|
||||
pub const FMT_BLOCK_QUOTES: &str = "'''";
|
||||
|
||||
/// A list of possible delimiters of the text literals.
|
||||
pub const STRING_DELIMITERS: &[&str] = &[RAW_BLOCK_QUOTES, FMT_BLOCK_QUOTES, "\"", "'"];
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
|
@ -26,7 +26,6 @@ use engine_protocol::language_server;
|
||||
use parser::Parser;
|
||||
use span_tree::action::Action;
|
||||
use span_tree::action::Actions;
|
||||
use span_tree::generate::context::CalledMethodInfo;
|
||||
use span_tree::generate::Context as SpanTreeContext;
|
||||
use span_tree::SpanTree;
|
||||
|
||||
@ -790,6 +789,20 @@ impl Handle {
|
||||
module::locate(&module_ast, &self.id)
|
||||
}
|
||||
|
||||
/// The span of the definition of this graph in the module's AST.
|
||||
pub fn definition_span(&self) -> FallibleResult<enso_text::Range<enso_text::Byte>> {
|
||||
let def = self.definition()?;
|
||||
self.module.ast().range_of_descendant_at(&def.crumbs)
|
||||
}
|
||||
|
||||
/// The location of the last byte of the definition of this graph in the module's AST.
|
||||
pub fn definition_end_location(&self) -> FallibleResult<enso_text::Location<enso_text::Byte>> {
|
||||
let module_ast = self.module.ast();
|
||||
let module_repr: enso_text::Rope = module_ast.repr().into();
|
||||
let def_span = self.definition_span()?;
|
||||
Ok(module_repr.offset_to_location_snapped(def_span.end))
|
||||
}
|
||||
|
||||
/// Updates the AST of the definition of this graph.
|
||||
#[profile(Debug)]
|
||||
pub fn update_definition_ast<F>(&self, f: F) -> FallibleResult
|
||||
@ -1023,24 +1036,6 @@ impl Handle {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === Span Tree Context ===
|
||||
|
||||
/// Span Tree generation context for a graph that does not know about execution.
|
||||
///
|
||||
/// It just applies the information from the metadata.
|
||||
impl span_tree::generate::Context for Handle {
|
||||
fn call_info(&self, id: node::Id, name: Option<&str>) -> Option<CalledMethodInfo> {
|
||||
let db = &self.suggestion_db;
|
||||
let metadata = self.module.node_metadata(id).ok()?;
|
||||
let db_entry = db.lookup_method(metadata.intended_method?)?;
|
||||
// If the name is different than intended method than apparently it is not intended anymore
|
||||
// and should be ignored.
|
||||
let matching = if let Some(name) = name { name == db_entry.name } else { true };
|
||||
matching.then(|| db_entry.invocation_info(db, &self.parser))
|
||||
}
|
||||
}
|
||||
|
||||
impl model::undo_redo::Aware for Handle {
|
||||
fn undo_redo_repository(&self) -> Rc<model::undo_redo::Repository> {
|
||||
self.module.undo_redo_repository()
|
||||
@ -1069,15 +1064,11 @@ pub mod tests {
|
||||
use engine_protocol::language_server::MethodPointer;
|
||||
use enso_text::index::*;
|
||||
use parser::Parser;
|
||||
use span_tree::generate::MockContext;
|
||||
|
||||
|
||||
|
||||
/// Returns information about all the connections between graph's nodes.
|
||||
///
|
||||
/// Will use `self` as the context for span tree generation.
|
||||
pub fn connections(graph: &Handle) -> FallibleResult<Connections> {
|
||||
graph.connections(graph)
|
||||
graph.connections(&span_tree::generate::context::Empty)
|
||||
}
|
||||
|
||||
/// All the data needed to set up and run the graph controller in mock environment.
|
||||
@ -1253,45 +1244,6 @@ main =
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_tree_context_handling_metadata_and_name() {
|
||||
let entry = crate::test::mock::data::suggestion_entry_foo();
|
||||
let mut test = Fixture::set_up();
|
||||
test.data.suggestions.insert(0, entry.clone());
|
||||
test.data.code = "main = bar".to_owned();
|
||||
test.run(|graph| async move {
|
||||
let nodes = graph.nodes().unwrap();
|
||||
assert_eq!(nodes.len(), 1);
|
||||
let id = nodes[0].info.id();
|
||||
graph
|
||||
.module
|
||||
.set_node_metadata(id, NodeMetadata {
|
||||
intended_method: entry.method_id(),
|
||||
..default()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let get_invocation_info = || {
|
||||
let node = &graph.nodes().unwrap()[0];
|
||||
assert_eq!(node.info.id(), id);
|
||||
let expression = node.info.expression().repr();
|
||||
graph.call_info(id, Some(expression.as_str()))
|
||||
};
|
||||
|
||||
// Now node is `bar` while the intended method is `foo`.
|
||||
// No invocation should be reported, as the name is mismatched.
|
||||
assert!(get_invocation_info().is_none());
|
||||
|
||||
// Now the name should be good and we should the information about node being a call.
|
||||
graph.set_expression(id, &entry.name).unwrap();
|
||||
crate::test::assert_call_info(get_invocation_info().unwrap(), &entry);
|
||||
|
||||
// Now we remove metadata, so the information is no more.
|
||||
graph.module.remove_node_metadata(id).unwrap();
|
||||
assert!(get_invocation_info().is_none());
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_controller_used_names_in_inline_def() {
|
||||
let mut test = Fixture::set_up();
|
||||
@ -1751,7 +1703,6 @@ main =
|
||||
struct Case {
|
||||
dest_node_expr: &'static str,
|
||||
dest_node_expected: &'static str,
|
||||
info: Option<CalledMethodInfo>,
|
||||
}
|
||||
|
||||
|
||||
@ -1763,57 +1714,28 @@ main =
|
||||
let expected = format!("{}{}", MAIN_PREFIX, self.dest_node_expected);
|
||||
let this = self.clone();
|
||||
test.run(|graph| async move {
|
||||
let error_message = format!("{this:?}");
|
||||
let ctx = match this.info.clone() {
|
||||
Some(info) => {
|
||||
let nodes = graph.nodes().expect(&error_message);
|
||||
let dest_node_id = nodes.last().expect(&error_message).id();
|
||||
MockContext::new_single(dest_node_id, info)
|
||||
}
|
||||
None => MockContext::default(),
|
||||
};
|
||||
let connections = graph.connections(&ctx).expect(&error_message);
|
||||
let connection = connections.connections.first().expect(&error_message);
|
||||
graph.disconnect(connection, &ctx).expect(&error_message);
|
||||
let new_main = graph.definition().expect(&error_message).ast.repr();
|
||||
assert_eq!(new_main, expected, "{error_message}");
|
||||
let connections = connections(&graph).unwrap();
|
||||
let connection = connections.connections.first().unwrap();
|
||||
graph.disconnect(connection, &span_tree::generate::context::Empty).unwrap();
|
||||
let new_main = graph.definition().unwrap().ast.repr();
|
||||
assert_eq!(new_main, expected, "Case {this:?}");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let info = || {
|
||||
Some(CalledMethodInfo {
|
||||
parameters: vec![
|
||||
span_tree::ArgumentInfo::named("arg1"),
|
||||
span_tree::ArgumentInfo::named("arg2"),
|
||||
span_tree::ArgumentInfo::named("arg3"),
|
||||
],
|
||||
..default()
|
||||
})
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
let cases = &[
|
||||
Case { info: None, dest_node_expr: "var + a", dest_node_expected: "_ + a" },
|
||||
Case { info: None, dest_node_expr: "a + var", dest_node_expected: "a + _" },
|
||||
Case { info: None, dest_node_expr: "var + b + c", dest_node_expected: "b + c" },
|
||||
Case { info: None, dest_node_expr: "a + var + c", dest_node_expected: "a + c" },
|
||||
Case { info: None, dest_node_expr: "a + b + var", dest_node_expected: "a + b" },
|
||||
Case { info: None, dest_node_expr: "foo var", dest_node_expected: "foo _" },
|
||||
Case { info: None, dest_node_expr: "foo var a", dest_node_expected: "foo a" },
|
||||
Case { info: None, dest_node_expr: "foo a var", dest_node_expected: "foo a" },
|
||||
Case { info: info(), dest_node_expr: "foo var", dest_node_expected: "foo" },
|
||||
Case { info: info(), dest_node_expr: "foo var a", dest_node_expected: "foo arg2=a" },
|
||||
Case { info: info(), dest_node_expr: "foo a var", dest_node_expected: "foo a" },
|
||||
Case { info: info(), dest_node_expr: "foo arg2=var a", dest_node_expected: "foo a" },
|
||||
Case { info: info(), dest_node_expr: "foo arg1=var a", dest_node_expected: "foo arg2=a" },
|
||||
Case { dest_node_expr: "var + a", dest_node_expected: "_ + a" },
|
||||
Case { dest_node_expr: "a + var", dest_node_expected: "a + _" },
|
||||
Case { dest_node_expr: "var + b + c", dest_node_expected: "b + c" },
|
||||
Case { dest_node_expr: "a + var + c", dest_node_expected: "a + c" },
|
||||
Case { dest_node_expr: "a + b + var", dest_node_expected: "a + b" },
|
||||
Case { dest_node_expr: "foo var", dest_node_expected: "foo _" },
|
||||
Case { dest_node_expr: "foo var a", dest_node_expected: "foo a" },
|
||||
Case { dest_node_expr: "foo a var", dest_node_expected: "foo a" },
|
||||
Case { dest_node_expr: "foo arg2=var a", dest_node_expected: "foo a" },
|
||||
Case { dest_node_expr: "foo arg1=var a", dest_node_expected: "foo a" },
|
||||
Case { dest_node_expr: "foo arg2=var a c", dest_node_expected: "foo a c" },
|
||||
Case {
|
||||
info: info(),
|
||||
dest_node_expr: "foo arg2=var a c",
|
||||
dest_node_expected: "foo a arg3=c"
|
||||
},
|
||||
Case {
|
||||
info: None,
|
||||
dest_node_expr: "f\n bar a var",
|
||||
dest_node_expected: "f\n bar a _",
|
||||
},
|
||||
|
@ -75,7 +75,6 @@ pub enum Notification {
|
||||
/// Handle providing executed graph controller interface.
|
||||
#[derive(Clone, CloneRef, Debug)]
|
||||
pub struct Handle {
|
||||
#[allow(missing_docs)]
|
||||
/// A handle to basic graph operations.
|
||||
graph: Rc<RefCell<controller::Graph>>,
|
||||
/// Execution Context handle, its call stack top contains `graph`'s definition.
|
||||
@ -343,30 +342,26 @@ impl Handle {
|
||||
/// Span Tree generation context for a graph that does not know about execution.
|
||||
/// Provides information based on computed value registry, using metadata as a fallback.
|
||||
impl Context for Handle {
|
||||
fn call_info(&self, id: ast::Id, name: Option<&str>) -> Option<CalledMethodInfo> {
|
||||
let lookup_registry = || {
|
||||
let info = self.computed_value_info_registry().get(&id)?;
|
||||
let method_call = info.method_call.as_ref()?;
|
||||
let suggestion_db = self.project.suggestion_db();
|
||||
let maybe_entry = suggestion_db.lookup_by_method_pointer(method_call).map(|e| {
|
||||
let invocation_info = e.invocation_info(&suggestion_db, &self.parser());
|
||||
invocation_info.with_called_on_type(false)
|
||||
});
|
||||
fn call_info(&self, id: ast::Id, _name: Option<&str>) -> Option<CalledMethodInfo> {
|
||||
let info = self.computed_value_info_registry().get(&id)?;
|
||||
let method_call = info.method_call.as_ref()?;
|
||||
let suggestion_db = self.project.suggestion_db();
|
||||
let maybe_entry = suggestion_db.lookup_by_method_pointer(method_call).map(|e| {
|
||||
let invocation_info = e.invocation_info(&suggestion_db, &self.parser());
|
||||
invocation_info.with_called_on_type(false)
|
||||
});
|
||||
|
||||
// When the entry was not resolved but the `defined_on_type` has a `.type` suffix,
|
||||
// try resolving it again with the suffix stripped. This indicates that a method was
|
||||
// called on type, either because it is a static method, or because it uses qualified
|
||||
// method syntax.
|
||||
maybe_entry.or_else(|| {
|
||||
let defined_on_type = method_call.defined_on_type.strip_suffix(".type")?.to_owned();
|
||||
let method_call = MethodPointer { defined_on_type, ..method_call.clone() };
|
||||
let entry = suggestion_db.lookup_by_method_pointer(&method_call)?;
|
||||
let invocation_info = entry.invocation_info(&suggestion_db, &self.parser());
|
||||
Some(invocation_info.with_called_on_type(true))
|
||||
})
|
||||
};
|
||||
let fallback = || self.graph.borrow().call_info(id, name);
|
||||
lookup_registry().or_else(fallback)
|
||||
// When the entry was not resolved but the `defined_on_type` has a `.type` suffix,
|
||||
// try resolving it again with the suffix stripped. This indicates that a method was
|
||||
// called on type, either because it is a static method, or because it uses qualified
|
||||
// method syntax.
|
||||
maybe_entry.or_else(|| {
|
||||
let defined_on_type = method_call.defined_on_type.strip_suffix(".type")?.to_owned();
|
||||
let method_call = MethodPointer { defined_on_type, ..method_call.clone() };
|
||||
let entry = suggestion_db.lookup_by_method_pointer(&method_call)?;
|
||||
let invocation_info = entry.invocation_info(&suggestion_db, &self.parser());
|
||||
Some(invocation_info.with_called_on_type(true))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -387,10 +382,8 @@ pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::model::execution_context::ExpressionId;
|
||||
use crate::model::module::NodeMetadata;
|
||||
use crate::test;
|
||||
|
||||
use engine_protocol::language_server::types::test::value_update_with_method_ptr;
|
||||
use engine_protocol::language_server::types::test::value_update_with_type;
|
||||
use wasm_bindgen_test::wasm_bindgen_test;
|
||||
use wasm_bindgen_test::wasm_bindgen_test_configure;
|
||||
@ -477,38 +470,4 @@ pub mod tests {
|
||||
|
||||
notifications.expect_pending();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn span_tree_context() {
|
||||
use crate::test::assert_call_info;
|
||||
use crate::test::mock;
|
||||
|
||||
let mut data = mock::Unified::new();
|
||||
let entry1 = data.suggestions.get(&1).unwrap().clone();
|
||||
let entry2 = data.suggestions.get(&2).unwrap().clone();
|
||||
data.set_inline_code(&entry1.name);
|
||||
|
||||
let mock::Fixture { graph, executed_graph, module, .. } = &data.fixture();
|
||||
let id = graph.nodes().unwrap()[0].info.id();
|
||||
let get_invocation_info = || executed_graph.call_info(id, Some(&entry1.name));
|
||||
assert!(get_invocation_info().is_none());
|
||||
|
||||
// Check that if we set metadata, executed graph can see this info.
|
||||
module
|
||||
.set_node_metadata(id, NodeMetadata {
|
||||
intended_method: entry1.method_id(),
|
||||
..default()
|
||||
})
|
||||
.unwrap();
|
||||
let info = get_invocation_info().unwrap();
|
||||
assert_call_info(info, &entry1);
|
||||
|
||||
// Now send update that expression actually was computed to be a call to the second
|
||||
// suggestion entry and check that executed graph provides this info over the metadata one.
|
||||
let method_pointer = entry2.clone().try_into().unwrap();
|
||||
let update = value_update_with_method_ptr(id, method_pointer);
|
||||
executed_graph.computed_value_info_registry().apply_updates(vec![update]);
|
||||
let info = get_invocation_info().unwrap();
|
||||
assert_call_info(info, &entry2);
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -2,10 +2,7 @@
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::model::SuggestionDatabase;
|
||||
|
||||
use double_representation::module::MethodId;
|
||||
use double_representation::name::QualifiedNameRef;
|
||||
use ordered_float::OrderedFloat;
|
||||
|
||||
|
||||
@ -21,7 +18,7 @@ pub mod hardcoded;
|
||||
// === Action ===
|
||||
// ==============
|
||||
|
||||
#[derive(Clone, CloneRef, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, CloneRef, Debug, PartialEq)]
|
||||
/// Suggestion for code completion: possible functions, arguments, etc.
|
||||
pub enum Suggestion {
|
||||
/// The suggestion from Suggestion Database received from the Engine.
|
||||
@ -35,18 +32,7 @@ impl Suggestion {
|
||||
pub fn code_to_insert(&self, generate_this: bool) -> Cow<str> {
|
||||
match self {
|
||||
Suggestion::FromDatabase(s) => s.code_to_insert(generate_this),
|
||||
Suggestion::Hardcoded(s) => s.code.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn required_imports(
|
||||
&self,
|
||||
db: &SuggestionDatabase,
|
||||
current_module: QualifiedNameRef,
|
||||
) -> impl IntoIterator<Item = model::suggestion_database::entry::Import> {
|
||||
match self {
|
||||
Suggestion::FromDatabase(s) => s.required_imports(db, current_module),
|
||||
Suggestion::Hardcoded(_) => default(),
|
||||
Suggestion::Hardcoded(s) => s.code.as_str().into(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +75,7 @@ pub enum ProjectManagement {
|
||||
}
|
||||
|
||||
/// A single action on the Searcher list. See also `controller::searcher::Searcher` docs.
|
||||
#[derive(Clone, CloneRef, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, CloneRef, Debug, PartialEq)]
|
||||
pub enum Action {
|
||||
/// Add to the searcher input a suggested code and commit editing (new node is inserted or
|
||||
/// existing is modified). This action can be also used to complete searcher input without
|
||||
@ -215,7 +201,7 @@ impl Eq for MatchInfo {}
|
||||
|
||||
/// The single list entry.
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ListEntry {
|
||||
pub category: CategoryId,
|
||||
pub match_info: MatchInfo,
|
||||
|
@ -7,6 +7,7 @@ use crate::prelude::*;
|
||||
use crate::model::suggestion_database;
|
||||
|
||||
use controller::searcher::action::MatchKind;
|
||||
use controller::searcher::Filter;
|
||||
use convert_case::Case;
|
||||
use convert_case::Casing;
|
||||
use double_representation::name::QualifiedName;
|
||||
@ -145,7 +146,7 @@ impl Component {
|
||||
pub fn name(&self) -> &str {
|
||||
match &self.data {
|
||||
Data::FromDatabase { entry, .. } => entry.name.as_str(),
|
||||
Data::Virtual { snippet } => snippet.name,
|
||||
Data::Virtual { snippet } => snippet.name.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,13 +175,13 @@ impl Component {
|
||||
/// Update matching info.
|
||||
///
|
||||
/// It should be called each time the filtering pattern changes.
|
||||
pub fn update_matching_info(&self, pattern: impl Str) {
|
||||
pub fn update_matching_info(&self, filter: Filter) {
|
||||
// Match the input pattern to the component label.
|
||||
let label = self.to_string();
|
||||
let label_matches = fuzzly::matches(&label, pattern.as_ref());
|
||||
let label_matches = fuzzly::matches(&label, filter.pattern.clone_ref());
|
||||
let label_subsequence = label_matches.and_option_from(|| {
|
||||
let metric = fuzzly::metric::default();
|
||||
fuzzly::find_best_subsequence(label, pattern.as_ref(), metric)
|
||||
fuzzly::find_best_subsequence(label, filter.pattern.clone_ref(), metric)
|
||||
});
|
||||
let label_match_info = label_subsequence
|
||||
.map(|subsequence| MatchInfo::Matches { subsequence, kind: MatchKind::Label });
|
||||
@ -190,10 +191,10 @@ impl Component {
|
||||
Data::FromDatabase { entry, .. } => entry.code_to_insert(true).to_string(),
|
||||
Data::Virtual { snippet } => snippet.code.to_string(),
|
||||
};
|
||||
let code_matches = fuzzly::matches(&code, pattern.as_ref());
|
||||
let code_matches = fuzzly::matches(&code, filter.pattern.clone_ref());
|
||||
let code_subsequence = code_matches.and_option_from(|| {
|
||||
let metric = fuzzly::metric::default();
|
||||
fuzzly::find_best_subsequence(code, pattern.as_ref(), metric)
|
||||
fuzzly::find_best_subsequence(code, filter.pattern.clone_ref(), metric)
|
||||
});
|
||||
let code_match_info = code_subsequence.map(|subsequence| {
|
||||
let subsequence = fuzzly::Subsequence { indices: Vec::new(), ..subsequence };
|
||||
@ -202,9 +203,10 @@ impl Component {
|
||||
|
||||
// Match the input pattern to an entry's aliases and select the best alias match.
|
||||
let alias_matches = self.aliases().filter_map(|alias| {
|
||||
if fuzzly::matches(alias, pattern.as_ref()) {
|
||||
if fuzzly::matches(alias, filter.pattern.clone_ref()) {
|
||||
let metric = fuzzly::metric::default();
|
||||
let subsequence = fuzzly::find_best_subsequence(alias, pattern.as_ref(), metric);
|
||||
let subsequence =
|
||||
fuzzly::find_best_subsequence(alias, filter.pattern.clone_ref(), metric);
|
||||
subsequence.map(|subsequence| (subsequence, alias))
|
||||
} else {
|
||||
None
|
||||
@ -223,6 +225,18 @@ impl Component {
|
||||
let match_info_iter = [alias_match_info, code_match_info, label_match_info].into_iter();
|
||||
let best_match_info = match_info_iter.flatten().max_by(|lhs, rhs| lhs.cmp(rhs));
|
||||
*self.match_info.borrow_mut() = best_match_info.unwrap_or(MatchInfo::DoesNotMatch);
|
||||
|
||||
// Filter out components with FQN not matching the context.
|
||||
if let Some(context) = filter.context {
|
||||
if let Data::FromDatabase { entry, .. } = &self.data {
|
||||
if !entry.qualified_name().to_string().contains(context.as_str()) {
|
||||
*self.match_info.borrow_mut() = MatchInfo::DoesNotMatch;
|
||||
}
|
||||
} else {
|
||||
// Remove virtual entries if the context is present.
|
||||
*self.match_info.borrow_mut() = MatchInfo::DoesNotMatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether the component contains the "PRIVATE" tag.
|
||||
@ -382,22 +396,22 @@ impl List {
|
||||
}
|
||||
|
||||
/// Update matching info in all components according to the new filtering pattern.
|
||||
pub fn update_filtering(&self, pattern: impl AsRef<str>) {
|
||||
let pattern = pattern.as_ref();
|
||||
pub fn update_filtering(&self, filter: Filter) {
|
||||
let pattern = &filter.pattern;
|
||||
for component in &*self.all_components {
|
||||
component.update_matching_info(pattern)
|
||||
component.update_matching_info(filter.clone_ref())
|
||||
}
|
||||
let pattern_not_empty = !pattern.is_empty();
|
||||
let filtering_enabled = !pattern.is_empty() || filter.context.is_some();
|
||||
let submodules_order =
|
||||
if pattern_not_empty { Order::ByMatch } else { Order::ByNameNonModulesThenModules };
|
||||
let favorites_order = if pattern_not_empty { Order::ByMatch } else { Order::Initial };
|
||||
if filtering_enabled { Order::ByMatch } else { Order::ByNameNonModulesThenModules };
|
||||
let favorites_order = if filtering_enabled { Order::ByMatch } else { Order::Initial };
|
||||
for group in self.all_groups_not_in_favorites() {
|
||||
group.update_match_info_and_sorting(submodules_order);
|
||||
}
|
||||
for group in self.favorites.iter() {
|
||||
group.update_match_info_and_sorting(favorites_order);
|
||||
}
|
||||
self.filtered.set(pattern_not_empty);
|
||||
self.filtered.set(filtering_enabled);
|
||||
}
|
||||
|
||||
/// All groups from [`List`] without the groups found in [`List::favorites`].
|
||||
@ -517,7 +531,7 @@ pub(crate) mod tests {
|
||||
builder.extend_list_and_allow_favorites_with_ids(&suggestion_db, 0..=4);
|
||||
let list = builder.build();
|
||||
|
||||
list.update_filtering("fu");
|
||||
list.update_filtering(Filter { pattern: "fu".into(), context: None });
|
||||
let match_infos = list.top_modules().next().unwrap()[0]
|
||||
.entries
|
||||
.borrow()
|
||||
@ -529,22 +543,22 @@ pub(crate) mod tests {
|
||||
assert_ids_of_matches_entries(&list.favorites[0], &[4, 2]);
|
||||
assert_ids_of_matches_entries(&list.local_scope, &[2]);
|
||||
|
||||
list.update_filtering("x");
|
||||
list.update_filtering(Filter { pattern: "x".into(), context: None });
|
||||
assert_ids_of_matches_entries(&list.top_modules().next().unwrap()[0], &[4]);
|
||||
assert_ids_of_matches_entries(&list.favorites[0], &[4]);
|
||||
assert_ids_of_matches_entries(&list.local_scope, &[]);
|
||||
|
||||
list.update_filtering("Sub");
|
||||
list.update_filtering(Filter { pattern: "Sub".into(), context: None });
|
||||
assert_ids_of_matches_entries(&list.top_modules().next().unwrap()[0], &[3]);
|
||||
assert_ids_of_matches_entries(&list.favorites[0], &[]);
|
||||
assert_ids_of_matches_entries(&list.local_scope, &[]);
|
||||
|
||||
list.update_filtering("y");
|
||||
list.update_filtering(Filter { pattern: "y".into(), context: None });
|
||||
assert_ids_of_matches_entries(&list.top_modules().next().unwrap()[0], &[]);
|
||||
assert_ids_of_matches_entries(&list.favorites[0], &[]);
|
||||
assert_ids_of_matches_entries(&list.local_scope, &[]);
|
||||
|
||||
list.update_filtering("");
|
||||
list.update_filtering(Filter { pattern: "".into(), context: None });
|
||||
assert_ids_of_matches_entries(&list.top_modules().next().unwrap()[0], &[2, 3]);
|
||||
assert_ids_of_matches_entries(&list.favorites[0], &[4, 2]);
|
||||
assert_ids_of_matches_entries(&list.local_scope, &[2]);
|
||||
|
@ -580,7 +580,7 @@ mod tests {
|
||||
components: vec![qn_of_db_entry_1.clone()],
|
||||
}];
|
||||
builder.set_grouping_and_order_of_favorites(&db, &groups);
|
||||
let snippet = component::hardcoded::Snippet { name: "test snippet", ..default() };
|
||||
let snippet = component::hardcoded::Snippet { name: "test snippet".into(), ..default() };
|
||||
let snippet_iter = std::iter::once(Rc::new(snippet));
|
||||
builder.insert_virtual_components_in_favorites_group(GROUP_NAME, project, snippet_iter);
|
||||
builder.extend_list_and_allow_favorites_with_ids(&db, std::iter::once(1));
|
||||
@ -607,7 +607,7 @@ mod tests {
|
||||
components: vec![qn_of_db_entry_1.clone()],
|
||||
}];
|
||||
builder.set_grouping_and_order_of_favorites(&db, &groups);
|
||||
let snippet = component::hardcoded::Snippet { name: "test snippet", ..default() };
|
||||
let snippet = component::hardcoded::Snippet { name: "test snippet".into(), ..default() };
|
||||
let snippet_iter = std::iter::once(Rc::new(snippet));
|
||||
const GROUP_2_NAME: &str = "Group 2";
|
||||
builder.insert_virtual_components_in_favorites_group(GROUP_2_NAME, project, snippet_iter);
|
||||
|
@ -7,8 +7,11 @@
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::controller::searcher::input;
|
||||
|
||||
use double_representation::name::QualifiedName;
|
||||
use enso_doc_parser::DocSection;
|
||||
use enso_suggestion_database::documentation_ir::EntryDocumentation;
|
||||
use enso_suggestion_database::SuggestionDatabase;
|
||||
use ide_view::component_browser::component_list_panel::grid::entry::icon::Id as IconId;
|
||||
|
||||
|
||||
@ -20,6 +23,10 @@ use ide_view::component_browser::component_list_panel::grid::entry::icon::Id as
|
||||
/// Name of the favorites component group in the `Standard.Base` library where virtual components
|
||||
/// created from the [`INPUT_SNIPPETS`] should be added.
|
||||
pub const INPUT_GROUP_NAME: &str = "Input";
|
||||
/// Qualified name of the `Text` type.
|
||||
const TEXT_ENTRY: &str = "Standard.Base.Main.Data.Text.Text";
|
||||
/// Qualified name of the `Number` type.
|
||||
const NUMBER_ENTRY: &str = "Standard.Base.Main.Data.Numbers.Number";
|
||||
|
||||
thread_local! {
|
||||
/// Snippets describing virtual components displayed in the [`INPUT_GROUP_NAME`] favorites
|
||||
@ -29,7 +36,7 @@ thread_local! {
|
||||
pub static INPUT_SNIPPETS: Vec<Rc<Snippet>> = vec![
|
||||
Snippet::new("text input", "\"\"", IconId::TextInput)
|
||||
.with_return_types(["Standard.Base.Data.Text.Text"])
|
||||
.with_documentation(
|
||||
.with_documentation_str(
|
||||
"A text input node.\n\n\
|
||||
An empty text. The value can be edited and used as an input for other nodes.",
|
||||
)
|
||||
@ -40,7 +47,7 @@ thread_local! {
|
||||
"Standard.Base.Data.Numbers.Decimal",
|
||||
"Standard.Base.Data.Numbers.Integer",
|
||||
])
|
||||
.with_documentation(
|
||||
.with_documentation_str(
|
||||
"A number input node.\n\n\
|
||||
A zero number. The value can be edited and used as an input for other nodes.",
|
||||
)
|
||||
@ -49,39 +56,24 @@ thread_local! {
|
||||
}
|
||||
|
||||
|
||||
// === Filtering by Return Type ===
|
||||
|
||||
/// Return a filtered copy of [`INPUT_SNIPPETS`] containing only snippets which have at least one
|
||||
/// of their return types on the given list of return types.
|
||||
pub fn input_snippets_with_matching_return_type(
|
||||
return_types: impl IntoIterator<Item = QualifiedName>,
|
||||
) -> Vec<Rc<Snippet>> {
|
||||
let rt_set: HashSet<_> = return_types.into_iter().collect();
|
||||
let rt_of_snippet_is_in_set =
|
||||
|s: &&Rc<Snippet>| s.return_types.iter().any(|rt| rt_set.contains(rt));
|
||||
INPUT_SNIPPETS
|
||||
.with(|snippets| snippets.iter().filter(rt_of_snippet_is_in_set).cloned().collect_vec())
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===============
|
||||
// === Snippet ===
|
||||
// ===============
|
||||
|
||||
/// A hardcoded snippet of code with a description and syntactic metadata.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Snippet {
|
||||
/// The name displayed in the [Component Browser](crate::controller::searcher).
|
||||
pub name: &'static str,
|
||||
pub name: ImString,
|
||||
/// The code inserted when picking the snippet.
|
||||
pub code: &'static str,
|
||||
pub code: ImString,
|
||||
/// A list of types that the return value of this snippet's code typechecks as. Used by the
|
||||
/// [Component Browser](crate::controller::searcher) to decide whether to display the
|
||||
/// snippet when filtering components by return type.
|
||||
pub return_types: Vec<QualifiedName>,
|
||||
/// The documentation bound to the snippet.
|
||||
pub documentation: Option<Vec<DocSection>>,
|
||||
pub documentation: Option<EntryDocumentation>,
|
||||
/// The ID of the icon bound to this snippet's entry in the [Component
|
||||
/// Browser](crate::controller::searcher).
|
||||
pub icon: IconId,
|
||||
@ -89,8 +81,31 @@ pub struct Snippet {
|
||||
|
||||
impl Snippet {
|
||||
/// Construct a hardcoded snippet with given name, code, and icon.
|
||||
fn new(name: &'static str, code: &'static str, icon: IconId) -> Self {
|
||||
Self { name, code, icon, ..default() }
|
||||
fn new(name: &str, code: &str, icon: IconId) -> Self {
|
||||
Self { name: name.into(), code: code.into(), icon, ..default() }
|
||||
}
|
||||
|
||||
/// Construct a hardcoded snippet for a single literal.
|
||||
pub fn from_literal(literal: &input::Literal, db: &SuggestionDatabase) -> Self {
|
||||
use input::Literal::*;
|
||||
let text_repr = literal.to_string();
|
||||
let snippet = match literal {
|
||||
Text { closing_delimiter: missing_quotation_mark, .. } => {
|
||||
let missing_quote = missing_quotation_mark.as_ref();
|
||||
let code = &missing_quote.map(ToString::to_string).unwrap_or_default();
|
||||
Self::new(&text_repr, code, IconId::TextInput)
|
||||
}
|
||||
Number(_) => Self::new(&text_repr, "", IconId::NumberInput),
|
||||
};
|
||||
let entry_path = match literal {
|
||||
Text { .. } => TEXT_ENTRY,
|
||||
Number(_) => NUMBER_ENTRY,
|
||||
};
|
||||
// `unwrap()` is safe here, because we test the validity of `entry_path` in the tests.
|
||||
let qualified_name = QualifiedName::from_text(entry_path).unwrap();
|
||||
let entry = db.lookup_by_qualified_name(&qualified_name);
|
||||
let docs = entry.map(|(entry_id, _)| db.documentation_for_entry(entry_id));
|
||||
snippet.with_documentation(docs.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Returns a modified suggestion with [`Snippet::return_types`] field set. This method is only
|
||||
@ -104,8 +119,34 @@ impl Snippet {
|
||||
|
||||
/// Returns a modified suggestion with the [`Snippet::documentation`] field set. This method
|
||||
/// is only intended to be used when defining hardcoded suggestions.
|
||||
fn with_documentation(mut self, documentation: &str) -> Self {
|
||||
self.documentation = Some(enso_doc_parser::parse(documentation));
|
||||
fn with_documentation_str(mut self, documentation: &str) -> Self {
|
||||
let docs = EntryDocumentation::builtin(&enso_doc_parser::parse(documentation));
|
||||
self.documentation = Some(docs);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a modified suggestion with the [`Snippet::documentation`] field set.
|
||||
fn with_documentation(mut self, documentation: EntryDocumentation) -> Self {
|
||||
self.documentation = Some(documentation);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Tests ===
|
||||
// =============
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Test that the qualified names used for hardcoded snippets can be constructed. We don't check
|
||||
/// if the entries are actually available in the suggestion database.
|
||||
#[test]
|
||||
fn test_qualified_names_construction() {
|
||||
QualifiedName::from_text(TEXT_ENTRY).unwrap();
|
||||
QualifiedName::from_text(NUMBER_ENTRY).unwrap();
|
||||
}
|
||||
}
|
||||
|
700
app/gui/src/controller/searcher/input.rs
Normal file
700
app/gui/src/controller/searcher/input.rs
Normal file
@ -0,0 +1,700 @@
|
||||
//! A module containing structures keeping and interpreting the input of the searcher controller.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::controller::searcher::action;
|
||||
use crate::controller::searcher::Filter;
|
||||
use crate::controller::searcher::RequiredImport;
|
||||
|
||||
use ast::HasTokens;
|
||||
use double_representation::name::QualifiedName;
|
||||
use enso_text as text;
|
||||
use parser::Parser;
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Errors ===
|
||||
// ==============
|
||||
|
||||
#[derive(Clone, Debug, Fail)]
|
||||
#[allow(missing_docs)]
|
||||
#[fail(display = "Not a char boundary: index {} at '{}'.", index, string)]
|
||||
pub struct NotACharBoundary {
|
||||
index: usize,
|
||||
string: String,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===============
|
||||
// === Literal ===
|
||||
// ===============
|
||||
|
||||
/// A text or number literal.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Literal {
|
||||
/// A literal of a number type.
|
||||
Number(ast::known::Number),
|
||||
/// A literal of a text type.
|
||||
Text {
|
||||
/// The string representation of the finished text literal. Includes the closing
|
||||
/// delimiter even if the user didn't type it yet.
|
||||
text: ImString,
|
||||
/// The missing closing delimiter if the user didn't type it yet.
|
||||
closing_delimiter: Option<&'static str>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for Literal {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Text { text, .. } => write!(f, "{text}"),
|
||||
Self::Number(number) => write!(f, "{}", number.repr()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Literal {
|
||||
/// Construct a string literal. Returns `None` if the given string is not starting with a
|
||||
/// quotation mark.
|
||||
fn try_new_text(text: &str) -> Option<Self> {
|
||||
for delimiter in ast::repr::STRING_DELIMITERS {
|
||||
if text.starts_with(delimiter) {
|
||||
let delimiter_only = text == *delimiter;
|
||||
let closing_delimiter = if text.ends_with(delimiter) && !delimiter_only {
|
||||
None
|
||||
} else {
|
||||
Some(*delimiter)
|
||||
};
|
||||
let text = if let Some(delimiter) = closing_delimiter {
|
||||
let mut text = text.to_owned();
|
||||
text.push_str(delimiter);
|
||||
text.into()
|
||||
} else {
|
||||
text.into()
|
||||
};
|
||||
return Some(Self::Text { text, closing_delimiter });
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === AstWithRange ===
|
||||
// ====================
|
||||
|
||||
/// A helper structure binding given AST node to the range of Component Browser input.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AstWithRange<A = Ast> {
|
||||
ast: A,
|
||||
range: text::Range<text::Byte>,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =========================
|
||||
// === AstPositionFinder ===
|
||||
// =========================
|
||||
|
||||
/// A structure locating what Ast overlaps with given text offset.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct AstAtPositionFinder {
|
||||
current_offset: text::Byte,
|
||||
target_offset: text::Byte,
|
||||
result: Vec<AstWithRange>,
|
||||
}
|
||||
|
||||
impl AstAtPositionFinder {
|
||||
fn new(target_offset: text::Byte) -> Self {
|
||||
Self { target_offset, ..default() }
|
||||
}
|
||||
|
||||
/// Find all AST nodes containing the character in the `target` offset. The most nested AST
|
||||
/// nodes will be last on the returned list.
|
||||
fn find(ast: &Ast, target: text::Byte) -> Vec<AstWithRange> {
|
||||
let mut finder = Self::new(target);
|
||||
ast.feed_to(&mut finder);
|
||||
finder.result
|
||||
}
|
||||
}
|
||||
|
||||
impl ast::TokenConsumer for AstAtPositionFinder {
|
||||
fn feed(&mut self, token: ast::Token) {
|
||||
match token {
|
||||
ast::Token::Ast(ast) if self.current_offset <= self.target_offset => {
|
||||
let start = self.current_offset;
|
||||
ast.shape().feed_to(self);
|
||||
let end = self.current_offset;
|
||||
if end > self.target_offset {
|
||||
self.result
|
||||
.push(AstWithRange { ast: ast.clone_ref(), range: (start..end).into() });
|
||||
}
|
||||
}
|
||||
other => self.current_offset += other.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === EditedAst ===
|
||||
// =================
|
||||
|
||||
/// Information about the AST node which is currently modified in the Searcher Input's expression.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct EditedAst {
|
||||
/// The edited name. The left side of the name should be used as searching pattern, and picked
|
||||
/// suggestion should replace the entire name. `None` if no name is edited - in that case
|
||||
/// suggestion should be inserted at the cursor position.
|
||||
pub edited_name: Option<AstWithRange>,
|
||||
/// Accessor chain (like `Foo.Bar.buz`) which is currently edited.
|
||||
pub edited_accessor_chain: Option<AstWithRange<ast::opr::Chain>>,
|
||||
/// The currently edited literal.
|
||||
pub edited_literal: Option<Literal>,
|
||||
}
|
||||
|
||||
impl EditedAst {
|
||||
/// Create EditedAst structure basing on the result of [`AstAtPositionFinder::find`].
|
||||
fn new(stack: Vec<AstWithRange>) -> Self {
|
||||
let mut stack = stack.into_iter();
|
||||
if let Some(leaf) = stack.next() {
|
||||
let leaf_parent = stack.next();
|
||||
let leaf_id = leaf.ast.id;
|
||||
let to_accessor_chain = |a: AstWithRange| {
|
||||
let ast = ast::opr::Chain::try_new_of(&a.ast, ast::opr::predefined::ACCESS)?;
|
||||
let target_id = ast.target.as_ref().map(|arg| arg.arg.id);
|
||||
let leaf_is_not_target = !target_id.contains(&leaf_id);
|
||||
leaf_is_not_target.then_some(AstWithRange { ast, range: a.range })
|
||||
};
|
||||
match leaf.ast.shape() {
|
||||
ast::Shape::Var(_) | ast::Shape::Cons(_) => Self {
|
||||
edited_name: Some(leaf),
|
||||
edited_accessor_chain: leaf_parent.and_then(to_accessor_chain),
|
||||
edited_literal: None,
|
||||
},
|
||||
ast::Shape::Opr(_) => Self {
|
||||
edited_name: None,
|
||||
edited_accessor_chain: leaf_parent.and_then(to_accessor_chain),
|
||||
edited_literal: None,
|
||||
},
|
||||
ast::Shape::Infix(_) => Self {
|
||||
edited_name: None,
|
||||
edited_accessor_chain: to_accessor_chain(leaf),
|
||||
edited_literal: None,
|
||||
},
|
||||
ast::Shape::Number(_) => {
|
||||
// Unwrapping is safe here, because we know that the shape is a number.
|
||||
let ast = ast::known::Number::try_from(leaf.ast).unwrap();
|
||||
Self { edited_literal: Some(Literal::Number(ast)), ..default() }
|
||||
}
|
||||
ast::Shape::Tree(tree) =>
|
||||
if let Some(text) = tree.leaf_info.as_ref() {
|
||||
let edited_literal = Literal::try_new_text(text);
|
||||
Self { edited_literal, ..default() }
|
||||
} else {
|
||||
default()
|
||||
},
|
||||
_ => default(),
|
||||
}
|
||||
} else {
|
||||
default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ================
|
||||
// === InputAst ===
|
||||
// ================
|
||||
|
||||
/// Searcher input content parsed to [`ast::BlockLine`] node if possible.
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum InputAst {
|
||||
Line(ast::BlockLine<Ast>),
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
impl Default for InputAst {
|
||||
fn default() -> Self {
|
||||
Self::Invalid(default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for InputAst {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
InputAst::Line(ast) => write!(f, "{}", ast.repr()),
|
||||
InputAst::Invalid(string) => write!(f, "{string}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Input ===
|
||||
// =============
|
||||
|
||||
/// The Component Browser Input.
|
||||
///
|
||||
/// This structure is responsible for keeping and interpreting the searcher input - it provides
|
||||
/// the information:
|
||||
/// * [what part of the input is a filtering pattern](Input::pattern), and
|
||||
/// * [what part should be replaced when inserting a suggestion](Input::after_inserting_suggestion).
|
||||
///
|
||||
/// Both information are deduced from the _edited name_. The edited name is a concrete
|
||||
/// identifier which we deduce the user is currently editing - we assume this is an identifier where
|
||||
/// the text cursor is positioned (including the "end" of the identifier, like `foo|`).
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Input {
|
||||
/// The input in the AST form.
|
||||
pub ast: InputAst,
|
||||
/// The current cursor position in the input.
|
||||
pub cursor_position: text::Byte,
|
||||
/// The edited part of the input: the name being edited and it's context. See [`EditedAst`] for
|
||||
/// details.
|
||||
pub edited_ast: EditedAst,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
/// Create the structure from the code already parsed to AST.
|
||||
pub fn new(ast: ast::BlockLine<Ast>, cursor_position: text::Byte) -> Self {
|
||||
// We primarily check the character on the left of the cursor (`cursor_position_before`),
|
||||
// to properly handle the situation when the cursor is at end of the edited name.
|
||||
// But if there is no identifier on the left then we check the right side.
|
||||
let cursor_position_before =
|
||||
(cursor_position > text::Byte(0)).then(|| cursor_position - text::ByteDiff(1));
|
||||
let ast_stack_on_left =
|
||||
cursor_position_before.map(|cp| AstAtPositionFinder::find(&ast.elem, cp));
|
||||
let edited_ast_on_left = ast_stack_on_left.map_or_default(EditedAst::new);
|
||||
let edited_ast = if edited_ast_on_left.edited_name.is_none() {
|
||||
let ast_stack_on_right = AstAtPositionFinder::find(&ast.elem, cursor_position);
|
||||
let edited_ast_on_right = EditedAst::new(ast_stack_on_right);
|
||||
if edited_ast_on_right.edited_name.is_some() {
|
||||
edited_ast_on_right
|
||||
} else {
|
||||
edited_ast_on_left
|
||||
}
|
||||
} else {
|
||||
edited_ast_on_left
|
||||
};
|
||||
let ast = InputAst::Line(ast);
|
||||
Self { ast, cursor_position, edited_ast }
|
||||
}
|
||||
|
||||
/// Create the structure parsing the string.
|
||||
pub fn parse(parser: &Parser, expression: impl Str, cursor_position: text::Byte) -> Self {
|
||||
match parser.parse_line(expression.as_ref()) {
|
||||
Ok(ast) => Self::new(ast, cursor_position),
|
||||
Err(_) => Self {
|
||||
ast: InputAst::Invalid(expression.into()),
|
||||
cursor_position,
|
||||
edited_ast: default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the filtering pattern for the input.
|
||||
pub fn filter(&self) -> Filter {
|
||||
let pattern = if let Some(edited) = &self.edited_ast.edited_name {
|
||||
let cursor_position_in_name = self.cursor_position - edited.range.start;
|
||||
let name = ast::identifier::name(&edited.ast);
|
||||
name.map_or_default(|name| {
|
||||
let range = ..cursor_position_in_name.value as usize;
|
||||
name.get(range).unwrap_or_default().into()
|
||||
})
|
||||
} else {
|
||||
default()
|
||||
};
|
||||
let context = self.context().map(|c| c.into_ast().repr().to_im_string());
|
||||
Filter { pattern, context }
|
||||
}
|
||||
|
||||
/// Return the accessor chain being the context of the edited name, i.e. the preceding fully
|
||||
/// qualified name parts or this argument.
|
||||
pub fn context(&self) -> Option<ast::opr::Chain> {
|
||||
self.edited_ast.edited_accessor_chain.clone().map(|mut chain| {
|
||||
chain.ast.args.pop();
|
||||
chain.ast
|
||||
})
|
||||
}
|
||||
|
||||
/// The range of the text which contains the currently edited name, if any.
|
||||
pub fn edited_name_range(&self) -> Option<text::Range<text::Byte>> {
|
||||
self.edited_ast.edited_name.as_ref().map(|name| name.range)
|
||||
}
|
||||
|
||||
/// The range of the text which contains the currently edited accessor chain, if any.
|
||||
pub fn accessor_chain_range(&self) -> Option<text::Range<text::Byte>> {
|
||||
self.edited_ast.edited_accessor_chain.as_ref().map(|chain| chain.range)
|
||||
}
|
||||
|
||||
/// The input as AST node. Returns [`None`] if the input is not a valid AST.
|
||||
pub fn ast(&self) -> Option<&Ast> {
|
||||
match &self.ast {
|
||||
InputAst::Line(ast) => Some(&ast.elem),
|
||||
InputAst::Invalid(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Currently edited literal, if any.
|
||||
pub fn edited_literal(&self) -> Option<&Literal> {
|
||||
self.edited_ast.edited_literal.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============================
|
||||
// === Inserting Suggestion ===
|
||||
// ============================
|
||||
|
||||
/// The description of the results of inserting suggestion.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct InsertedSuggestion {
|
||||
/// An entire input after inserting the suggestion.
|
||||
pub new_input: String,
|
||||
/// The replaced range in the old input.
|
||||
pub replaced: text::Range<text::Byte>,
|
||||
/// The range of the inserted code in the new input. It does not contain any additional spaces
|
||||
/// (in contrary to [`inserted_text`] field.
|
||||
pub inserted_code: text::Range<text::Byte>,
|
||||
/// The range of the entire inserted text in the new input. It contains code and all additional
|
||||
/// spaces.
|
||||
pub inserted_text: text::Range<text::Byte>,
|
||||
/// An import that needs to be added when applying the suggestion.
|
||||
pub import: Option<RequiredImport>,
|
||||
}
|
||||
|
||||
|
||||
impl InsertedSuggestion {
|
||||
/// The text change resulting from this insertion.
|
||||
pub fn input_change(&self) -> text::Change<text::Byte, String> {
|
||||
let to_insert = self.new_input[self.inserted_text].to_owned();
|
||||
text::Change { range: self.replaced, text: to_insert }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === Insert Context ===
|
||||
|
||||
/// A helper abstraction responsible for generating proper code for the suggestion.
|
||||
///
|
||||
/// If the context contains a qualified name, we want to reuse it in the generated code sample.
|
||||
/// For example, if the user types `Foo.Bar.` and accepts a method `Foo.Bar.baz` we insert
|
||||
/// `Foo.Bar.baz` with `Foo` import instead of inserting `Bar.baz` with `Bar` import.
|
||||
struct InsertContext<'a> {
|
||||
suggestion: &'a action::Suggestion,
|
||||
context: Option<ast::opr::Chain>,
|
||||
generate_this: bool,
|
||||
}
|
||||
|
||||
impl InsertContext<'_> {
|
||||
/// Whether the context of the edited expression contains a qualified name. Qualified name is a
|
||||
/// infix chain of `ACCESS` operators with identifiers.
|
||||
fn has_qualified_name(&self) -> bool {
|
||||
if let Some(ref context) = self.context {
|
||||
let every_operand_is_name = context.enumerate_operands().flatten().all(|opr| {
|
||||
matches!(opr.item.arg.shape(), ast::Shape::Cons(_) | ast::Shape::Var(_))
|
||||
});
|
||||
let every_operator_is_access = context
|
||||
.enumerate_operators()
|
||||
.all(|opr| opr.item.ast().repr() == ast::opr::predefined::ACCESS);
|
||||
every_operand_is_name && every_operator_is_access
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// All the segments of the qualified name already written by the user.
|
||||
fn qualified_name_segments(&self) -> Option<Vec<ImString>> {
|
||||
if self.has_qualified_name() {
|
||||
// Unwrap is safe here, because `has_qualified_name` would not return true in case of no
|
||||
// context.
|
||||
let context = self.context.as_ref().unwrap();
|
||||
let name_segments = context
|
||||
.enumerate_operands()
|
||||
.flatten()
|
||||
.filter_map(|opr| ast::identifier::name(&opr.item.arg).map(ImString::new))
|
||||
.collect_vec();
|
||||
Some(name_segments)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of name segments to replace the user input, and also a required import for the
|
||||
/// final expression. The returned list of segments contains both the segments already entered
|
||||
/// by the user and the segments that need to be added.
|
||||
fn segments_to_replace(&self) -> Option<(Vec<ImString>, Option<RequiredImport>)> {
|
||||
if let Some(existing_segments) = self.qualified_name_segments() {
|
||||
if let action::Suggestion::FromDatabase(entry) = self.suggestion {
|
||||
let name = entry.qualified_name();
|
||||
let all_segments = name.segments().cloned().collect_vec();
|
||||
// A list of search windows is reversed, because we want to look from the end, as it
|
||||
// gives the shortest possible name.
|
||||
let window_size = existing_segments.len();
|
||||
let mut windows = all_segments.windows(window_size).rev();
|
||||
let window_position_from_end = windows.position(|w| w == existing_segments)?;
|
||||
let pos = all_segments.len().saturating_sub(window_position_from_end + window_size);
|
||||
let name_segments = all_segments.get(pos..).unwrap_or(&[]).to_vec();
|
||||
let import_segments = all_segments.get(..=pos).unwrap_or(&[]).to_vec();
|
||||
// Valid qualified name requires at least 2 segments (namespace and project name).
|
||||
// Not enough segments to build a qualified name is not a mistake here – it just
|
||||
// means we don't need any import, as the entry is already in scope.
|
||||
let minimal_count_of_segments = 2;
|
||||
let import = if import_segments.len() < minimal_count_of_segments {
|
||||
None
|
||||
} else if let Ok(import) = QualifiedName::from_all_segments(import_segments.clone())
|
||||
{
|
||||
Some(RequiredImport::Name(import))
|
||||
} else {
|
||||
error!("Invalid import formed in `segments_to_replace`: {import_segments:?}.");
|
||||
None
|
||||
};
|
||||
Some((name_segments, import))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn code_to_insert(&self) -> (Cow<str>, Option<RequiredImport>) {
|
||||
match self.suggestion {
|
||||
action::Suggestion::FromDatabase(entry) => {
|
||||
if let Some((segments, import)) = self.segments_to_replace() {
|
||||
(Cow::from(segments.iter().join(ast::opr::predefined::ACCESS)), import)
|
||||
} else {
|
||||
let code = entry.code_to_insert(self.generate_this);
|
||||
let import = Some(RequiredImport::Entry(entry.clone_ref()));
|
||||
(code, import)
|
||||
}
|
||||
}
|
||||
action::Suggestion::Hardcoded(snippet) => (Cow::from(snippet.code.as_str()), None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Input ===
|
||||
|
||||
impl Input {
|
||||
/// Return an information about input change after inserting given suggestion.
|
||||
pub fn after_inserting_suggestion(
|
||||
&self,
|
||||
suggestion: &action::Suggestion,
|
||||
has_this: bool,
|
||||
) -> FallibleResult<InsertedSuggestion> {
|
||||
let context = self.context();
|
||||
let generate_this = !has_this;
|
||||
let context = InsertContext { suggestion, context, generate_this };
|
||||
let default_range = (self.cursor_position..self.cursor_position).into();
|
||||
let replaced = if context.has_qualified_name() {
|
||||
self.accessor_chain_range().unwrap_or(default_range)
|
||||
} else {
|
||||
self.edited_name_range().unwrap_or(default_range)
|
||||
};
|
||||
let (code_to_insert, import) = context.code_to_insert();
|
||||
debug!("Code to insert: \"{code_to_insert}\"");
|
||||
let end_of_inserted_code = replaced.start + text::Bytes(code_to_insert.len());
|
||||
let end_of_inserted_text = end_of_inserted_code + text::Bytes(1);
|
||||
let mut new_input = self.ast.to_string();
|
||||
let raw_range = replaced.start.value..replaced.end.value;
|
||||
Self::ensure_on_char_boundary(&new_input, raw_range.start)?;
|
||||
Self::ensure_on_char_boundary(&new_input, raw_range.end)?;
|
||||
new_input.replace_range(raw_range, &code_to_insert);
|
||||
new_input.insert(end_of_inserted_code.value, ' ');
|
||||
Ok(InsertedSuggestion {
|
||||
new_input,
|
||||
replaced,
|
||||
inserted_code: (replaced.start..end_of_inserted_code).into(),
|
||||
inserted_text: (replaced.start..end_of_inserted_text).into(),
|
||||
import,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if the `index` is at a char boundary inside `string` and can be used as a range
|
||||
/// boundary. Otherwise, return an error.
|
||||
fn ensure_on_char_boundary(string: &str, index: usize) -> FallibleResult<()> {
|
||||
if !string.is_char_boundary(index) {
|
||||
Err(NotACharBoundary { index, string: string.into() }.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ============
|
||||
// === Test ===
|
||||
// ============
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn edited_literal() {
|
||||
struct Case {
|
||||
input: &'static str,
|
||||
expected: Option<Literal>,
|
||||
}
|
||||
|
||||
let cases: Vec<Case> = vec![
|
||||
Case { input: "", expected: None },
|
||||
Case {
|
||||
input: "'",
|
||||
expected: Some(Literal::Text {
|
||||
text: "''".into(),
|
||||
closing_delimiter: Some("'"),
|
||||
}),
|
||||
},
|
||||
Case {
|
||||
input: "\"\"\"",
|
||||
expected: Some(Literal::Text {
|
||||
text: "\"\"\"\"\"\"".into(),
|
||||
closing_delimiter: Some("\"\"\""),
|
||||
}),
|
||||
},
|
||||
Case {
|
||||
input: "\"text",
|
||||
expected: Some(Literal::Text {
|
||||
text: "\"text\"".into(),
|
||||
closing_delimiter: Some("\""),
|
||||
}),
|
||||
},
|
||||
Case {
|
||||
input: "\"text\"",
|
||||
expected: Some(Literal::Text {
|
||||
text: "\"text\"".into(),
|
||||
closing_delimiter: None,
|
||||
}),
|
||||
},
|
||||
Case {
|
||||
input: "'text",
|
||||
expected: Some(Literal::Text {
|
||||
text: "'text'".into(),
|
||||
closing_delimiter: Some("'"),
|
||||
}),
|
||||
},
|
||||
Case {
|
||||
input: "'text'",
|
||||
expected: Some(Literal::Text {
|
||||
text: "'text'".into(),
|
||||
closing_delimiter: None,
|
||||
}),
|
||||
},
|
||||
Case {
|
||||
input: "x = 'text",
|
||||
expected: Some(Literal::Text {
|
||||
text: "'text'".into(),
|
||||
closing_delimiter: Some("'"),
|
||||
}),
|
||||
},
|
||||
Case {
|
||||
input: "x = \"\"\"text",
|
||||
expected: Some(Literal::Text {
|
||||
text: "\"\"\"text\"\"\"".into(),
|
||||
closing_delimiter: Some("\"\"\""),
|
||||
}),
|
||||
},
|
||||
];
|
||||
let parser = Parser::new();
|
||||
|
||||
let input = Input::parse(&parser, "123", text::Byte(3));
|
||||
assert!(matches!(input.edited_literal(), Some(Literal::Number(n)) if n.repr() == "123"));
|
||||
let input = Input::parse(&parser, "x = 123", text::Byte(7));
|
||||
assert!(matches!(input.edited_literal(), Some(Literal::Number(n)) if n.repr() == "123"));
|
||||
|
||||
for case in cases {
|
||||
let input = Input::parse(&parser, case.input, text::Byte(case.input.len()));
|
||||
assert_eq!(input.edited_literal(), case.expected.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_input() {
|
||||
#[derive(Debug, Default)]
|
||||
struct Case {
|
||||
input: String,
|
||||
cursor_position: text::Byte,
|
||||
expected_accessor_chain_range: Option<text::Range<text::Byte>>,
|
||||
expected_name_range: Option<text::Range<text::Byte>>,
|
||||
expected_pattern: String,
|
||||
}
|
||||
|
||||
impl Case {
|
||||
fn new(
|
||||
input: impl Into<String>,
|
||||
cursor_position: impl Into<text::Byte>,
|
||||
expected_accessor_chain_range: Option<impl Into<text::Range<text::Byte>>>,
|
||||
expected_name_range: Option<impl Into<text::Range<text::Byte>>>,
|
||||
expected_pattern: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
input: input.into(),
|
||||
cursor_position: cursor_position.into(),
|
||||
expected_accessor_chain_range: expected_accessor_chain_range.map(Into::into),
|
||||
expected_name_range: expected_name_range.map(Into::into),
|
||||
expected_pattern: expected_pattern.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(self, parser: &Parser) {
|
||||
debug!("Running case {} cursor position {}", self.input, self.cursor_position);
|
||||
let input = Input::parse(parser, self.input, self.cursor_position);
|
||||
let pattern = input.filter().pattern.clone_ref();
|
||||
assert_eq!(input.cursor_position, self.cursor_position);
|
||||
assert_eq!(input.edited_ast.edited_name.map(|a| a.range), self.expected_name_range);
|
||||
assert_eq!(
|
||||
input.edited_ast.edited_accessor_chain.map(|a| a.range),
|
||||
self.expected_accessor_chain_range
|
||||
);
|
||||
assert_eq!(pattern, self.expected_pattern);
|
||||
}
|
||||
}
|
||||
|
||||
let none_range: Option<text::Range<text::Byte>> = None;
|
||||
let cases = [
|
||||
Case::new("foo", 3, none_range, Some(0..3), "foo"),
|
||||
Case::new("foo", 1, none_range, Some(0..3), "f"),
|
||||
Case::new("foo", 0, none_range, Some(0..3), ""),
|
||||
Case::new("(2 + foo) * bar", 2, none_range, none_range, ""),
|
||||
Case::new("(2 + foo)*bar", 5, none_range, Some(5..8), ""),
|
||||
Case::new("(2 + foo)*bar", 7, none_range, Some(5..8), "fo"),
|
||||
Case::new("(2 + foo)*bar", 8, none_range, Some(5..8), "foo"),
|
||||
Case::new("(2 + foo)*bar", 9, none_range, none_range, ""),
|
||||
Case::new("(2 + foo)*b", 10, none_range, Some(10..11), ""),
|
||||
Case::new("(2 + foo)*b", 11, none_range, Some(10..11), "b"),
|
||||
Case::new("Standard.Base.foo", 0, none_range, Some(0..8), ""),
|
||||
Case::new("Standard.Base.foo", 4, none_range, Some(0..8), "Stan"),
|
||||
Case::new("Standard.Base.foo", 8, none_range, Some(0..8), "Standard"),
|
||||
Case::new("Standard.Base.foo", 9, Some(0..13), Some(9..13), ""),
|
||||
Case::new("Standard.Base.foo", 11, Some(0..13), Some(9..13), "Ba"),
|
||||
Case::new("Standard.Base.foo", 13, Some(0..13), Some(9..13), "Base"),
|
||||
Case::new("Standard.Base.foo", 14, Some(0..17), Some(14..17), ""),
|
||||
Case::new("Standard.Base.foo", 15, Some(0..17), Some(14..17), "f"),
|
||||
Case::new("Standard.Base.foo", 17, Some(0..17), Some(14..17), "foo"),
|
||||
Case::new("Standard . Base . foo", 17, Some(0..21), none_range, ""),
|
||||
Case::new("Standard . Base.foo", 16, Some(11..19), Some(16..19), ""),
|
||||
Case::new("Standard.Base.", 14, Some(0..14), none_range, ""),
|
||||
Case::new("(2 + Standard.Base.foo)*b", 21, Some(5..22), Some(19..22), "fo"),
|
||||
Case::new("(2 + Standard.Base.)*b", 19, Some(5..19), none_range, ""),
|
||||
];
|
||||
|
||||
let parser = Parser::new();
|
||||
for case in cases {
|
||||
case.run(&parser);
|
||||
}
|
||||
}
|
||||
}
|
@ -392,9 +392,7 @@ pub enum NodeEditStatus {
|
||||
/// The node was edited and had a previous expression.
|
||||
Edited {
|
||||
/// Expression of the node before the edit was started.
|
||||
previous_expression: String,
|
||||
/// Intended method of the node before editing (if known).
|
||||
previous_intended_method: Option<MethodId>,
|
||||
previous_expression: String,
|
||||
},
|
||||
/// The node was created and did not previously exist.
|
||||
Created,
|
||||
@ -411,6 +409,9 @@ pub struct NodeMetadata {
|
||||
///
|
||||
/// The methods may be defined for different types, so the name alone don't specify them.
|
||||
#[serde(default, deserialize_with = "enso_prelude::deserialize_or_default")]
|
||||
#[deprecated(
|
||||
note = "No longer used anywhere, but is kept for compatibility with old projects"
|
||||
)]
|
||||
pub intended_method: Option<MethodId>,
|
||||
/// Information about uploading file.
|
||||
///
|
||||
@ -803,7 +804,6 @@ main = 5
|
||||
let id = ast::Id::from_str("1d6660c6-a70b-4eeb-b5f7-82f05a51df25").unwrap();
|
||||
let node = file.metadata.ide.node.get(&id).unwrap();
|
||||
assert_eq!(node.position, Some(Position::new(-75.5, 52.0)));
|
||||
assert_eq!(node.intended_method, None);
|
||||
assert_eq!(file.metadata.rest, serde_json::Value::Object(default()));
|
||||
}
|
||||
}
|
||||
|
@ -323,13 +323,12 @@ fn restore_edited_node_in_graph(
|
||||
graph.remove_node(node_id)?;
|
||||
md_entry.remove();
|
||||
}
|
||||
Some(NodeEditStatus::Edited { previous_expression, previous_intended_method }) => {
|
||||
Some(NodeEditStatus::Edited { previous_expression }) => {
|
||||
debug!(
|
||||
"Restoring edited node {node_id} to original expression \
|
||||
\"{previous_expression}\"."
|
||||
);
|
||||
graph.edit_node(node_id, Parser::new().parse_line_ast(previous_expression)?)?;
|
||||
md_entry.get_mut().intended_method = previous_intended_method;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
@ -315,7 +315,7 @@ impl Project {
|
||||
let content_roots = ContentRoots::new_from_connection(language_server);
|
||||
let content_roots = Rc::new(content_roots);
|
||||
let notifications = notification::Publisher::default();
|
||||
let urm = Rc::new(model::undo_redo::Manager::new());
|
||||
let urm = default();
|
||||
let properties = Rc::new(RefCell::new(properties));
|
||||
|
||||
let ret = Project {
|
||||
|
@ -343,7 +343,6 @@ impl Repository {
|
||||
/// Owns [`Repository`] and keeps track of open modules.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Manager {
|
||||
#[allow(missing_docs)]
|
||||
/// Repository with undo and redo stacks.
|
||||
pub repository: Rc<Repository>,
|
||||
/// Currently available modules.
|
||||
@ -535,7 +534,8 @@ main =
|
||||
assert_eq!(sum_node.expression().to_string(), "2 + 2");
|
||||
assert_eq!(product_node.expression().to_string(), "5 * 5");
|
||||
|
||||
let sum_tree = SpanTree::<()>::new(&sum_node.expression(), graph).unwrap();
|
||||
let context = &span_tree::generate::context::Empty;
|
||||
let sum_tree = SpanTree::<()>::new(&sum_node.expression(), context).unwrap();
|
||||
let sum_input =
|
||||
sum_tree.root_ref().leaf_iter().find(|n| n.is_argument()).unwrap().crumbs;
|
||||
let connection = controller::graph::Connection {
|
||||
@ -543,7 +543,7 @@ main =
|
||||
destination: controller::graph::Endpoint::new(sum_node.id(), sum_input),
|
||||
};
|
||||
|
||||
graph.connect(&connection, &span_tree::generate::context::Empty).unwrap();
|
||||
graph.connect(&connection, context).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -19,11 +19,11 @@ use crate::presenter::searcher::provider::ControllerComponentsProviderExt;
|
||||
use enso_frp as frp;
|
||||
use enso_suggestion_database::documentation_ir::EntryDocumentation;
|
||||
use enso_suggestion_database::documentation_ir::Placeholder;
|
||||
use enso_text as text;
|
||||
use ide_view as view;
|
||||
use ide_view::component_browser::component_list_panel::grid as component_grid;
|
||||
use ide_view::component_browser::component_list_panel::BreadcrumbId;
|
||||
use ide_view::component_browser::component_list_panel::SECTION_NAME_CRUMB_INDEX;
|
||||
use ide_view::graph_editor::component::node as node_view;
|
||||
use ide_view::graph_editor::GraphEditor;
|
||||
use ide_view::project::SearcherParams;
|
||||
|
||||
@ -96,8 +96,8 @@ impl Model {
|
||||
}
|
||||
|
||||
#[profile(Debug)]
|
||||
fn input_changed(&self, new_input: &str) {
|
||||
if let Err(err) = self.controller.set_input(new_input.to_owned()) {
|
||||
fn input_changed(&self, new_input: &str, cursor_position: text::Byte) {
|
||||
if let Err(err) = self.controller.set_input(new_input.to_owned(), cursor_position) {
|
||||
error!("Error while setting new searcher input: {err}.");
|
||||
}
|
||||
}
|
||||
@ -143,7 +143,7 @@ impl Model {
|
||||
fn suggestion_accepted(
|
||||
&self,
|
||||
id: component_grid::GroupEntryId,
|
||||
) -> Option<(ViewNodeId, node_view::Expression)> {
|
||||
) -> Option<(ViewNodeId, text::Range<text::Byte>, ImString)> {
|
||||
let provider = self.controller.provider();
|
||||
let component: FallibleResult<_> =
|
||||
provider.component_by_view_id(id).ok_or_else(|| NoSuchComponent(id).into());
|
||||
@ -156,10 +156,9 @@ impl Model {
|
||||
self.controller.use_suggestion(suggestion)
|
||||
});
|
||||
match new_code {
|
||||
Ok(new_code) => {
|
||||
Ok(text::Change { range, text }) => {
|
||||
self.update_breadcrumbs();
|
||||
let new_code_and_trees = node_view::Expression::new_plain(new_code);
|
||||
Some((self.input_view, new_code_and_trees))
|
||||
Some((self.input_view, range, text.into()))
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error while applying suggestion: {err}.");
|
||||
@ -248,7 +247,7 @@ impl Model {
|
||||
component::Data::FromDatabase { id, .. } =>
|
||||
self.controller.documentation_for_entry(*id),
|
||||
component::Data::Virtual { snippet } =>
|
||||
snippet.documentation.as_ref().map_or_default(EntryDocumentation::builtin),
|
||||
snippet.documentation.clone().unwrap_or_default(),
|
||||
}
|
||||
} else {
|
||||
default()
|
||||
@ -292,9 +291,13 @@ impl Searcher {
|
||||
let network = frp::Network::new("presenter::Searcher");
|
||||
|
||||
let graph = &model.view.graph().frp;
|
||||
let browser = model.view.searcher();
|
||||
|
||||
frp::extend! { network
|
||||
eval model.view.searcher_input_changed ((expr) model.input_changed(expr));
|
||||
eval model.view.searcher_input_changed ([model]((expr, selections)) {
|
||||
let cursor_position = selections.last().map(|sel| sel.end).unwrap_or_default();
|
||||
model.input_changed(expr, cursor_position);
|
||||
});
|
||||
|
||||
action_list_changed <- source::<()>();
|
||||
|
||||
@ -302,7 +305,6 @@ impl Searcher {
|
||||
model.controller.reload_list());
|
||||
}
|
||||
|
||||
let browser = model.view.searcher();
|
||||
let grid = &browser.model().list.model().grid;
|
||||
let navigator = &browser.model().list.model().section_navigator;
|
||||
let breadcrumbs = &browser.model().list.model().breadcrumbs;
|
||||
@ -316,8 +318,8 @@ impl Searcher {
|
||||
let provider = provider::Component::provide_new_list(controller_provider, &grid);
|
||||
*model.provider.borrow_mut() = Some(provider);
|
||||
});
|
||||
new_input <- grid.suggestion_accepted.filter_map(f!((e) model.suggestion_accepted(*e)));
|
||||
graph.set_node_expression <+ new_input;
|
||||
input_edit <- grid.suggestion_accepted.filter_map(f!((e) model.suggestion_accepted(*e)));
|
||||
graph.edit_node_expression <+ input_edit;
|
||||
|
||||
entry_selected <- grid.active.filter_map(|&s| s?.as_entry_id());
|
||||
selected_entry_changed <- entry_selected.on_change().constant(());
|
||||
@ -380,7 +382,7 @@ impl Searcher {
|
||||
/// The expression to be used for newly created nodes when initialising the searcher without
|
||||
/// an existing node.
|
||||
const DEFAULT_INPUT_EXPRESSION: &str = "Nothing";
|
||||
let SearcherParams { input, source_node } = parameters;
|
||||
let SearcherParams { input, source_node, .. } = parameters;
|
||||
|
||||
let view_data = graph_editor.model.nodes.get_cloned_ref(&input);
|
||||
|
||||
@ -443,6 +445,10 @@ impl Searcher {
|
||||
view: view::project::View,
|
||||
parameters: SearcherParams,
|
||||
) -> FallibleResult<Self> {
|
||||
// We get the position for searcher before initializing the input node, because the
|
||||
// added node will affect the AST, and the position will become incorrect.
|
||||
let position_in_code = graph_controller.graph().definition_end_location()?;
|
||||
|
||||
let mode = Self::init_input_node(
|
||||
parameters,
|
||||
graph_presenter,
|
||||
@ -455,13 +461,15 @@ impl Searcher {
|
||||
&project_controller.model,
|
||||
graph_controller,
|
||||
mode,
|
||||
parameters.cursor_position,
|
||||
position_in_code,
|
||||
)?;
|
||||
|
||||
// Clear input on a new node. By default this will be set to whatever is used as the default
|
||||
// content of the new node.
|
||||
if let Mode::NewNode { source_node, .. } = mode {
|
||||
if source_node.is_none() {
|
||||
if let Err(e) = searcher_controller.set_input("".to_string()) {
|
||||
if let Err(e) = searcher_controller.set_input("".to_string(), text::Byte(0)) {
|
||||
error!("Failed to clear input when creating searcher for a new node: {e:?}.");
|
||||
}
|
||||
}
|
||||
|
@ -33,9 +33,7 @@ pub fn create_providers_from_controller(controller: &controller::Searcher) -> An
|
||||
match controller.actions() {
|
||||
Actions::Loading => as_any(Rc::new(list_view::entry::EmptyProvider)),
|
||||
Actions::Loaded { list } => {
|
||||
let user_action = controller.current_user_action();
|
||||
let intended_function = controller.intended_function_suggestion();
|
||||
let provider = Action { actions: list, user_action, intended_function };
|
||||
let provider = Action { actions: list };
|
||||
as_any(Rc::new(provider))
|
||||
}
|
||||
Actions::Error(err) => {
|
||||
@ -61,9 +59,7 @@ where P: list_view::entry::ModelProvider<view::searcher::Entry>
|
||||
/// An searcher actions provider, based on the action list retrieved from the searcher controller.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Action {
|
||||
actions: Rc<controller::searcher::action::List>,
|
||||
user_action: controller::searcher::UserAction,
|
||||
intended_function: Option<controller::searcher::action::Suggestion>,
|
||||
actions: Rc<controller::searcher::action::List>,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
@ -126,11 +122,7 @@ impl list_view::entry::ModelProvider<GlyphHighlightedLabel> for Action {
|
||||
|
||||
impl ide_view::searcher::DocumentationProvider for Action {
|
||||
fn get(&self) -> Option<String> {
|
||||
use controller::searcher::UserAction::*;
|
||||
self.intended_function.as_ref().and_then(|function| match self.user_action {
|
||||
StartingTypingArgument => function.documentation_html().map(ToOwned::to_owned),
|
||||
_ => None,
|
||||
})
|
||||
None
|
||||
}
|
||||
|
||||
fn get_for_entry(&self, id: usize) -> Option<String> {
|
||||
|
@ -210,12 +210,12 @@ pub mod mock {
|
||||
&self,
|
||||
module: model::Module,
|
||||
db: Rc<model::SuggestionDatabase>,
|
||||
) -> crate::controller::Graph {
|
||||
) -> controller::Graph {
|
||||
let parser = self.parser.clone_ref();
|
||||
let method = self.method_pointer();
|
||||
let definition =
|
||||
module.lookup_method(self.project_name.clone(), &method).expect("Lookup failed.");
|
||||
crate::controller::Graph::new(module, db, parser, definition)
|
||||
controller::Graph::new(module, db, parser, definition)
|
||||
.expect("Graph could not be created")
|
||||
}
|
||||
|
||||
@ -291,11 +291,14 @@ pub mod mock {
|
||||
let data = self.clone();
|
||||
let searcher_target = executed_graph.graph().nodes().unwrap().last().unwrap().id();
|
||||
let searcher_mode = controller::searcher::Mode::EditNode { node_id: searcher_target };
|
||||
let position_in_code = executed_graph.graph().definition_end_location().unwrap();
|
||||
let searcher = controller::Searcher::new_from_graph_controller(
|
||||
ide.clone_ref(),
|
||||
&project,
|
||||
executed_graph.clone_ref(),
|
||||
searcher_mode,
|
||||
enso_text::Byte(0),
|
||||
position_in_code,
|
||||
)
|
||||
.unwrap();
|
||||
executor.run_until_stalled();
|
||||
|
@ -1,6 +1,5 @@
|
||||
use super::prelude::*;
|
||||
|
||||
use crate::controller::graph::NodeTrees;
|
||||
use crate::ide;
|
||||
use crate::transport::test_utils::TestWithMockedTransport;
|
||||
|
||||
@ -50,112 +49,3 @@ fn failure_to_open_project_is_reported() {
|
||||
// Further investigation needed, tracked by https://github.com/enso-org/ide/issues/1575
|
||||
fixture.run_until_stalled();
|
||||
}
|
||||
|
||||
|
||||
// ====================================
|
||||
// === SpanTree in Graph Controller ===
|
||||
// ====================================
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn span_tree_args() {
|
||||
use crate::test::mock::*;
|
||||
use span_tree::Node;
|
||||
|
||||
let data = Unified::new();
|
||||
let fixture = data.fixture_customize(|_, json_client, _| {
|
||||
// The searcher requests for completion when we clear the input.
|
||||
controller::searcher::test::expect_completion(json_client, &[1]);
|
||||
// Additional completion request happens after picking completion.
|
||||
controller::searcher::test::expect_completion(json_client, &[1]);
|
||||
});
|
||||
let Fixture { graph, executed_graph, searcher, suggestion_db, .. } = &fixture;
|
||||
let entry = suggestion_db.lookup(1).unwrap();
|
||||
|
||||
searcher.set_input("".into()).unwrap();
|
||||
searcher
|
||||
.use_suggestion(controller::searcher::action::Suggestion::FromDatabase(entry.clone_ref()))
|
||||
.unwrap();
|
||||
|
||||
let id = searcher.commit_node().unwrap();
|
||||
|
||||
let get_node = || graph.node(id).unwrap();
|
||||
let get_inputs = || NodeTrees::new(&get_node().info, executed_graph).unwrap().inputs;
|
||||
let get_param = |n| {
|
||||
let inputs = get_inputs();
|
||||
let mut args = inputs.root_ref().leaf_iter().filter(|n| n.is_function_parameter());
|
||||
args.nth(n).and_then(|node| node.argument_info())
|
||||
};
|
||||
|
||||
let parser = executed_graph.parser();
|
||||
let mut invocation_info = entry.invocation_info(suggestion_db, &parser);
|
||||
let expected_this_param = invocation_info.parameters.remove(0).with_call_id(Some(id));
|
||||
let expected_arg1_param = invocation_info.parameters.remove(0).with_call_id(Some(id));
|
||||
|
||||
// === Method notation, without prefix application ===
|
||||
assert_eq!(get_node().info.expression().repr(), "Base.foo");
|
||||
match get_inputs().root.children.as_slice() {
|
||||
// The tree here should have two nodes under root - one with given Ast and second for
|
||||
// an additional prefix application argument.
|
||||
[_, second] => {
|
||||
let Node { children, kind, .. } = &second.node;
|
||||
assert!(children.is_empty());
|
||||
assert_eq!(kind.argument_info().as_ref(), Some(&expected_arg1_param));
|
||||
}
|
||||
_ => panic!("Expected only two children in the span tree's root"),
|
||||
};
|
||||
|
||||
|
||||
// === Method notation, with prefix application ===
|
||||
graph.set_expression(id, "Base.foo 50").unwrap();
|
||||
match get_inputs().root.children.as_slice() {
|
||||
// The tree here should have two nodes under root - one with given Ast and second for
|
||||
// an additional prefix application argument.
|
||||
[_target, argument] => {
|
||||
let Node { children, kind, .. } = &argument.node;
|
||||
assert!(children.is_empty());
|
||||
assert_eq!(kind.argument_info().as_ref(), Some(&expected_arg1_param));
|
||||
}
|
||||
inputs =>
|
||||
panic!("Expected two children in the span tree's root but got {:?}", inputs.len()),
|
||||
};
|
||||
|
||||
|
||||
// === Function notation, without prefix application ===
|
||||
assert_eq!(entry.name, "foo");
|
||||
graph.set_expression(id, "foo").unwrap();
|
||||
assert_eq!(get_param(0).as_ref(), Some(&expected_this_param));
|
||||
assert_eq!(get_param(1).as_ref(), Some(&expected_arg1_param));
|
||||
assert_eq!(get_param(2).as_ref(), None);
|
||||
|
||||
|
||||
// === Function notation, with prefix application ===
|
||||
graph.set_expression(id, "foo Base").unwrap();
|
||||
assert_eq!(get_param(0).as_ref(), Some(&expected_this_param));
|
||||
assert_eq!(get_param(1).as_ref(), Some(&expected_arg1_param));
|
||||
assert_eq!(get_param(2).as_ref(), None);
|
||||
|
||||
|
||||
// === Changed function name, should not have known parameters ===
|
||||
graph.set_expression(id, "bar").unwrap();
|
||||
assert_eq!(get_param(0), None);
|
||||
assert_eq!(get_param(1), None);
|
||||
assert_eq!(get_param(2), None);
|
||||
|
||||
graph.set_expression(id, "bar Base").unwrap();
|
||||
assert_eq!(get_param(0), Some(default()));
|
||||
assert_eq!(get_param(1), None);
|
||||
assert_eq!(get_param(2), None);
|
||||
|
||||
graph.set_expression(id, "Base.bar").unwrap();
|
||||
assert_eq!(get_param(0), Some(default()));
|
||||
assert_eq!(get_param(1), Some(default()));
|
||||
assert_eq!(get_param(2), None);
|
||||
|
||||
// === Oversaturated call ===
|
||||
graph.set_expression(id, "foo Base 10 20 30").unwrap();
|
||||
assert_eq!(get_param(0).as_ref(), Some(&expected_this_param));
|
||||
assert_eq!(get_param(1).as_ref(), Some(&expected_arg1_param));
|
||||
assert_eq!(get_param(2).as_ref(), Some(&default()));
|
||||
assert_eq!(get_param(3).as_ref(), Some(&default()));
|
||||
assert_eq!(get_param(4).as_ref(), None);
|
||||
}
|
||||
|
@ -452,7 +452,13 @@ impl Entry {
|
||||
let is_extension_method =
|
||||
self_type_module.map_or(false, |stm| *stm != self.defined_in);
|
||||
let extension_method_import = is_extension_method.and_option_from(|| {
|
||||
self.defined_in_entry(db).map(|e| e.required_imports(db, in_module.clone_ref()))
|
||||
self.defined_in_entry(db).map(|e| {
|
||||
if e.kind == Kind::Module && e.defined_in == in_module {
|
||||
default()
|
||||
} else {
|
||||
e.required_imports(db, in_module.clone_ref())
|
||||
}
|
||||
})
|
||||
});
|
||||
let self_type_import = self
|
||||
.is_static
|
||||
@ -469,7 +475,7 @@ impl Entry {
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
Kind::Module | Kind::Type if defined_in_same_module => default(),
|
||||
Kind::Type if defined_in_same_module => default(),
|
||||
Kind::Module => {
|
||||
let import = if let Some(reexport) = &self.reexported_in {
|
||||
Import::Unqualified {
|
||||
@ -1125,8 +1131,12 @@ mod test {
|
||||
}
|
||||
|
||||
expect_imports(&static_method, ¤t_modules[0], &[]);
|
||||
expect_imports(&module_method, ¤t_modules[0], &[]);
|
||||
expect_imports(&submodule, ¤t_modules[0], &[]);
|
||||
// The following asserts are disabled, because we actually add import for the module even if
|
||||
// we're inside it. It is necessary, because otherwise `Project.foo` expressions
|
||||
// currently do not work without importing `local.Project` into the scope.
|
||||
// See https://github.com/enso-org/enso/issues/5616.
|
||||
// expect_imports(&module_method, ¤t_modules[0], &[]);
|
||||
// expect_imports(&submodule, ¤t_modules[0], &[]);
|
||||
expect_imports(&extension_method, ¤t_modules[0], &[
|
||||
"from Standard.Base import Number",
|
||||
]);
|
||||
|
@ -21,6 +21,7 @@ use ide_view_component_list_panel_icons::SIZE;
|
||||
use ide_view_graph_editor::component::node::action_bar;
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Frame ===
|
||||
// =============
|
||||
|
@ -299,6 +299,7 @@ ensogl::define_endpoints_2! {
|
||||
set_disabled (bool),
|
||||
set_input_connected (span_tree::Crumbs,Option<Type>,bool),
|
||||
set_expression (Expression),
|
||||
edit_expression (text::Range<text::Byte>, ImString),
|
||||
set_skip_macro (bool),
|
||||
set_freeze_macro (bool),
|
||||
set_comment (Comment),
|
||||
@ -750,6 +751,7 @@ impl Node {
|
||||
);
|
||||
eval filtered_usage_type (((a,b)) model.set_expression_usage_type(a,b));
|
||||
eval input.set_expression ((a) model.set_expression(a));
|
||||
model.input.edit_expression <+ input.edit_expression;
|
||||
out.expression <+ model.input.frp.expression;
|
||||
out.expression_span <+ model.input.frp.on_port_code_update;
|
||||
out.requested_widgets <+ model.input.frp.requested_widgets;
|
||||
|
@ -1,22 +1,25 @@
|
||||
//! Definition of the `ActionBar` component for the `visualization::Container`.
|
||||
|
||||
|
||||
|
||||
pub mod icon;
|
||||
|
||||
use crate::prelude::*;
|
||||
use ensogl::display::shape::*;
|
||||
|
||||
use enso_config::ARGS;
|
||||
use enso_frp as frp;
|
||||
use ensogl::application::Application;
|
||||
use ensogl::display;
|
||||
use ensogl::display::shape::*;
|
||||
use ensogl_component::toggle_button;
|
||||
use ensogl_component::toggle_button::ColorableShape;
|
||||
use ensogl_component::toggle_button::ToggleButton;
|
||||
use ensogl_hardcoded_theme as theme;
|
||||
|
||||
|
||||
// ==============
|
||||
// === Export ===
|
||||
// ==============
|
||||
|
||||
pub mod icon;
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// === Constants ===
|
||||
|
@ -23,6 +23,8 @@ use ensogl::display;
|
||||
use ensogl::gui::cursor;
|
||||
use ensogl::Animation;
|
||||
use ensogl_component::text;
|
||||
use ensogl_component::text::buffer::selection::Selection;
|
||||
use ensogl_component::text::FromInContextSnapped;
|
||||
use ensogl_hardcoded_theme as theme;
|
||||
|
||||
|
||||
@ -852,6 +854,12 @@ ensogl::define_endpoints! {
|
||||
/// Set the node expression.
|
||||
set_expression (node::Expression),
|
||||
|
||||
/// Edit the node expression: if the node is currently edited, the given range will be
|
||||
/// replaced with the string, and the text cursor will be placed after the inserted string.
|
||||
///
|
||||
/// If the node is **not** edited, nothing changes.
|
||||
edit_expression (text::Range<Byte>, ImString),
|
||||
|
||||
/// Set the mode in which the cursor will indicate that editing of the node is possible.
|
||||
set_edit_ready_mode (bool),
|
||||
|
||||
@ -893,6 +901,8 @@ ensogl::define_endpoints! {
|
||||
pointer_style (cursor::Style),
|
||||
width (f32),
|
||||
expression (ImString),
|
||||
expression_edit (ImString, Vec<Selection<Byte>>),
|
||||
|
||||
editing (bool),
|
||||
ports_visible (bool),
|
||||
body_hover (bool),
|
||||
@ -1017,9 +1027,21 @@ impl Area {
|
||||
model.set_expression(expr, *is_editing, &frp_endpoints)
|
||||
)
|
||||
);
|
||||
legit_edit <- frp.input.edit_expression.gate(&frp.input.set_editing);
|
||||
model.label.select <+ legit_edit.map(|(range, _)| (range.start.into(), range.end.into()));
|
||||
model.label.insert <+ legit_edit._1();
|
||||
frp.output.source.expression <+ expression.map(|e| e.code.clone_ref());
|
||||
expression_changed_by_user <- model.label.content.gate(&frp.input.set_editing);
|
||||
frp.output.source.expression <+ expression_changed_by_user.ref_into();
|
||||
frp.output.source.expression_edit <+ model.label.selections.map2(
|
||||
&expression_changed_by_user,
|
||||
f!([model](selection, full_content) {
|
||||
let full_content = full_content.into();
|
||||
let to_byte = |loc| text::Byte::from_in_context_snapped(&model.label, loc);
|
||||
let selections = selection.iter().map(|sel| sel.map(to_byte)).collect_vec();
|
||||
(full_content, selections)
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
// === Expression Type ===
|
||||
|
@ -72,6 +72,8 @@ use ensogl::system::web::traits::*;
|
||||
use ensogl::Animation;
|
||||
use ensogl::DEPRECATED_Animation;
|
||||
use ensogl::Easing;
|
||||
use ensogl_component::text;
|
||||
use ensogl_component::text::buffer::selection::Selection;
|
||||
use ensogl_component::tooltip::Tooltip;
|
||||
use ensogl_hardcoded_theme as theme;
|
||||
|
||||
@ -590,13 +592,13 @@ ensogl::define_endpoints_2! {
|
||||
|
||||
// === VCS Status ===
|
||||
|
||||
set_node_vcs_status ((NodeId,Option<node::vcs::Status>)),
|
||||
set_node_vcs_status ((NodeId, Option<node::vcs::Status>)),
|
||||
|
||||
|
||||
set_detached_edge_targets (EdgeEndpoint),
|
||||
set_detached_edge_sources (EdgeEndpoint),
|
||||
set_edge_source ((EdgeId,EdgeEndpoint)),
|
||||
set_edge_target ((EdgeId,EdgeEndpoint)),
|
||||
set_edge_source ((EdgeId, EdgeEndpoint)),
|
||||
set_edge_target ((EdgeId, EdgeEndpoint)),
|
||||
unset_edge_source (EdgeId),
|
||||
unset_edge_target (EdgeId),
|
||||
connect_nodes ((EdgeEndpoint,EdgeEndpoint)),
|
||||
@ -611,6 +613,7 @@ ensogl::define_endpoints_2! {
|
||||
edit_node (NodeId),
|
||||
collapse_nodes ((Vec<NodeId>,NodeId)),
|
||||
set_node_expression ((NodeId,node::Expression)),
|
||||
edit_node_expression ((NodeId, text::Range<text::Byte>, ImString)),
|
||||
set_node_skip ((NodeId,bool)),
|
||||
set_node_freeze ((NodeId,bool)),
|
||||
set_node_comment ((NodeId,node::Comment)),
|
||||
@ -618,10 +621,10 @@ ensogl::define_endpoints_2! {
|
||||
set_expression_usage_type ((NodeId,ast::Id,Option<Type>)),
|
||||
update_node_widgets ((NodeId,WidgetUpdates)),
|
||||
cycle_visualization (NodeId),
|
||||
set_visualization ((NodeId,Option<visualization::Path>)),
|
||||
set_visualization ((NodeId, Option<visualization::Path>)),
|
||||
register_visualization (Option<visualization::Definition>),
|
||||
set_visualization_data ((NodeId,visualization::Data)),
|
||||
set_error_visualization_data ((NodeId,visualization::Data)),
|
||||
set_visualization_data ((NodeId, visualization::Data)),
|
||||
set_error_visualization_data ((NodeId, visualization::Data)),
|
||||
enable_visualization (NodeId),
|
||||
disable_visualization (NodeId),
|
||||
|
||||
@ -693,11 +696,12 @@ ensogl::define_endpoints_2! {
|
||||
node_hovered (Option<Switch<NodeId>>),
|
||||
node_selected (NodeId),
|
||||
node_deselected (NodeId),
|
||||
node_position_set ((NodeId, Vector2)),
|
||||
node_position_set_batched ((NodeId, Vector2)),
|
||||
node_expression_set ((NodeId, ImString)),
|
||||
node_position_set ((NodeId,Vector2)),
|
||||
node_position_set_batched ((NodeId,Vector2)),
|
||||
node_expression_set ((NodeId,ImString)),
|
||||
node_expression_span_set ((NodeId, span_tree::Crumbs, ImString)),
|
||||
node_comment_set ((NodeId, String)),
|
||||
node_expression_edited ((NodeId,ImString,Vec<Selection<text::Byte>>)),
|
||||
node_comment_set ((NodeId,String)),
|
||||
node_entered (NodeId),
|
||||
node_exited (),
|
||||
node_editing_started (NodeId),
|
||||
@ -1588,6 +1592,7 @@ impl GraphEditorModelWithNetwork {
|
||||
));
|
||||
|
||||
eval node.expression((t) model.frp.private.output.node_expression_set.emit((node_id,t.into())));
|
||||
|
||||
eval node.expression_span([model]((crumbs,code)) {
|
||||
let args = (node_id, crumbs.clone(), code.clone());
|
||||
model.frp.private.output.node_expression_span_set.emit(args)
|
||||
@ -1598,6 +1603,10 @@ impl GraphEditorModelWithNetwork {
|
||||
model.frp.private.output.widgets_requested.emit(args)
|
||||
});
|
||||
|
||||
let node_expression_edit = node.model().input.expression_edit.clone_ref();
|
||||
model.frp.private.output.node_expression_edited <+ node_expression_edit.map(
|
||||
move |(expr, selection)| (node_id, expr.clone_ref(), selection.clone())
|
||||
);
|
||||
model.frp.private.output.request_import <+ node.request_import;
|
||||
|
||||
|
||||
@ -1947,6 +1956,20 @@ impl GraphEditorModel {
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_node_expression(
|
||||
&self,
|
||||
node_id: impl Into<NodeId>,
|
||||
range: impl Into<text::Range<text::Byte>>,
|
||||
inserted_str: impl Into<ImString>,
|
||||
) {
|
||||
let node_id = node_id.into();
|
||||
let range = range.into();
|
||||
let inserted_str = inserted_str.into();
|
||||
if let Some(node) = self.nodes.get_cloned_ref(&node_id) {
|
||||
node.edit_expression(range, inserted_str);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_node_skip(&self, node_id: impl Into<NodeId>, skip: &bool) {
|
||||
let node_id = node_id.into();
|
||||
if let Some(node) = self.nodes.get_cloned_ref(&node_id) {
|
||||
@ -2809,8 +2832,6 @@ fn new_graph_editor(app: &Application) -> GraphEditor {
|
||||
|
||||
let node_input_touch = TouchNetwork::<EdgeEndpoint>::new(network,mouse);
|
||||
let node_output_touch = TouchNetwork::<EdgeEndpoint>::new(network,mouse);
|
||||
node_expression_set <- source();
|
||||
out.node_expression_set <+ node_expression_set;
|
||||
|
||||
on_output_connect_drag_mode <- node_output_touch.down.constant(true);
|
||||
on_output_connect_follow_mode <- node_output_touch.selected.constant(false);
|
||||
@ -3147,15 +3168,6 @@ fn new_graph_editor(app: &Application) -> GraphEditor {
|
||||
}
|
||||
|
||||
|
||||
// === Set Node Expression ===
|
||||
frp::extend! { network
|
||||
|
||||
set_node_expression_string <- inputs.set_node_expression.map(|(id,expr)| (*id,expr.code.clone()));
|
||||
out.node_expression_set <+ set_node_expression_string;
|
||||
|
||||
}
|
||||
|
||||
|
||||
// === Set Node SKIP and FREEZE macros ===
|
||||
|
||||
frp::extend! { network
|
||||
@ -3642,8 +3654,9 @@ fn new_graph_editor(app: &Application) -> GraphEditor {
|
||||
model.profiling_statuses.remove <+ out.node_removed;
|
||||
out.on_visualization_select <+ out.node_removed.map(|&id| Switch::Off(id));
|
||||
|
||||
eval inputs.set_node_expression (((id,expr)) model.set_node_expression(id,expr));
|
||||
port_to_refresh <= inputs.set_node_expression.map(f!(((id,_))model.node_in_edges(id)));
|
||||
eval inputs.set_node_expression (((id, expr)) model.set_node_expression(id, expr));
|
||||
eval inputs.edit_node_expression (((id, range, ins)) model.edit_node_expression(id, range, ins));
|
||||
port_to_refresh <= inputs.set_node_expression.map(f!(((id, _))model.node_in_edges(id)));
|
||||
eval port_to_refresh ((id) model.set_edge_target_connection_status(*id,true));
|
||||
|
||||
// === Remove implementation ===
|
||||
|
@ -25,6 +25,8 @@ use ensogl::display;
|
||||
use ensogl::system::web;
|
||||
use ensogl::system::web::dom;
|
||||
use ensogl::DEPRECATED_Animation;
|
||||
use ensogl_component::text;
|
||||
use ensogl_component::text::selection::Selection;
|
||||
use ensogl_hardcoded_theme::Theme;
|
||||
use ide_view_graph_editor::NodeSource;
|
||||
|
||||
@ -49,19 +51,21 @@ const INPUT_CHANGE_DELAY_MS: i32 = 200;
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct SearcherParams {
|
||||
/// The node being an Expression Input.
|
||||
pub input: NodeId,
|
||||
pub input: NodeId,
|
||||
/// The node being a source for the edited node data - usually it's output shall be a `this`
|
||||
/// port for inserted expression.
|
||||
pub source_node: Option<NodeSource>,
|
||||
pub source_node: Option<NodeSource>,
|
||||
/// A position of the cursor in the input node.
|
||||
pub cursor_position: text::Byte,
|
||||
}
|
||||
|
||||
impl SearcherParams {
|
||||
fn new_for_new_node(node_id: NodeId, source_node: Option<NodeSource>) -> Self {
|
||||
Self { input: node_id, source_node }
|
||||
Self { input: node_id, source_node, cursor_position: default() }
|
||||
}
|
||||
|
||||
fn new_for_edited_node(node_id: NodeId) -> Self {
|
||||
Self { input: node_id, source_node: None }
|
||||
fn new_for_edited_node(node_id: NodeId, cursor_position: text::Byte) -> Self {
|
||||
Self { input: node_id, source_node: None, cursor_position }
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +111,7 @@ ensogl::define_endpoints! {
|
||||
/// Is **not** emitted with every graph's node expression change, only when
|
||||
/// [`INPUT_CHANGE_DELAY_MS`] passes since last change, so we won't needlessly update the
|
||||
/// Component Browser when user is quickly typing in the input.
|
||||
searcher_input_changed (ImString),
|
||||
searcher_input_changed (ImString, Vec<Selection<text::Byte>>),
|
||||
is_searcher_opened (bool),
|
||||
adding_new_node (bool),
|
||||
old_expression_of_edited_node (Expression),
|
||||
@ -367,6 +371,7 @@ impl View {
|
||||
// TODO[WD]: This should not be needed after the theme switching issue is implemented.
|
||||
// See: https://github.com/enso-org/ide/issues/795
|
||||
let input_change_delay = frp::io::timer::Timeout::new(network);
|
||||
let searcher_open_delay = frp::io::timer::Timeout::new(network);
|
||||
|
||||
if let Some(window_control_buttons) = &*model.window_control_buttons {
|
||||
let initial_size = &window_control_buttons.size.value();
|
||||
@ -456,20 +461,47 @@ impl View {
|
||||
|
||||
// === Editing ===
|
||||
|
||||
existing_node_edited <- graph.node_being_edited.filter_map(|x| *x).gate_not(&frp.adding_new_node);
|
||||
frp.source.searcher <+ existing_node_edited.map(
|
||||
|&node| Some(SearcherParams::new_for_edited_node(node))
|
||||
);
|
||||
searcher_input_change_opt <- graph.node_expression_set.map2(&frp.searcher, |(node_id, expr), searcher| {
|
||||
(searcher.as_ref()?.input == *node_id).then(|| expr.clone_ref())
|
||||
node_edited_by_user <- graph.node_being_edited.gate_not(&frp.adding_new_node);
|
||||
existing_node_edited <- graph.node_expression_edited.gate_not(&frp.is_searcher_opened);
|
||||
open_searcher <- existing_node_edited.map2(&node_edited_by_user,
|
||||
|(id, _, _), edited| edited.map_or(false, |edited| *id == edited)
|
||||
).on_true();
|
||||
searcher_open_delay.restart <+ open_searcher.constant(0);
|
||||
cursor_position <- existing_node_edited.map2(
|
||||
&node_edited_by_user,
|
||||
|(node_id, _, selections), edited| {
|
||||
edited.map_or(None, |edited| {
|
||||
let position = || selections.last().map(|sel| sel.end).unwrap_or_default();
|
||||
(*node_id == edited).then(position)
|
||||
})
|
||||
}
|
||||
).filter_map(|pos| *pos);
|
||||
edited_node <- node_edited_by_user.filter_map(|node| *node);
|
||||
position_and_edited_node <- cursor_position.map2(&edited_node, |pos, id| (*pos, *id));
|
||||
prepare_params <- position_and_edited_node.sample(&searcher_open_delay.on_expired);
|
||||
frp.source.searcher <+ prepare_params.map(|(pos, node_id)| {
|
||||
Some(SearcherParams::new_for_edited_node(*node_id, *pos))
|
||||
});
|
||||
searcher_input_change <- searcher_input_change_opt.filter_map(|expr| expr.clone());
|
||||
searcher_input_change_opt <- graph.node_expression_edited.map2(&frp.searcher,
|
||||
|(node_id, expr, selections), searcher| {
|
||||
let input_change = || (*node_id, expr.clone_ref(), selections.clone());
|
||||
(searcher.as_ref()?.input == *node_id).then(input_change)
|
||||
}
|
||||
);
|
||||
searcher_input_change <- searcher_input_change_opt.filter_map(|change| change.clone());
|
||||
input_change_delay.restart <+ searcher_input_change.constant(INPUT_CHANGE_DELAY_MS);
|
||||
update_searcher_input_on_commit <- frp.output.editing_committed.constant(());
|
||||
input_change_delay.cancel <+ update_searcher_input_on_commit;
|
||||
update_searcher_input <- any(&input_change_delay.on_expired, &update_searcher_input_on_commit);
|
||||
frp.source.searcher_input_changed <+ searcher_input_change.sample(&update_searcher_input);
|
||||
|
||||
input_change_and_searcher <- map2(&searcher_input_change, &frp.searcher,
|
||||
|c, s| (c.clone(), *s)
|
||||
);
|
||||
updated_input <- input_change_and_searcher.sample(&update_searcher_input);
|
||||
input_changed <- updated_input.filter_map(|((node_id, expr, selections), searcher)| {
|
||||
let input_change = || (expr.clone_ref(), selections.clone());
|
||||
(searcher.as_ref()?.input == *node_id).then(input_change)
|
||||
});
|
||||
frp.source.searcher_input_changed <+ input_changed;
|
||||
|
||||
// === Adding Node ===
|
||||
|
||||
|
@ -130,6 +130,7 @@ ensogl_core::define_endpoints! {
|
||||
cursors_select (Option<Transform>),
|
||||
set_cursor (Location),
|
||||
add_cursor (Location),
|
||||
set_single_selection (selection::Shape),
|
||||
set_newest_selection_end (Location),
|
||||
set_oldest_selection_end (Location),
|
||||
insert (ImString),
|
||||
@ -220,6 +221,9 @@ impl Buffer {
|
||||
|
||||
sel_on_set_cursor <- input.set_cursor.map(f!((t) m.set_cursor(*t)));
|
||||
sel_on_add_cursor <- input.add_cursor.map(f!((t) m.add_cursor(*t)));
|
||||
sel_on_set_single_selection <- input.set_single_selection.map(
|
||||
f!((t) m.set_single_selection(*t))
|
||||
);
|
||||
sel_on_set_newest_end <- input.set_newest_selection_end.map
|
||||
(f!((t) m.set_newest_selection_end(*t)));
|
||||
sel_on_set_oldest_end <- input.set_oldest_selection_end.map
|
||||
@ -247,6 +251,7 @@ impl Buffer {
|
||||
output.source.selection_non_edit_mode <+ sel_on_keep_oldest_cursor;
|
||||
output.source.selection_non_edit_mode <+ sel_on_set_cursor;
|
||||
output.source.selection_non_edit_mode <+ sel_on_add_cursor;
|
||||
output.source.selection_non_edit_mode <+ sel_on_set_single_selection;
|
||||
output.source.selection_non_edit_mode <+ sel_on_set_newest_end;
|
||||
output.source.selection_non_edit_mode <+ sel_on_set_oldest_end;
|
||||
output.source.selection_non_edit_mode <+ sel_on_remove_all;
|
||||
@ -446,10 +451,14 @@ impl BufferModel {
|
||||
self.oldest_selection().snap_selections_to_start()
|
||||
}
|
||||
|
||||
fn new_cursor(&self, location: Location) -> Selection {
|
||||
fn new_selection(&self, shape: selection::Shape) -> Selection {
|
||||
let id = self.next_selection_id.get();
|
||||
self.next_selection_id.set(selection::Id { value: id.value + 1 });
|
||||
Selection::new_cursor(location, id)
|
||||
Selection { shape, id }
|
||||
}
|
||||
|
||||
fn new_cursor(&self, location: Location) -> Selection {
|
||||
self.new_selection(selection::Shape::new_cursor(location))
|
||||
}
|
||||
|
||||
/// Returns the last used selection or a new one if no active selection exists. This allows for
|
||||
@ -467,6 +476,12 @@ impl BufferModel {
|
||||
selection
|
||||
}
|
||||
|
||||
fn set_single_selection(&self, shape: selection::Shape) -> selection::Group {
|
||||
let last_selection = self.selection.borrow().last().cloned();
|
||||
let opt_existing = last_selection.map(|t| t.with_shape(shape));
|
||||
opt_existing.unwrap_or_else(|| self.new_selection(shape)).into()
|
||||
}
|
||||
|
||||
fn set_newest_selection_end(&self, location: Location) -> selection::Group {
|
||||
let mut group = self.selection.borrow().clone();
|
||||
group.newest_mut().for_each(|s| s.end = location);
|
||||
@ -813,6 +828,7 @@ pub enum LocationLike {
|
||||
LocationUBytesLine(Location<Byte, Line>),
|
||||
LocationColumnViewLine(Location<Column, ViewLine>),
|
||||
LocationUBytesViewLine(Location<Byte, ViewLine>),
|
||||
Byte(Byte),
|
||||
}
|
||||
|
||||
impl Default for LocationLike {
|
||||
@ -831,6 +847,7 @@ impl LocationLike {
|
||||
Location::from_in_context_snapped(buffer, loc),
|
||||
LocationLike::LocationUBytesViewLine(loc) =>
|
||||
Location::from_in_context_snapped(buffer, loc),
|
||||
LocationLike::Byte(byte) => Location::from_in_context_snapped(buffer, byte),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -303,6 +303,7 @@ ensogl_core::define_endpoints_2! {
|
||||
|
||||
set_cursor (LocationLike),
|
||||
add_cursor (LocationLike),
|
||||
select (LocationLike, LocationLike),
|
||||
paste_string (ImString),
|
||||
insert (ImString),
|
||||
set_property (RangeLike, Option<formatting::Property>),
|
||||
@ -341,6 +342,7 @@ ensogl_core::define_endpoints_2! {
|
||||
width (f32),
|
||||
height (f32),
|
||||
changed (Rc<Vec<buffer::Change>>),
|
||||
selections (buffer::selection::Group),
|
||||
content (Rope),
|
||||
hovered (bool),
|
||||
selection_color (color::Lch),
|
||||
@ -408,6 +410,9 @@ impl Text {
|
||||
|
||||
loc_on_set <- input.set_cursor.map(f!([m](t) t.expand(&m)));
|
||||
loc_on_add <- input.add_cursor.map(f!([m](t) t.expand(&m)));
|
||||
shape_on_select <- input.select.map(
|
||||
f!([m]((s, e)) buffer::selection::Shape(s.expand(&m), e.expand(&m)))
|
||||
);
|
||||
|
||||
mouse_on_set <- mouse.position.sample(&input.set_cursor_at_mouse_position);
|
||||
mouse_on_add <- mouse.position.sample(&input.add_cursor_at_mouse_position);
|
||||
@ -422,8 +427,9 @@ impl Text {
|
||||
loc_on_set <- any(loc_on_set,loc_on_mouse_set,loc_on_set_at_front,loc_on_set_at_end);
|
||||
loc_on_add <- any(loc_on_add,loc_on_mouse_add,loc_on_add_at_front,loc_on_add_at_end);
|
||||
|
||||
eval loc_on_set ((loc) m.buffer.frp.set_cursor(loc));
|
||||
eval loc_on_add ((loc) m.buffer.frp.add_cursor(loc));
|
||||
m.buffer.frp.set_cursor <+ loc_on_set;
|
||||
m.buffer.frp.add_cursor <+ loc_on_add;
|
||||
m.buffer.frp.set_single_selection <+ shape_on_select;
|
||||
|
||||
|
||||
// === Cursor Transformations ===
|
||||
@ -579,6 +585,8 @@ impl Text {
|
||||
// read the new content, so it should be up-to-date.
|
||||
out.content <+ m.buffer.frp.text_change.map(f_!(m.buffer.text()));
|
||||
out.changed <+ m.buffer.frp.text_change;
|
||||
out.selections <+ m.buffer.frp.selection_non_edit_mode;
|
||||
out.selections <+ m.buffer.frp.selection_edit_mode.map(|m| m.selection_group.clone());
|
||||
|
||||
|
||||
// === Text Width And Height Updates ===
|
||||
@ -1914,6 +1922,14 @@ where T: for<'t> FromInContextSnapped<&'t buffer::Buffer, S>
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> FromInContextSnapped<&Text, S> for T
|
||||
where T: for<'t> FromInContextSnapped<&'t TextModel, S>
|
||||
{
|
||||
fn from_in_context_snapped(context: &Text, arg: S) -> Self {
|
||||
T::from_in_context_snapped(&context.data, arg)
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for TextModel {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.display_object
|
||||
|
Loading…
Reference in New Issue
Block a user