-
Notifications
You must be signed in to change notification settings - Fork 347
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
Implement 'find in JAR files' LSP extension #3093
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Demonstration looks awesome! Thanks for this contribution!
The approach in general looks good.
The only thing is that I'm not sure if it should be named Find in non-source files
.
Maybe Search in dependency sources
? Anyway let's wait what others will say.
I left some suggestions to the code:
metals/src/main/scala/scala/meta/internal/metals/FindNonSourceTextRequest.scala
Outdated
Show resolved
Hide resolved
metals/src/main/scala/scala/meta/internal/metals/FindTextInFiles.scala
Outdated
Show resolved
Hide resolved
metals/src/main/scala/scala/meta/internal/metals/FindTextInFiles.scala
Outdated
Show resolved
Hide resolved
if (mask == null || content == null) { | ||
List.empty[Location] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably null checking might done earlier. Somewhere at the moment of extracting parameters
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably an annotation would work there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean javax.annotation.Nonnull
? I didn't see its usages in metals, will it work?
metals/src/main/scala/scala/meta/internal/metals/FindTextInFiles.scala
Outdated
Show resolved
Hide resolved
locations <- server.findTextInFiles(".conf", "jvm-shutdown-hooks") | ||
_ = assertEquals(locations, expectedLocations) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In metals tests, usually received and expected results are rendered into String representation that is more readable.
Could you do assert in a similar way to how it's done in WorkspaceSymbolLspSuite
?
metals/tests/unit/src/test/scala/tests/WorkspaceSymbolLspSuite.scala
Lines 160 to 198 in 55161a5
optionSourcePath = server | |
.toPath("scala/Option.scala") | |
.toRelative(workspace) | |
.toString | |
.replace("\\", "/") | |
_ = assertNoDiff( | |
optionReferences, | |
s"""|a/src/main/scala/a/A.scala:4:24: info: reference | |
| val x: Option[Int] = None | |
| ^^^^ | |
|$optionSourcePath:29:50: info: reference | |
| def apply[A](x: A): Option[A] = if (x == null) None else Some(x) | |
| ^^^^ | |
|$optionSourcePath:34:30: info: reference | |
| def empty[A] : Option[A] = None | |
| ^^^^ | |
|$optionSourcePath:230:18: info: reference | |
| if (isEmpty) None else Some(f(this.get)) | |
| ^^^^ | |
|$optionSourcePath:271:18: info: reference | |
| if (isEmpty) None else f(this.get) | |
| ^^^^ | |
|$optionSourcePath:274:18: info: reference | |
| if (isEmpty) None else ev(this.get) | |
| ^^^^ | |
|$optionSourcePath:289:43: info: reference | |
| if (isEmpty || p(this.get)) this else None | |
| ^^^^ | |
|$optionSourcePath:304:44: info: reference | |
| if (isEmpty || !p(this.get)) this else None | |
| ^^^^ | |
|$optionSourcePath:432:42: info: reference | |
| if (!isEmpty) pf.lift(this.get) else None | |
| ^^^^ | |
|$optionSourcePath:527:13: info: reference | |
|case object None extends Option[Nothing] { | |
| ^^^^ | |
|""".stripMargin | |
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not yet done, moved file so this change is not really outdated, will fix this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
I wonder though if it wouldn't be a better UX if we had just one QuickPick
displayed that would contain all text files (we could cache them), we can than easily filter those in the quick pick window by writing .conf
. That would mean we would need to search for a specific string later on, so maybe that wouldn't be better 🤔
} else { | ||
val allLocations: ArrayBuffer[Location] = new ArrayBuffer[Location]() | ||
|
||
buildTargets.allWorkspaceJars.foreach { classpathEntry => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could cache text file paths when indexing, which would make it all much faster and wouldn't take up much more memory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So essentially an index of jar path -> list of files inside a jar
? Would that not be huge (since there are lots of classfiles etc)?
package scala.meta.internal.metals | ||
|
||
case class FindNonSourceTextRequest( | ||
mask: java.lang.String, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need a mask really? Jars will contain .conf
files and .properties
most likely, so not much possible values there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there are more plain-text extensions that might be presented in jars and we will never cover them all.
Also the mask might be used not only for filtering by extension but also just by full name like: sample.json
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did it by advice from @dos65 and I guess he has a point - some people put xml/json/txt files in jars, so who knows what a user may need to search for
What if we unpack all the matched files to |
Hmm, I never thought of that, a good point! But here we need to estimate the performance impact, I guess? Copying (potentially) many files to dependencies directory (and no guarantees that we will find required content there) versus reading files in-place. I have nothing against both solutions, but I guess they are different in their nature of the interaction with the editor - copying is a side-effect which returns nothing and requires executing the editor command later and in-place search is a plain function which returns a list of locations so editor client can update some internal view representation.
Initially, I wanted to provide a 'peek' view, similar to the one used in 'find references', but VSCode API said no 😄 - it required peeking from some specific file location. TreeView looks okay I think - it gives both the file name and a preview of found entries. But I also don't mind coming to a consensus for the best solution. And other editors will be able to implement it the way they see fit for themselves anyway 😄 |
A task list for myself:
|
It's too late, but I realized that we need to check if vscode is able to search in virtual file systems. This feature is anyway requires support from the editor side so maybe we can do that using std vscode api. |
In my showcase implementation I used only standard vscode API for opening files and showing them, nothing fancy, so I guess it should work? |
By the way, here it is - very unpolished and not yet ready for PR to the main repo, so I made a PR to the main branch of my fork so changes can be easily seen: |
@Z1kkurat I'm about a different thing. There is |
@dos65 can you please elaborate? I'm not very familiar with vscode API, so I guess we will need to index such dependency files to present them as a filesystem tree, since my current implementation scans files on demand? |
I checked vscode api. There are plans to expose interfaces to make custom search providers but it still in proposed status I think the solution in this pr is quite good and cover this missing feature for some users who are switching from IDEA to Metals. @tgodzik @ckipp01 wdyt? Could we agree on introducing this new endpoint and push this feature further? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
metals/src/main/scala/scala/meta/internal/metals/FindTextInDependencyJarsRequest.scala
Outdated
Show resolved
Hide resolved
Can we confirm that the current extension will be usable with the new proposed text search API? It would be a shame if we have to change the JSON-RPC endpoints to make it work with the new text search API once it’s available. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the great work! I finally managed to dig into the PR and think about it. I left a bunch of comments, which might be a bit overwhelming, so if you need help let me know I would be happy to work on this a bit! We don't have to implement everything in this PR for sure,
@@ -1891,6 +1894,46 @@ class MetalsLanguageServer( | |||
.orNull | |||
}.asJava | |||
|
|||
@JsonRequest("metals/findTextInDependencyJars") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should document it similar to docs/integrations/tree-view-protocol.md
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on this. Before merging this in lets get a basic section on this in the new-editor.md
if possible. Not sure it needs to be a full page on its own since it's quite small, but having it documented somewhere that it exists, what it expects, and what it returns in important for other clients to know how to implement this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tgodzik do you mind if I do it as a separate PR? This one is quite old already and has more than 40 comments, so I would prefer to finally merge it when others approve.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, not an issue. Let me know if you don't manage to do it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry was mid review and didn't see this before. We can merge and do a follow-up if you'd prefer, but I do think it's pretty important to get this documented. If not others have to dig into the code to know how to implement it.
metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala
Outdated
Show resolved
Hide resolved
metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala
Outdated
Show resolved
Hide resolved
metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala
Outdated
Show resolved
Hide resolved
metals/src/main/scala/scala/meta/internal/metals/FindTextInDependencyJarsRequest.scala
Outdated
Show resolved
Hide resolved
metals/src/main/scala/scala/meta/internal/metals/FindTextInDependencyJars.scala
Outdated
Show resolved
Hide resolved
metals/src/main/scala/scala/meta/internal/metals/FindTextInDependencyJarsRequest.scala
Outdated
Show resolved
Hide resolved
Wow, that's a lot of feedback, thank you very much @tgodzik ! 😄 |
e9b29a2
to
6c4a7f8
Compare
@tgodzik I think that I addressed most of the comments. Please let me know if there's anything else on your mind regarding this PR or if you notice that I have missed something! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks amazing! With a properly specified API we can improve it later on when the text search API becomes available. Do you have a PR for VS Code ready?
We can merge this for sure, will just leave it for a bit for others to comment if they want.
@tgodzik Thank you!
Honestly, it is more of a PoC implementation. I made it to my fork and it has these peculiarities:
I've never encountered any vscode APIs previously, so I might've just misused them or haven't got the idea of what's going on properly. Here's the link if you are interested: Z1kkurat/metals-vscode#1. |
It should be ok to create a WiP PR for VS Code and iterate over that. Would you be interested in working on it? We can have a separate treeview I think, but maybe it would make sense to use the search icon? |
Yes, I definitely would be. PR is created: scalameta/metals-vscode#719 |
buildTargets.allWorkspaceJars.foreach { classpathEntry => | ||
try { | ||
val jarLocations: List[Location] = | ||
if (classpathEntry.isFile && classpathEntry.isJar) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tried this locally on metals repo with following parameters: *.java
+ startsWith
and got an empty response.
You need to include JdkSources
to all jars and allow .zip
files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! Turned out the same jar-related API worked on JDK zip just fine. Although I'm not sure about tests - I guess almost anything we can search for in the JDK sources would be repeated tens or hundreds of times there 😄 Besides, the test will depend on the specific JDK version since sources will change accordingly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think searching for a constructor of in a old public class should work stable and have only a single occurrence.
Maybe public String(StringBuffer buffer) {
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
Added a small fix to create PathMatcher for |
This is needed for the functionality added in scalameta/metals#3093.
This is needed for the functionality added in scalameta/metals#3093.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Z1kkurat Awesome work. Thx!
@ckipp01 you implemented that way too fast 😄 Amazing! And it works not too bad, considering that you were searching for a quite common word. Can you please try it with my latest commit with JDK sources scanning? I wonder if you would have to wait for a minute to get results from that... |
workspace | ||
.resolve(Directories.dependencies) | ||
.resolve("src.zip") | ||
.resolve("java.base") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooh, I think this won't work on JDK8... @dos65 WDYT about that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Damn!
For jdk8 you need skip resolve("java.base")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done!
Honestly the time difference wasn't that noticeable. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Great job on the @Z1kkurat!
I'll go ahead and merge seeing that we have 3 ✅ , although please do follow up and add some docs @Z1kkurat. |
This is an attempt to implement this functionality: scalameta/metals-feature-requests#196
The implementation is very naive and just scans through all workspace jars, reading files by mask and searching for a full content match. As was discussed on Discord with @olafurpg, this can be tolerated as the initial implementation.
To test this I implemented client-side functionality in
metals-vscode
using a separate custom tree view just to showcase that the functionality itself works. I think I will attempt to make a PR with it once I clean it up (the code is very bad and dirty right now).