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

Improved hover support: Structs, qualified calls and types, more info for modules #356

Merged
merged 33 commits into from
Sep 22, 2023
Merged

Conversation

zachallaun
Copy link
Collaborator

@zachallaun zachallaun commented Sep 5, 2023

This PR builds on #331, adding hover support for additional entities:

  • Structs
  • Qualified calls
  • Qualified types

Improvements:

  • Generally improved formatting for all hover content.
  • Respond with range information to support editor highlighting.
  • Include callback signatures and docs for modules that are behaviours.

Notes:

  • Add vendored Future.Code.Typespec, which includes required fixes in @spec to make Dialyzer happy.
  • Update :sourceror dep to the latest commit until a new release is cut. This latest version updates Sourceror's zippers to be a %Zipper{} struct and includes fixes/improvements to Sourceror.get_range/1.
  • Lexical.Ast now uses the unescape: false option when parsing code. See ba4223d for more context.

@zachallaun
Copy link
Collaborator Author

Struct hover:

image

Qualified call hover:

image

@zachallaun
Copy link
Collaborator Author

@scohen would appreciate your input on this:

In d1edb31, I added some additional utilities to Lexical.RemoteControl.Modules that deal with extracting information from BEAM object code -- docs, types, specs, and callbacks. While I was looking at Code.Typespec to better understand some things, I noticed that all of the functions will also accept the beam object binary, which means they don't have to read it off disk (and we don't have to wait for just-compiled files to be written to disk).

Unfortunately, this introduced a Dialyzer error -- the @spec for these functions only accepts module() even though the guards are for is_atom(module) or is_binary(module). See here.

Options I can think of:

  1. Revert the changes and just operate on modules
  2. Vendor Code.Typespec into Future.Code.Typespec, which I think would cause the Dialyzer error to be ignored because lib/elixir/lib/future is in dialyzer.ignore-warnings
  3. Add a bunch of @dialyzer {:nowarn_function, ...} attributes, but they'd have to go both in the Modules utility and where it's being used, which is gross.

@scohen
Copy link
Collaborator

scohen commented Sep 5, 2023

@zachallaun this looks like a typespec bug in elixir, we should fix that too, then pull the fix into Future.Code, which we do already.

Copy link
Collaborator

@scottming scottming left a comment

Choose a reason for hiding this comment

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

For me, it looks good overall. However, I'm not sure if we need the assert_hover abstraction. Can you explain the reason behind it?

apps/common/lib/lexical/ast.ex Outdated Show resolved Hide resolved
@zachallaun
Copy link
Collaborator Author

For me, it looks good overall. However, I'm not sure if we need the assert_hover abstraction. Can you explain the reason behind it?

I think you're right. It was my attempt to clean up the tests a bit, but I think they'll be just as clear and more explicit just using variables (if a little bit longer).

@zachallaun zachallaun changed the title Improved textDocument/hover support Improved hover support: Structs, qualified calls and types, more info for modules Sep 15, 2023
@zachallaun zachallaun marked this pull request as ready for review September 15, 2023 15:43
@zachallaun
Copy link
Collaborator Author

@scohen @scottming This PR is ready for review. Please see the updated OP for additional context/notes.

While I was originally planning for this PR to also include hover support for unqualified calls, I chose to punt on that until the next one. Resolving unqualified calls is a lot more complex, since it requires import tracking, so I plan to address it separately.

Copy link
Collaborator

@scottming scottming left a comment

Choose a reason for hiding this comment

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

Only a few minor comments were left. Overall, I feel that this improvement is excellent.

@scottming
Copy link
Collaborator

scottming commented Sep 18, 2023

Another distracting element that I think can be removed is the block separator symbol ---. I find it too bright in the nvim editor, which causes distraction.

Of course, it would be good to have the option to customize the configuration and decide whether we need this separator.

CleanShot 2023-09-18 at 19 30 13@2x

Here's what it looks like when it's removed:

CleanShot 2023-09-18 at 19 34 48@2x

In vscode, the difference is minor.

before

CleanShot 2023-09-18 at 19 35 26@2x

after removing

CleanShot 2023-09-18 at 19 36 43@2x

@zachallaun
Copy link
Collaborator Author

@scottming I think that's fair; perhaps the --- can be reserved for only separating between arities. (It shows docs for arities >= the one currently used.)

This is more of a meta-thought, but I wish there were a way for all of us to easily test these kinds of features in all major editors. Maybe it's worth setting up an "editor test configuration" repo that contains config for all the major editors and scripts to launch the editor using that config. Needs more thought, but could be useful in the long term.

Copy link
Collaborator

@scottming scottming left a comment

Choose a reason for hiding this comment

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

Very good, you're awesome, @zachallaun . Thank you.

Another improvement may be to convert the Examples section to an Elixir code block, but feel free to do that in another PR.

CleanShot 2023-09-19 at 22 07 10@2x

@zachallaun
Copy link
Collaborator Author

Another improvement may be to convert the Examples section to an Elixir code block, but feel free to do that in another PR.

@scottming This is a great idea! I wanted to see if I could sneak it in, but it's unfortunately a bit more complicated than it first seems and, I think, should be in its own PR. Basically, we need to handle all of these cases:

Indented code in an explicit code block shouldn't be re-wrapped:

```
    some_code
```

Multiple indented blocks separated by empty lines should be joined together into a single block:

## Examples

    some_code

    with_an_empty_line_between

Code indented relative to a parent element (like a list item) should be wrapped, but retain the parent's indentation:

## List of items

  - Something with a code example:

        some_code_4_spaces_in_from_Something

I think there's a somewhat simple way to do this that doesn't require a full Markdown parser by basically map-reducing over the lines, looking for lines that start with ``` to ignore until the block ends, and then tracking the indentation level of the previous line so that indented blocks can be identified by current_indent == previous_line_indent + 4.

But it's enough work that I don't want to include it here :)

Copy link
Collaborator

@scohen scohen left a comment

Choose a reason for hiding this comment

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

This looks very good. I like the abstractions and what you've done with cursor support.

I have some comments along the lines of how positions are represented and if we can do better in tests with defining ranges and positions, but this is nearly ready.

apps/common/lib/lexical/ast.ex Show resolved Hide resolved
apps/common/lib/lexical/ast.ex Show resolved Hide resolved
apps/common/lib/lexical/ast.ex Outdated Show resolved Hide resolved
apps/common/mix.exs Outdated Show resolved Hide resolved
apps/common/test/lexical/ast_test.exs Outdated Show resolved Hide resolved
apps/server/lib/lexical/server/provider/handlers/hover.ex Outdated Show resolved Hide resolved
This moves all of the fetching of docs, specs, types, and callbacks into
`Lexical.RemoteControl.Modules`. Those functions now also operate on
BEAM object code, which means the object code doesn't need to be
repeatedly read from disk. The object code for a module also appears to
be available immediately after calling `Code.ensure_compiled(module)`,
which allows us to get rid of a timeout/loop that was waiting for it to
be written to disk.
The correct guard logic for a call is quite verbose, as you must ensure
that the call in a `{call, meta, args}` node isn't a special form, like
`:__aliases__`.
This commit currently pulls in a to-be-merged branch of `Sourceror` which
contains fixes and changes around `Sourceror.get_range/1`. These are required
in order to accurately determine the cursor position in an AST.

An additional important change that this commit makes is to add the `unescape:
false` option when converting strings to quoted forms. This is needed to
determine whether newlines in strings were escaped literals (`\n`) or
multi-line.

Take this example:

    iex> Code.string_to_quoted!(~S|"foo\nbar"|)
    "foo\nbar"

    iex> Code.string_to_quoted!(~S|"foo
    ...> bar"|)
    "foo\nbar"

    iex> Code.string_to_quoted!(~S|"foo\nbar"|, unescape: false)
    "foo\\nbar"

    iex> Code.string_to_quoted!(~S|"foo
    ...> bar"|, unescape: false)
    "foo\nbar"

Note that, with `unescape: false`, there is a difference between the strings.
There were a few commonalities in all existing usage of `CursorSupport`:

1. In almost all usage, `cursor_position` and `strip_cursor` are used
   together.
2. In all usage, the returned `{line, column}` tuple could/should be
   a `Lexical.Document.Position`.
3. In many cases, the stripped text is then used to create
   a `Lexical.Document`.

To address these, a higher level `pop_cursor/2` has been added, returning
`{%Position{}, stripped_text}` by default. However, by using the `:as` or
`:document` options, `pop_cursor/2` can return `{%Position{}, %Document{}}`,
reducing a significant amount of boilerplate.

This change also has the effect of reducing the number of tests requiring
`PositionSupport`.
When resolving modules, we'll exclude segments trailing the cursor so
that you can walk up a module to better understand its context. For
instance, for `My.|Nested.Module`, we resolve to `My.Nested`.

Using the same behavior for structs makes less sense. If you're hovering
a module being used as a struct, often only the full alias is actually
a struct; the parents might not be. For instance, with
`%My.|Nested.Struct{}`, resolving to `My.Nested` will not yield useful
results. Because of this, the full alias is now always resolved in a
struct context, so the above would resolve to the struct
`My.Nested.Struct`.
@zachallaun
Copy link
Collaborator Author

Draft squash commit:

  • Include the range of resolved entities when responding to hover requests

This allows editors to highlight the token that the hover content applies to. For instance, if you hovered Baz in the module Foo.Bar.Baz, the entire module will be highlighted, not just the hovered segment in the module.

  • Improve formatting and docs when hovering modules

Behaviours (modules that have @callback attributes) will now show callback specs and below any module docs.

  • Don't show return content when hovering modules without docs

This applies to modules that use @moduledoc false or modules that are missing a @moduledoc attribute and don't have other additional docs to show (e.g. behaviour callbacks).

  • Add support for hovering structs

When hovering a module being used as a struct, e.g. %MyStruct{}, MyStruct.t types will now be shown before any additional module docs.

  • Add support for hovering qualified functions, macros, and types

Qualified calls are those that begin with a module, e.g. MyModule.my_fun(). When hovering qualified functions or macros, the signatures of those calls will be shown followed by any specs and docs. When hovering qualified types, the type definition will be shown followed by any docs.

For functions with multiple arities, the arity currently being used will try to be inferred and those definitions/docs shown first, followed by those for any higher arities.

  • [Internal] Add additional utilities to Lexical.Ast, including path_at/2 and contains_position?/2

  • [Internal] Add Lexical.Test.CursorSupport.pop_range/2

  • [Internal] Add Lexical.Test.RangeSupport.pop_range/1

  • [Internal] Update Sourceror to 0.14

This version bump includes more complete and resilient APIs for getting ranges, as well as a new %Zipper{} struct that replaces the old {tree, meta} tuple that previously was used to represent zippers.

@zachallaun zachallaun merged commit 9bff1bb into lexical-lsp:main Sep 22, 2023
6 checks passed
@zachallaun zachallaun deleted the za-hover-2 branch September 22, 2023 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants