Skip to content
This repository was archived by the owner on Aug 30, 2025. It is now read-only.

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

Merged
zachallaun merged 33 commits into
lexical-lsp:mainfrom
zachallaun:za-hover-2
Sep 22, 2023
Merged

Improved hover support: Structs, qualified calls and types, more info for modules#356
zachallaun merged 33 commits into
lexical-lsp:mainfrom
zachallaun:za-hover-2

Conversation

@zachallaun

@zachallaun zachallaun commented Sep 5, 2023

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
Collaborator Author

Struct hover:

image

Qualified call hover:

image

@zachallaun

Copy link
Copy Markdown
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

scohen commented Sep 5, 2023

Copy link
Copy Markdown
Collaborator

@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.

@scottming scottming left a comment

Copy link
Copy Markdown
Collaborator

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?

Comment thread apps/common/lib/lexical/ast.ex Outdated
@zachallaun

Copy link
Copy Markdown
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
Copy Markdown
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.

@scottming scottming left a comment

Copy link
Copy Markdown
Collaborator

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.

Comment thread apps/common/lib/lexical/ast.ex Outdated
Comment thread apps/remote_control/lib/lexical/remote_control/code_intelligence/docs.ex Outdated
Comment thread apps/server/lib/lexical/server/code_intelligence/entity.ex
Comment thread apps/server/lib/lexical/server/code_intelligence/entity.ex Outdated
Comment thread apps/server/lib/lexical/server/provider/handlers/hover.ex Outdated
Comment thread apps/server/test/lexical/server/code_intelligence/entity_test.exs
@scottming

scottming commented Sep 18, 2023

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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.

@scottming scottming left a comment

Copy link
Copy Markdown
Collaborator

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

Comment thread projects/lexical_test/lib/lexical/test/cursor_support.ex
Comment thread apps/common/test/lexical/ast_test.exs
@zachallaun

Copy link
Copy Markdown
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 :)

@scohen scohen left a comment

Copy link
Copy Markdown
Collaborator

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.

Comment thread apps/common/lib/lexical/ast.ex
Comment thread apps/common/lib/lexical/ast.ex
Comment thread apps/common/lib/lexical/ast.ex Outdated
Comment thread apps/common/mix.exs Outdated
Comment thread apps/common/test/lexical/ast_test.exs Outdated
Comment thread apps/server/lib/lexical/server/provider/markdown.ex
Comment thread apps/server/lib/lexical/server/provider/markdown.ex
Comment thread apps/server/lib/lexical/server/provider/handlers/hover.ex Outdated
Comment thread apps/server/test/lexical/server/code_intelligence/entity_test.exs
Comment thread apps/server/test/lexical/server/code_intelligence/entity_test.exs
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
Copy Markdown
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
@zachallaun zachallaun deleted the za-hover-2 branch September 22, 2023 12:42
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants