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

Support textDocument/prepareRename & textDocument/documentHighlight, improve accuracy of reference finding #732

Open
wants to merge 23 commits into
base: main
Choose a base branch
from

Conversation

4teapo
Copy link
Contributor

@4teapo 4teapo commented Aug 9, 2024

Zed requires that language servers support textDocument/prepareRename for symbol renaming to work. Zed also requires textDocument/documentHighlighting for highlighting to work, and this request is also used to mark everything that is about to get renamed when you enter symbol renaming mode. This PR adds support for both these capabilities.

Before, with the current type solver, finding property references didn't recognize assigning to properties, meaning foo in tbl.foo = bar wouldn't be recognized as a reference to the foo property inside of tbl. This PR changes the logic behind finding property references to instead go through the AST, which also makes it possible to properly determine if symbols should be marked as Read or Write for the documentHighlight request.

Previously, finding references of imported modules in types also didn't work with scopes or shadowing. This PR alters the algorithm so that it stops processing a statement block when the symbol is shadowed by a module import, and when the scope in which the symbol is declared ends, which also is an optimization.

The old findAllReferences function would also allow finding references of properties of globals, which makes an assertment fail in debug builds. With these changes, it returns early instead.

Resolves 4teapo/zed-luau#2.

4teapo and others added 5 commits August 9, 2024 01:37
…ight, improve reference results

Zed requires servers to support textDocument/prepareRename in order for symbol renaming to work. Zed also doesn't do any highlighting without textDocument/documentHighlight, and this request is also used to create a visual effect, highlighting everything that will get renamed when you start the symbol renaming action. This commit adds support for both these capabilities.

It also resolves a previous issue regarding variable shadowing and module imports when searching for symbols, and makes assigning fields in tables work with references (before, `bar` wouldn't be recognized as a reference in `foo.bar = baz`, at least with the old solver, provided the type of `foo` contains `bar`).
@JohnnyMorganz
Copy link
Owner

Apologies, but if it's not too difficult could you split out these different features + bug fixes into separate PRs? It makes it a bit easier on me to review, but it's alright if not.

Could you also add changelog entries for each of them too?

I hope to review within the next week

@4teapo
Copy link
Contributor Author

4teapo commented Aug 9, 2024

I don't think there's a good way to separate these into different PRs, because the two main bug fixes are both related to AST traversal structures that depend on lsp::DocumentHighlightKind. I could put prepareRename in a different one, but it's a very small part of this.

@JohnnyMorganz
Copy link
Owner

Fair enough

Could you also write some test cases for the bugs you fixed and the 2 new features you added?

@4teapo
Copy link
Contributor Author

4teapo commented Aug 12, 2024

I just added some test cases that capture the things I've added and fixed.

@JohnnyMorganz
Copy link
Owner

So sorry for the delay, will try to take a look this weekend

Copy link
Owner

@JohnnyMorganz JohnnyMorganz left a comment

Choose a reason for hiding this comment

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

Thank you for this! And so sorry for the delay.

I think the fundamentals look sound to me, just some more minor comments about some refactors / tests

@@ -12,6 +12,8 @@ static bool isGlobalBinding(const WorkspaceFolder* workspaceFolder, const lsp::R
auto position = textDocument->convertPosition(params.position);

auto sourceModule = workspaceFolder->frontend.getSourceModule(moduleName);
if (!sourceModule)
Copy link
Owner

Choose a reason for hiding this comment

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

When would we hit such a case?

if (!sourceModule)
throw JsonRpcException(lsp::ErrorCode::RequestFailed, "Unable to read source code");

auto exprOrLocal = findExprOrLocalAtPositionClosed(*sourceModule, position);
Copy link
Owner

Choose a reason for hiding this comment

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

I see a lot of this code is still duplicated from References.cpp.

AIUI, DocumentHighlight should basically use the exact same logic (and hence return the exact same data) as textDocument/references, except filtered down to the current file and maybe with R/W data.

Can we extract this out into one whole thing that is used by both? (similar to how rename calls out to find all references). This "universal" setup would return all the details, then references would filter out the R/W data, whilst documenthighlight will filter out just those in the current file. If that makes sense

Copy link
Owner

Choose a reason for hiding this comment

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

If it's easier, we can also do this in a followup

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 think document highlight should process only the current file for performance reasons, if you mean that it could process like the references request but only use the results for the current file. If I remember correctly, there's nothing preventing us from having a more unified setup, so I can implement that.

CHANGELOG.md Show resolved Hide resolved

explicit FindSymbolReferences(Luau::Symbol symbol)
explicit FindSymbolReferences(Luau::Symbol symbol, bool withKinds)
Copy link
Owner

Choose a reason for hiding this comment

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

To make this simpler, let's just always add kinds. The consumer can then choose to discard the info if necessary.

Hence result should just store a std::pair<Location, Kind>

@@ -624,7 +712,8 @@ struct FindSymbolReferences : public Luau::AstVisitor
{
if (visitLocal(var))
{
result.push_back(var->location);
addResult(var->location, lsp::DocumentHighlightKind::Write);
Copy link
Owner

Choose a reason for hiding this comment

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

this is the "i" in for i,v in ... do right? In that case, should it be a read?

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, it is the i,v. I think all loop variables should be write, because it's basically declaring a variable (similar to local i, v = fun(...))? Sumneko lua has them be write too.

if (ttv->definitionModuleName.empty())
// When the definition module name is starts with '@', e.g. "@luau" or "@roblox", `LUAU_ASSERT(!buildQueueItems.empty());` in Frontend.cpp fails,
// after we call checkStrict below
if (!ttv || ttv->definitionModuleName.empty() || ttv->definitionModuleName[0] == '@')
Copy link
Owner

Choose a reason for hiding this comment

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

Ideally we map this to a definitions file in future.

Can you add a test and a TODO for this?

@@ -637,19 +726,205 @@ struct FindSymbolReferences : public Luau::AstVisitor

bool visit(Luau::AstTypeReference* typeReference) override
{
// TODO: this is not *completely* correct in the case of shadowing, as it is just a name comparison
Copy link
Owner

Choose a reason for hiding this comment

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

Please keep the TODO, but instead mention that this can be simplified in future

Comment on lines +170 to +172
auto requireInfo = findClosestAncestorModuleImport(*sourceModule, reference->prefix.value(), reference->prefixLocation->begin);
if (!requireInfo)
return std::nullopt;
Copy link
Owner

Choose a reason for hiding this comment

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

Do we need this? We don't use the result

tests/PrepareRename.test.cpp Show resolved Hide resolved
tests/PrepareRename.test.cpp Show resolved Hide resolved
@JohnnyMorganz
Copy link
Owner

FWIW, still waiting on the comments being resolved here before merging this. Let me know if there are any issues with it, I can also take a look

@4teapo
Copy link
Contributor Author

4teapo commented Nov 11, 2024

FWIW, still waiting on the comments being resolved here before merging this. Let me know if there are any issues with it, I can also take a look

I've had to put this aside for a long time, so I haven't been able to resolve the comments yet. Hopefully I can get to it soon.

I've been using luau-lsp with these changes personally and haven't experienced any bugs so far, so AFAIK there's no issues of that sort

@JohnnyMorganz
Copy link
Owner

Cool no worries, just wanted to double check you weren't expecting anything since I noticed you're still merging main into this PR

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.

Can't rename symbols
2 participants