Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cursor aware Component Browser #5770

Merged
merged 31 commits into from
Mar 22, 2023
Merged

Conversation

vitvakatu
Copy link
Contributor

@vitvakatu vitvakatu commented Feb 27, 2023

Pull Request Description

Closes #5220

This PR implements the cursor-aware behavior of the CB, as described in pivotal issue

2023-02-28.13-12-48.mp4
2023-03-06.17-24-59.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:

Screenshot 2023-02-28 at 13 18 11

Checklist

Please include the following checklist in your PR:

  • The documentation has been updated if necessary.
  • All code conforms to the
    Scala,
    Java,
    and
    Rust
    style guides.
  • All code has been tested:
    • Unit tests have been written where possible.
    • If GUI codebase was changed: Enso GUI was tested when built using BOTH
      ./run ide build and ./run ide watch.

farmaazon and others added 10 commits February 10, 2023 12:11
Fixes after rebase

Add some docs
# Conflicts:
#	app/gui/src/controller/searcher.rs
# Conflicts:
#	app/gui/src/controller/searcher.rs
#	app/gui/src/presenter/searcher.rs
@vitvakatu vitvakatu self-assigned this Feb 27, 2023
@@ -628,7 +439,8 @@ impl Searcher {
let def_id = graph.graph().id;
let def_span = double_representation::module::definition_span(&module_ast, &def_id)?;
let module_repr: Rope = module_ast.repr().into();
let position = module_repr.offset_to_location_snapped(def_span.end);
// TODO: is it correct?
let position = module_repr.offset_to_location_snapped(def_span.end - text::ByteDiff(1));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subtracting one byte from position helps to find local variables declared inside main function. Apparently the engine thinks that locals are out of scope otherwise and doesn't send them in completion response. This issue should be properly investigated separately.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make a GH issue for that and put its number here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the issue completely by searching position before creating a new node.

let old_context = old_input.context().map(|ctx| ctx.into_ast().repr());

self.invalidate_picked_suggestions();
let context_changed = old_context != new_context;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we're doing the correct thing here. Previous code compared expressions, now we compare access chains, and they usually None. So we are reloading the list far less frequently now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct. Before we were filtering the components by expected argument type (and it depended on the left side of the expression), but now the filtering depends only on accessor chain (i.e. the context).

assert!(are_same(&arg1.entry, &test_var_1));
assert!(are_same(&arg2.entry, &test_var_1));

// TODO
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what's going on in this part of the test, I will fix it or remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems it is not compatible with the new behavior, removed this piece

let doc_string = documentation.to_string();
let documentation_html = doc_parser.generate_html_doc_pure(doc_string);
self.documentation_html = Some(documentation_html.unwrap());
// TODO: we need the Scala parser in searcher tests only for HTML documentation.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look like the correct change, but removing the doc parser usage allows to turn all searcher tests into native ones. This is a huge advantage while developing, though I probably should revert this afterwards and make searcher tests wasm_test again.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the same time, the documentation for snippets is extremely primitive and doc parser doesn't really bring much.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can stay with this change. Before the searcher, tests were usually wasm_bindgen anyway, because we used the Scala parser. But I guess it's no longer the case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, now the tests use Rust parser in general.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to remove the scala parser from here, it does not bring us much.

let range_start = raw_range.start;
let range_end = raw_range.end;
Self::ensure_on_char_boundary(&new_input, range_start)?;
Self::ensure_on_char_boundary(&new_input, range_end)?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw rare panics when one of the indices was not on char boundary inside the input, which was weird. This code helps to mitigate the issue if it ever happens again.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd "inline" the range_start and range_end variables. They won't make the lines too long.

@@ -411,6 +411,7 @@ 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")]
// TODO: It is not used anywhere. Deprecate?
pub intended_method: Option<MethodId>,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now all usages of the intended_method are removed. We are no longer using it to communicate with the engine, so this field should be probably removed or marked as deprecated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marked it as deprecated and removed all usages from the code.

@vitvakatu vitvakatu added the CI: No changelog needed Do not require a changelog entry for this PR. label Feb 28, 2023
@vitvakatu vitvakatu marked this pull request as ready for review February 28, 2023 09:21
@vitvakatu vitvakatu requested a review from farmaazon February 28, 2023 09:21
Comment on lines +171 to +188
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 }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code deserves a bit of explanation in the comment.

let range_start = raw_range.start;
let range_end = raw_range.end;
Self::ensure_on_char_boundary(&new_input, range_start)?;
Self::ensure_on_char_boundary(&new_input, range_end)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd "inline" the range_start and range_end variables. They won't make the lines too long.

Comment on lines 309 to 315
#[derive(Clone, Debug, Fail)]
#[allow(missing_docs)]
#[fail(display = "Not a char boundary: index {} at '{}'.", index, string)]
pub struct NotACharBoundary {
index: usize,
string: String,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put it in Errors section somewhere at the top.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 1015 to 1019
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())
fn call_info(&self, _id: node::Id, _name: Option<&str>) -> Option<CalledMethodInfo> {
None
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have Empty context for such cases: https://github.com/enso-org/enso/blob/d8f6f3fd52d87f63d8b067b609162423ed23ebcc/app/gui/language/span-tree/src/generate/context.rs

So, if we are not in some weird generics' machinery, the controller should not implement Context at all, and use Empty instead if itself where needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, also removed a couple of tests that relied on arguments information from node metadata.

let old_context = old_input.context().map(|ctx| ctx.into_ast().repr());

self.invalidate_picked_suggestions();
let context_changed = old_context != new_context;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct. Before we were filtering the components by expected argument type (and it depended on the left side of the expression), but now the filtering depends only on accessor chain (i.e. the context).

@@ -264,7 +264,7 @@ pub mod mock {
let mut json_client = language_server::MockClient::default();
let mut binary_client = binary::MockClient::new();
// Creating a searcher controller always triggers a query for completion.
controller::searcher::test::expect_completion(&mut json_client, &[]);
// controller::searcher::test::expect_completion(&mut json_client, &[]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented code. Remove if unneeded.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed it.

@@ -850,6 +852,8 @@ ensogl::define_endpoints! {
/// Set the node expression.
set_expression (node::Expression),

edit_expression (text::Range<Byte>, ImString),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing docs.

@sylwiabr sylwiabr requested a review from kazcw February 28, 2023 18:39
Copy link
Contributor

@kazcw kazcw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. @farmaazon I think this answers my questions about how the frontend uses the Scala doc parser!

self.graph.graph().set_expression(node_id, expression)?;
if let Mode::NewNode { .. } = self.mode.as_ref() {
let graph = self.graph.graph();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this bound for convenience, or to capture the value returned by graph() before a subsequent operation changes it? If it's for convenience it should be used consistently so it doesn't look like something tricky is happening; if it is intentionally capturing an older value, a comment would be helpful.

Comment on lines 322 to 324
/// Return `true` if the `index` is on a char boundary inside `string` and can be used as a
/// range boundary. Otherwise, return `false` and report an error.
fn ensure_on_char_boundary(string: &str, index: usize) -> FallibleResult<()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs don't match return type.

let cursor_position_in_name = self.cursor_position - edited.range.start;
let name = ast::identifier::name(&edited.ast);
name.map_or("", |n| {
&n[text::Range::new(text::Byte(0), text::Byte(0) + cursor_position_in_name)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This slice can panic if edited_ast and cursor_position are inconsistent. That shouldn't happen, but the invariant relied on is not trivial--slicing with get here would reduce the consequences of potential future bugs.

@farmaazon
Copy link
Contributor

QA report

Issues

When starting editing node in the middle of identifier, I see CB with all entries first, and the filtering appears only after a moment.

cursor-aware-test-2023-03-01_13.47.34.mp4

Bad UX

Besides the above issue, the PR meets the acceptance criteria. Still, when testing, I found additional things worsening my UX:

  • When writing literals (text, number), the results are no longer filtered, so we consistently try to add the first selected entry (usually Column.from_vector) to the input. Before, because literals were part of pattern (and usually no entry matched the literal) the component browser was empty. The solution would be to not display suggestions when the cursor is in or next to a literal, or, even better, we could display a single entry, for example "foo bar" if user puts "foo bar, or 43 if user types 43 - but this is worth a separate task.
  • When editing existing static method invocation, accepting the same method adds the type name again, resulting in erroneous code. It may also be seen as error in visualization preview:

Screenshot from 2023-03-01 13-50-13

Solution for it is implementing #5356 we may consider keeping this branch unmerged until we implement and merge that task to it.

@wdanilo
Copy link
Member

wdanilo commented Mar 17, 2023

@vitvakatu what is the status of the PR? It was accepted 2 weeks ago (!), and the QA was done then. Why is it not merged yet?

@vitvakatu
Copy link
Contributor Author

@wdanilo as we agreed with @farmaazon, we will merge #5356 into this PR, and then merge the whole thing. The QA done 2 weeks ago discovered a few issues, and they were addressed during that two weeks.

@wdanilo
Copy link
Member

wdanilo commented Mar 17, 2023

@vitvakatu gotcha! Sorry, I forgot about it. Please make short notes about agreements in the future so we will not be missing them. Thanks again! :)

@wdanilo
Copy link
Member

wdanilo commented Mar 22, 2023

@vitvakatu as we are making changes here, could you please move it to draft stage and request review again after the code is final, please?

@vitvakatu vitvakatu marked this pull request as draft March 22, 2023 07:57
Closes #5356

Implements recognition of qualified names in the input of the CB. Now accepting of the suggestion produces a valid code, and only necessary imports are added.


https://user-images.githubusercontent.com/6566674/225487445-855dfb04-47c7-44c9-bc92-4bb5663d500a.mp4


https://user-images.githubusercontent.com/6566674/225487486-028d1ae0-02e0-4bbc-a623-7d4e620c87d9.mp4

# Important Notes
- Filtering by inserted code no longer works, but instead, we filter out entries with qualified names not matching the edited accessor chain. It may result in worsened behavior when typing expressions with local variables as `this` argument.
@vitvakatu vitvakatu marked this pull request as ready for review March 22, 2023 13:15
@vitvakatu
Copy link
Contributor Author

QA was done in #5960 on an identical state of the code.

@vitvakatu vitvakatu added the CI: Ready to merge This PR is eligible for automatic merge label Mar 22, 2023
@vitvakatu vitvakatu removed the CI: Ready to merge This PR is eligible for automatic merge label Mar 22, 2023
@vitvakatu vitvakatu added the CI: Ready to merge This PR is eligible for automatic merge label Mar 22, 2023
@mergify mergify bot merged commit 1b30a52 into develop Mar 22, 2023
@mergify mergify bot deleted the wip/farmaazon/cursor-aware-cb-183521918 branch March 22, 2023 17:10
mergify bot pushed a commit that referenced this pull request Mar 23, 2023
Fix a stupid typo introduced in #5770. It affected the code generated by CB while previewing suggestions.
Procrat added a commit that referenced this pull request Mar 27, 2023
* develop:
  Layout fixes (#6066)
  Use new Enso Hash Codes and Comparable (#6060)
  Search suggestions by static attribute (#6036)
  Use .node-version for pinning Node.js version (#6057)
  Fix code generation for suggestion preview (#6054)
  Implementation of EnsoGL predefined Rectangle shape. (#6033)
  Tidy up the public module level statics (#6032)
  Cursor aware Component Browser (#5770)
mergify bot pushed a commit that referenced this pull request Mar 28, 2023
The test was mistakenly removed during the merge with develop in #5770. The code is now restored from develop version without any modifications.

Thanks to @Frizi for heads up.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CI: No changelog needed Do not require a changelog entry for this PR. CI: Ready to merge This PR is eligible for automatic merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Finish Component Browser suggestions based on the cursor position
5 participants