Skip to content

Commit

Permalink
feat(search): add ripgrep search command
Browse files Browse the repository at this point in the history
- Introduce RipgrepSearcher for file search functionality.
- Add RipgrepOutputProcessor to handle ripgrep JSON output.
- Implement RipgrepSearchShireCommand for executing ripgrep searches.
- Extend BuiltinCommand to include RIPGREP_SEARCH.
  • Loading branch information
phodal committed Feb 2, 2025
1 parent 7b84715 commit bafff1b
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.phodal.shirelang.compiler.execute.command

import com.intellij.openapi.project.Project
import com.phodal.shirelang.compiler.execute.command.search.RipgrepSearcher

class RipgrepSearchShireCommand(
val myProject: Project, private val scope: String, val text: String?,
) : ShireCommand {
override suspend fun doExecute(): String? {
val searchDirectory = myProject.baseDir!!.path
return RipgrepSearcher.searchFiles(myProject, searchDirectory, text ?: scope, null).get()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.phodal.shirelang.compiler.execute.command.search

import com.google.gson.JsonParser
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.openapi.util.Key
import kotlinx.serialization.Serializable

@Serializable
public data class RipgrepSearchResult(
var filePath: String? = null,
var line: Int = 0,
var column: Int = 0,
var match: String? = null,
var beforeContext: MutableList<String?> = ArrayList<String?>(),
var afterContext: MutableList<String?> = ArrayList<String?>()
)

class RipgrepOutputProcessor : ProcessAdapter() {
private val results: MutableList<RipgrepSearchResult> = ArrayList<RipgrepSearchResult>()
private var currentResult: RipgrepSearchResult? = null

override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
if (outputType === ProcessOutputTypes.STDOUT) {
parseJsonLine(event.text)
}
}

private val jsonBuffer = StringBuilder()

fun parseJsonLine(line: String) {
if (line.isBlank()) {
return
}

jsonBuffer.append(line)

// Try to parse the buffer as JSON
val json = try {
JsonParser.parseString(jsonBuffer.toString())
} catch (e: Exception) {
// If parsing fails, it might be because the JSON is incomplete
// So we just return and wait for more lines
return
}

// If parsing succeeds, clear the buffer and process the JSON
jsonBuffer.clear()

if (json.isJsonObject) {
val jsonObject = json.asJsonObject
val type = jsonObject.get("type").asString

when (type) {
"match" -> {
val data = jsonObject.getAsJsonObject("data")
val path = data.getAsJsonObject("path").get("text").asString
val lines = data.getAsJsonObject("lines").get("text").asString
val lineNumber = data.get("line_number").asInt
val absoluteOffset = data.get("absolute_offset").asInt
val submatches = data.getAsJsonArray("submatches")

currentResult = RipgrepSearchResult(
filePath = path,
line = lineNumber,
column = absoluteOffset,
match = lines.trim()
)

submatches.forEach { submatch ->
val submatchObj = submatch.asJsonObject
val matchText = submatchObj.get("match").asJsonObject.get("text").asString
currentResult?.match = matchText
}

results.add(currentResult!!)
}

"context" -> {
val data = jsonObject.getAsJsonObject("data")
val lines = data.getAsJsonObject("lines").get("text").asString
val lineNumber = data.get("line_number").asInt

if (currentResult != null) {
if (lineNumber < currentResult!!.line) {
currentResult!!.beforeContext.add(lines.trim())
} else {
currentResult!!.afterContext.add(lines.trim())
}
}
}
}
}
}

fun getResults(): MutableList<RipgrepSearchResult> {
if (currentResult != null) {
results.add(currentResult!!)
}

return results
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright 2024 Cline Bot Inc. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.phodal.shirelang.compiler.execute.command.search

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.*
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.SystemIndependent
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit

/**
* 使用Ripgrep进行文件搜索
* Inspired by: https://github.com/cline/cline/blob/main/src/services/ripgrep/index.ts Apache-2.0
*/
object RipgrepSearcher {
private val LOG = Logger.getInstance(RipgrepSearcher::class.java)

fun searchFiles(
project: Project,
searchDirectory: String,
regexPattern: String,
filePattern: String?
): CompletableFuture<String?> {
return CompletableFuture.supplyAsync<String> {
try {
val rgPath = findRipgrepBinary() ?: throw IOException("Ripgrep binary not found")
val results = executeRipgrep(
project,
rgPath,
searchDirectory,
regexPattern,
filePattern
)
return@supplyAsync formatResults(results, project.basePath!!)
} catch (e: Exception) {
LOG.error("Search failed", e)
return@supplyAsync "Search error: " + e.message
}
}
}

@Throws(IOException::class)
fun findRipgrepBinary(): Path? {
val osName = System.getProperty("os.name").lowercase(Locale.getDefault())
val binName = if (osName.contains("win")) "rg.exe" else "rg"

val pb = ProcessBuilder("which", binName)
val process = pb.start()
try {
if (process.waitFor(1, TimeUnit.SECONDS) && process.exitValue() == 0) {
val path = String(process.inputStream.readAllBytes(), StandardCharsets.UTF_8).trim { it <= ' ' }
return Paths.get(path)
}
} catch (_: InterruptedException) {
return null
}

return null
}

@Throws(IOException::class)
private fun executeRipgrep(project: Project, rgPath: Path, directory: String, regex: String, filePattern: String?):
MutableList<RipgrepSearchResult> {
val cmd = getCommandLine(rgPath, regex, filePattern, directory, project.basePath)

val handler: OSProcessHandler = ColoredProcessHandler(cmd)
val processor = RipgrepOutputProcessor()
handler.addProcessListener(processor)

handler.startNotify()
handler.waitFor()

return processor.getResults()
}

fun getCommandLine(
rgPath: Path,
regex: String? = null,
filePattern: String? = null,
directory: String? = null,
basePath: @SystemIndependent @NonNls String? = null,
): GeneralCommandLine {
val cmd = GeneralCommandLine(rgPath.toString())
cmd.withWorkDirectory(basePath)

cmd.addParameters("--json")

if (regex != null) {
cmd.addParameters("-e", regex)
}

if (filePattern != null) {
cmd.addParameters("--glob", filePattern)
}

cmd.addParameters("--context", "1")

if (directory != null) {
cmd.addParameters(directory)
}

cmd.charset = StandardCharsets.UTF_8
return cmd
}

private fun formatResults(results: MutableList<RipgrepSearchResult>, basePath: String): String {
val output = StringBuilder()
val grouped: MutableMap<String?, MutableList<RipgrepSearchResult?>?> =
LinkedHashMap<String?, MutableList<RipgrepSearchResult?>?>()

for (result in results) {
val relPath = getRelativePath(basePath, result.filePath!!)
grouped.computeIfAbsent(relPath) { k: String? -> ArrayList<RipgrepSearchResult?>() }!!.add(result)
}

for (entry in grouped.entries) {
output.append("### filepath: ").append(entry.key).append("\n")
val filePath = Paths.get(basePath, entry.key)
val content = filePath.toFile().readLines()

val lineNumbers = entry.value!!.map { it!!.line }

val displayLines = mutableSetOf<Int>()
for (lineNumber in lineNumbers) {
val start = 1.coerceAtLeast(lineNumber - 4)
val end = content.size.coerceAtMost(lineNumber + 4)
for (i in start..end) {
displayLines.add(i)
}
}

val sortedDisplayLines = displayLines.sorted()
for (lineNumber in sortedDisplayLines) {
val line = content.getOrNull(lineNumber - 1)
if (line != null) {
output.append(lineNumber).append(" ").append(line).append("\n")
}
}

output.append("\n")
}

return output.toString()
}

private fun getRelativePath(basePath: String, absolutePath: String): String {
val base = Paths.get(basePath)
val target = Paths.get(absolutePath)
return base.relativize(target).toString().replace('\\', '/')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,13 @@ class ShireSyntaxAnalyzer(
result.isLocalCommand = true
OpenShireCommand(myProject, prop)
}

BuiltinCommand.RIPGREP_SEARCH -> {
result.isLocalCommand = true
val shireCode: String? = lookupNextCode(used)?.text
RipgrepSearchShireCommand(myProject, prop, shireCode)
}

}

val execResult = runBlocking {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ enum class BuiltinCommand(
true
),
OPEN("open", "Open a file in the editor", AllIcons.Actions.MenuOpen, false),
RIPGREP_SEARCH("ripgrepSearch", "Search text in the project with ripgrep", AllIcons.Actions.Regex, false, true),
;

companion object {
Expand All @@ -103,5 +104,17 @@ enum class BuiltinCommand(
}

fun fromString(agentName: String): BuiltinCommand? = values().find { it.commandName == agentName }

val READ_COMMANDS = setOf(
DIR,
LOCAL_SEARCH,
FILE,
REV,
STRUCTURE,
SYMBOL,
DATABASE,
RELATED,
RIPGREP_SEARCH
)
}
}

0 comments on commit bafff1b

Please sign in to comment.