This is an archive of the discontinued LLVM Phabricator instance.

[clangd] Support multifile edits as output of Tweaks
ClosedPublic

Authored by kadircet on Aug 23 2019, 3:45 AM.

Details

Summary

First patch for propogating multifile changes from tweak outputs to LSP
WorkspaceEdits.

Uses FS to convert tooling::Replacements to TextEdits.
Errors out if there are any inconsistencies between the draft version and the
version generated the edits.

Diff Detail

Repository
rL LLVM

Event Timeline

kadircet created this revision.Aug 23 2019, 3:45 AM
Herald added a project: Restricted Project. · View Herald TranscriptAug 23 2019, 3:45 AM
ilya-biryukov added inline comments.Aug 23 2019, 5:20 AM
clang-tools-extra/clangd/ClangdLSPServer.cpp
644 ↗(On Diff #216794)

This callback is called asynchronously and the version inside DraftMgr may be newer than the one we used to calculate the original offsets inside replacements at this point.

We should not rely on DraftMgr, doing conversions on the source buffers inside SourceManager is probably the most reliable option we have.

kadircet marked an inline comment as done.Aug 23 2019, 5:35 AM
kadircet added inline comments.
clang-tools-extra/clangd/ClangdLSPServer.cpp
644 ↗(On Diff #216794)

This callback is called asynchronously and the version inside DraftMgr may be newer than the one we used to calculate the original offsets inside replacements at this point.

That's exactly what we are checking though?

We want to bail out if user has a "different" version of the source code in their editor by the time we generated edits. Since editors will most likely try to apply the edits onto the version in editor.

sammccall added inline comments.Aug 23 2019, 5:36 AM
clang-tools-extra/clangd/ClangdLSPServer.cpp
644 ↗(On Diff #216794)

I think you've misunderstood what this code is doing (it needs comments!)

The SourceManager contains the code that we've calculated the edits against, by definition. So checking against it does nothing.

When we send the edits to the client, it's going to apply them to the file if it's not open, but to the dirty buffer if it is open.
If the dirty buffer has different contents than what we saw (and calculated edits against) then the edits will be wrong - that's what this code guards against.

So looking at the latest available content in the draftmgr (at the end of the request, not the start!) is the right thing here, I think.

646 ↗(On Diff #216794)

We're comparing here content in the source manager (which was read as bytes from disk, and converted to utf8) to that from LSP (which was sent as unicode text by the editor, and converted to utf8).

Are editors going to preserve the actual line endings from disk when sending LSP, or are they going to use the "array-of-lines" from their editor's native model? (e.g. join(lines, '\n'))

If the latter, we'd need to normalize line endings, possibly strip trailing line endings, etc.
I ask because clangd-vim definitely has an "array-of-lines" model :-)

sammccall added inline comments.Aug 23 2019, 5:42 AM
clang-tools-extra/clangd/SourceCode.h
41 ↗(On Diff #216794)

Hmm, it is also attractive to make this something like

struct Edit {
  string CodeBefore;
  tooling::Replacements Reps;

  string codeAfter();
  std::vector<TextEdit> asTextEdits();
  bool canApplyTo(StringRef CodeBefore); // does line-ending normalization
}

if we were worried about abuse, we could make CodeBefore private

43 ↗(On Diff #216794)

, if possible.
(Sometimes we're just sending these over the wire)

46 ↗(On Diff #216794)

document that these are equivalent.
Why is Edits optional?

46 ↗(On Diff #216794)

nit: reps -> replacements
ContentDigests -> ContentDigest

49 ↗(On Diff #216794)

make this a constructor?

ilya-biryukov marked an inline comment as done.Aug 23 2019, 5:48 AM
ilya-biryukov added inline comments.
clang-tools-extra/clangd/ClangdLSPServer.cpp
644 ↗(On Diff #216794)

You are right, I'm sorry. I did not read into the code, just assumed it was doing something it doesn't.
Again, very sorry for the disturbance.

kadircet updated this revision to Diff 216823.Aug 23 2019, 6:35 AM
kadircet marked 8 inline comments as done.
  • Address comments
sammccall added inline comments.Aug 28 2019, 8:07 AM
clang-tools-extra/clangd/SourceCode.cpp
601 ↗(On Diff #216823)

you seem to be checking this both here and in clangdlspserver. Why?

clang-tools-extra/clangd/SourceCode.h
42 ↗(On Diff #216823)

nit: drop "represents"

42 ↗(On Diff #216823)

nit: this could also describe Replacements, vector<TextEdit>, etc. Motivate this class a little more?

215 ↗(On Diff #216823)

not sure what "generates TextEdits from those" refers to.

Could this function be called "reformatEdits" or "formatAroundEdits"?

220 ↗(On Diff #216823)

This signature is confusing: we pass code in *three* different ways (FS, Edits, and MainFileCode). Much of this is because we put the loop (and therefore all special cases) inside this function.
The logic around the way the VFS is mapped for the main file vs others doesn't really belong in this layer. Neither does "please save"...

It seems this wants to be something simpler/smaller like reformatEdit(const Edit&, const Style&) that could be called from ClangdServer. There's probably another helper like checkApplies(const Edit&, VFS*) that would go in ClangdLSPServer.

clang-tools-extra/clangd/refactor/Tweak.h
74 ↗(On Diff #216823)

what's the difference between None and an empty map?

76 ↗(On Diff #216823)

(if the null map case goes away, this can too)

81 ↗(On Diff #216823)

This greatly increases the burden of callers of this function, who mostly want to edit the current file.

  • need to provide a fully-qualified name (I don't think the implementations in this patch actually do that)
  • need to construct an Edit

can we provide an Effect mainFileEdit(const SourceManager&, Replacements)? and maybe Effect fileEdit(const SourceManager&, FileID, Replacements) though maybe the latter doesn't cover enough cases to pull its weight.

kadircet marked 14 inline comments as done.Aug 29 2019, 2:28 AM
kadircet added inline comments.
clang-tools-extra/clangd/SourceCode.cpp
601 ↗(On Diff #216823)

the contents in here are coming from disk, whereas the one in clangdlspserver is coming from drafts. I suppose it would make more sense to merge the two in clangdlspserver, use draft manager as content source backed by disk in case user hasn't opened that file.

clang-tools-extra/clangd/SourceCode.h
215 ↗(On Diff #216823)

not sure what "generates TextEdits from those" refers to.

Leftover from an old-version.

220 ↗(On Diff #216823)

as discussed offline for reformatEdit to work, made InitialCode in Edit public. As both formatting and style deduction requires access to file contents.

for checkApplies, it requires access to both VFS and DraftMgrand pretty special use-case for clangdlspserver, so don't think the function would carry its weight. Therefore left the check inlined.

clang-tools-extra/clangd/refactor/Tweak.h
74 ↗(On Diff #216823)

None :P

kadircet updated this revision to Diff 217803.Aug 29 2019, 2:29 AM
kadircet marked 4 inline comments as done.
  • Address comments
kadircet updated this revision to Diff 217806.Aug 29 2019, 2:38 AM
  • Handle formatting error in ClangdServer rather than just printing it
sammccall added inline comments.Aug 29 2019, 3:17 AM
clang-tools-extra/clangd/SourceCode.cpp
601 ↗(On Diff #216823)

OK, so you're checking here whether the contents-on-disk have changed between clang reading the file as part of parse, and the tweak completing?

I don't think this is worth doing. Sure, there's potentially a race condition, but it's relatively small (bounded on the order of a few seconds, vs unbounded for unsaved drafts) and we can't eliminate it entirely (file on disk can change between when we create edits and when they're applied).

kadircet updated this revision to Diff 217820.Aug 29 2019, 3:31 AM
kadircet marked an inline comment as done.
  • Get rid off compatibility check for files that are not open in the editor.
sammccall added inline comments.Sep 4 2019, 1:37 AM
clang-tools-extra/clangd/ClangdLSPServer.cpp
647 ↗(On Diff #217820)

please pull out a method for this part
e.g. Error validateEdits(...)

654 ↗(On Diff #217820)

As these are absolute paths, we probably want to invert the error message for readability, and at least count the files.

e.g.
"Files must be saved first: path/to/Foo.cpp (and 3 others)"

clang-tools-extra/clangd/ClangdServer.cpp
414 ↗(On Diff #217820)

isn't this just
elog("Failed to format {0}: {1}", std::move(Err))

clang-tools-extra/clangd/SourceCode.cpp
856 ↗(On Diff #217820)

documentation

870 ↗(On Diff #217820)

why?

clang-tools-extra/clangd/SourceCode.h
42 ↗(On Diff #217820)

move this class near the related code? (replacementToEdit etc)

clang-tools-extra/clangd/refactor/Tweak.cpp
90 ↗(On Diff #217820)

how do you know this is the correct/absolute path for the file? Can we add tests?

clang-tools-extra/clangd/refactor/Tweak.h
73 ↗(On Diff #217820)

is it possible to be more specific than "absolute"?

130 ↗(On Diff #217820)

what will this be used for?

You can use at most one of these Effect factories (unless we're going to do some convoluted merge thing), so this seems useful if you've got exactly one edit to a non-main file.

It seems more useful to return a pair<string, Edit> which could be inserted into a map

130 ↗(On Diff #217820)

why are these moved out of the Effect class?

133 ↗(On Diff #217820)

nit: convenience

kadircet marked 13 inline comments as done.Sep 4 2019, 3:52 AM
kadircet added inline comments.
clang-tools-extra/clangd/ClangdServer.cpp
414 ↗(On Diff #217820)

IIUC, just printing the error doesn't actually mark it as checked, therefore causes an assertion failure (see getPtr vs getPayload in llvm::Error). And I couldn't see any special handling in elog, but not sure if formatv object has this.

clang-tools-extra/clangd/refactor/Tweak.cpp
90 ↗(On Diff #217820)

changed it to use FileInfo::getName rather then FileEntry::getName, as the former says:

/// Returns the name of the file that was used when the file was loaded from
/// the underlying file system.

These also get tested through any lit tests we have for tweaks, adding more unittests.

clang-tools-extra/clangd/refactor/Tweak.h
130 ↗(On Diff #217820)

need to provide a fully-qualified name (I don't think the implementations in this patch actually do that)

I've thought you were also requesting these to be helper functions rather than static methods so that callers don't need to qualify the call.
Can move it back into class if I misunderstood your point, but this actually seems a lot cleaner.

130 ↗(On Diff #217820)

what will this be used for?

you are right it doesn't really compose well, changed it to return the pair instead.

kadircet updated this revision to Diff 218637.Sep 4 2019, 3:52 AM
kadircet marked 2 inline comments as done.
  • Address comments
kadircet updated this revision to Diff 218878.Sep 5 2019, 3:15 AM
kadircet marked 2 inline comments as done.
  • Define more strict semantics around filename
clang-tools-extra/clangd/refactor/Tweak.h
130 ↗(On Diff #217820)

moved back into class

kadircet edited the summary of this revision. (Show Details)Sep 5 2019, 3:50 AM
sammccall accepted this revision.Sep 9 2019, 4:35 AM
sammccall added inline comments.
clang-tools-extra/clangd/ClangdServer.cpp
414 ↗(On Diff #217820)

The special handling is on Logger.h:38. Do you see crashes?

clang-tools-extra/clangd/refactor/Tweak.cpp
97 ↗(On Diff #218883)

nit: "for edited file: "...

This revision is now accepted and ready to land.Sep 9 2019, 4:35 AM
kadircet updated this revision to Diff 219324.Sep 9 2019, 4:49 AM
kadircet marked 4 inline comments as done.
  • Address comments
clang-tools-extra/clangd/ClangdServer.cpp
414 ↗(On Diff #217820)

ah OK, didn't notice the fmt_consume previously. no, I wasn't seeing any crashes.

This revision was automatically updated to reflect the committed changes.
Herald added a project: Restricted Project. · View Herald TranscriptSep 9 2019, 5:30 AM