-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
56bf358
Implement find in files LSP extension
Z1kkurat 103add7
Fix formatting
Z1kkurat a021707
Organize imports
Z1kkurat 24d87c6
Addressed some of comments
Z1kkurat 6c4a7f8
Fix fmt
Z1kkurat 849d8f2
Address review comments
Z1kkurat 74af023
Use glob syntax
Z1kkurat 43a44fb
Don't create unnecessary PathMatcher
Z1kkurat 427937f
Include JDK zip sources
Z1kkurat 1c3187d
Add searching in JDK lib
Z1kkurat 3239879
Fix JDK check in test
Z1kkurat fe9bb0f
Add test case for JDK 17
Z1kkurat File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
208 changes: 208 additions & 0 deletions
208
metals/src/main/scala/scala/meta/internal/metals/findfiles/FindTextInDependencyJars.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
package scala.meta.internal.metals.findfiles | ||
|
||
import java.io.BufferedReader | ||
import java.io.InputStreamReader | ||
import java.nio.file.Files | ||
|
||
import scala.collection.mutable.ArrayBuffer | ||
import scala.concurrent.ExecutionContext | ||
import scala.concurrent.Future | ||
import scala.util.control.NonFatal | ||
|
||
import scala.meta.internal.io.FileIO | ||
import scala.meta.internal.metals.MetalsEnrichments._ | ||
import scala.meta.internal.metals.ScalafmtConfig.PathMatcher.Nio | ||
import scala.meta.internal.metals._ | ||
import scala.meta.io.AbsolutePath | ||
|
||
import org.eclipse.lsp4j.Location | ||
import org.eclipse.lsp4j.Position | ||
import org.eclipse.lsp4j.Range | ||
|
||
class FindTextInDependencyJars( | ||
buildTargets: BuildTargets, | ||
workspace: () => AbsolutePath, | ||
languageClient: MetalsLanguageClient | ||
)(implicit ec: ExecutionContext) { | ||
import FindTextInDependencyJars._ | ||
|
||
def find(request: FindTextInDependencyJarsRequest): Future[List[Location]] = { | ||
val req = Request.fromRequest(request) | ||
|
||
def readInclude: Future[Option[String]] = | ||
paramOrInput(req.options.flatMap(_.include))( | ||
MetalsInputBoxParams(value = ".conf", prompt = "Enter file mask") | ||
) | ||
|
||
def readPattern: Future[Option[String]] = | ||
paramOrInput(Option(req.query.pattern))( | ||
MetalsInputBoxParams(prompt = "Enter content to search for") | ||
) | ||
|
||
readInclude.zipWith(readPattern) { (maybeInclude, maybePattern) => | ||
maybeInclude | ||
.zip(maybePattern) | ||
.map { case (include, pattern) => | ||
val allLocations: ArrayBuffer[Location] = new ArrayBuffer[Location]() | ||
val includeMatcher = Nio(s"glob:**$include") | ||
val excludeMatcher = | ||
req.options.flatMap(_.exclude).map(e => Nio(s"glob:**$e")) | ||
|
||
(buildTargets.allWorkspaceJars ++ JdkSources()).foreach { | ||
classpathEntry => | ||
try { | ||
val locations: List[Location] = | ||
if ( | ||
classpathEntry.isFile && (classpathEntry.isJar || classpathEntry.isZip) | ||
) { | ||
visitJar( | ||
path = classpathEntry, | ||
include = includeMatcher, | ||
exclude = excludeMatcher, | ||
pattern = pattern | ||
) | ||
} else Nil | ||
|
||
allLocations ++= locations | ||
} catch { | ||
case NonFatal(e) => | ||
scribe.error( | ||
s"Failed to find text in dependency files for $classpathEntry", | ||
e | ||
) | ||
} | ||
} | ||
|
||
allLocations.toList | ||
} | ||
.flatten | ||
.toList | ||
} | ||
} | ||
|
||
private def isSuitableFile( | ||
path: AbsolutePath, | ||
include: Nio, | ||
exclude: Option[Nio] | ||
): Boolean = { | ||
path.isFile && | ||
include.matches(path) && | ||
exclude.forall(matcher => !matcher.matches(path)) | ||
} | ||
|
||
private def visitJar( | ||
path: AbsolutePath, | ||
include: Nio, | ||
exclude: Option[Nio], | ||
pattern: String | ||
): List[Location] = { | ||
FileIO | ||
.withJarFileSystem(path, create = false, close = true) { root => | ||
FileIO | ||
.listAllFilesRecursively(root) | ||
.filter(isSuitableFile(_, include, exclude)) | ||
.flatMap { absPath => | ||
val fileRanges: List[Range] = visitFileInsideJar(absPath, pattern) | ||
if (fileRanges.nonEmpty) { | ||
val result = absPath.toFileOnDisk(workspace()) | ||
fileRanges | ||
.map(range => new Location(result.toURI.toString, range)) | ||
} else Nil | ||
} | ||
} | ||
.toList | ||
} | ||
|
||
private def visitFileInsideJar( | ||
path: AbsolutePath, | ||
pattern: String | ||
): List[Range] = { | ||
var reader: BufferedReader = null | ||
val positions: ArrayBuffer[Int] = new ArrayBuffer[Int]() | ||
val results: ArrayBuffer[Range] = new ArrayBuffer[Range]() | ||
val contentLength: Int = pattern.length() | ||
|
||
try { | ||
reader = new BufferedReader( | ||
new InputStreamReader(Files.newInputStream(path.toNIO)) | ||
) | ||
var lineNumber: Int = 0 | ||
var line: String = reader.readLine() | ||
while (line != null) { | ||
var occurence = line.indexOf(pattern) | ||
while (occurence != -1) { | ||
positions += occurence | ||
occurence = line.indexOf(pattern, occurence + 1) | ||
} | ||
|
||
positions.foreach { position => | ||
results += new Range( | ||
new Position(lineNumber, position), | ||
new Position(lineNumber, position + contentLength) | ||
) | ||
} | ||
|
||
positions.clear() | ||
lineNumber = lineNumber + 1 | ||
line = reader.readLine() | ||
} | ||
} finally { | ||
if (reader != null) reader.close() | ||
} | ||
|
||
results.toList | ||
} | ||
|
||
private def paramOrInput( | ||
param: Option[String] | ||
)(input: => MetalsInputBoxParams): Future[Option[String]] = { | ||
param match { | ||
case Some(value) => | ||
Future.successful(Some(value)) | ||
case None => | ||
languageClient | ||
.metalsInputBox(input) | ||
.asScala | ||
.map(checkResult) | ||
} | ||
} | ||
|
||
private def checkResult(result: MetalsInputBoxResult) = result match { | ||
case name if !name.cancelled && name.value.nonEmpty => | ||
Some(name.value) | ||
case _ => | ||
None | ||
} | ||
} | ||
|
||
object FindTextInDependencyJars { | ||
// These are just more typesafe wrappers, duplicating the structure of original model | ||
private case class Request(options: Option[Options], query: TextSearchQuery) | ||
private object Request { | ||
def fromRequest(request: FindTextInDependencyJarsRequest): Request = { | ||
val options = Option(request.options).map { options => | ||
Options( | ||
include = Option(options.include), | ||
exclude = Option(options.exclude) | ||
) | ||
} | ||
|
||
val query = TextSearchQuery( | ||
pattern = request.query.pattern, | ||
isRegExp = Option(request.query.isRegExp), | ||
isCaseSensitive = Option(request.query.isCaseSensitive), | ||
isWordMatch = Option(request.query.isWordMatch) | ||
) | ||
|
||
Request(options = options, query = query) | ||
} | ||
} | ||
|
||
private case class Options(include: Option[String], exclude: Option[String]) | ||
private case class TextSearchQuery( | ||
pattern: String, | ||
isRegExp: Option[Boolean], | ||
isCaseSensitive: Option[Boolean], | ||
isWordMatch: Option[Boolean] | ||
) | ||
} |
26 changes: 26 additions & 0 deletions
26
...src/main/scala/scala/meta/internal/metals/findfiles/FindTextInDependencyJarsRequest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package scala.meta.internal.metals.findfiles | ||
|
||
import javax.annotation.Nullable | ||
|
||
case class FindTextInDependencyJarsRequest( | ||
@Nullable | ||
options: FindTextInFilesOptions, | ||
query: TextSearchQuery | ||
) | ||
|
||
case class TextSearchQuery( | ||
pattern: String, | ||
@Nullable | ||
isRegExp: java.lang.Boolean, | ||
@Nullable | ||
isCaseSensitive: java.lang.Boolean, | ||
@Nullable | ||
isWordMatch: java.lang.Boolean | ||
) | ||
|
||
case class FindTextInFilesOptions( | ||
@Nullable | ||
include: String, | ||
@Nullable | ||
exclude: String | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.