mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[terminal] add generic file path filter
GitOrigin-RevId: 02763625ba6e6f658847a65d777835cbb9e0f4b8
This commit is contained in:
committed by
intellij-monorepo-bot
parent
20e36a1bc2
commit
92e121eb92
@@ -72,6 +72,9 @@
|
||||
description="Disabled completion popUp for reworked terminal for development time"/>
|
||||
<registryKey key="terminal.type.ahead" defaultValue="false"
|
||||
description="Early prototype of type-ahead for the reworked terminal"/>
|
||||
<registryKey key="terminal.generic.hyperlinks" defaultValue="false"
|
||||
description="Show generic file hyperlinks in the reworked terminal. Such hyperlinks are highlighted on mouse hover.
|
||||
Open a new terminal after enabling this option to take effect."/>
|
||||
|
||||
<experimentalFeature id="terminal.shell.command.handling" percentOfUsers="100"/>
|
||||
<terminal.shellCommandHandler implementation="org.jetbrains.plugins.terminal.RunAnythingTerminalBridge"/>
|
||||
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
package org.jetbrains.plugins.terminal.block.reworked.hyperlinks
|
||||
|
||||
import com.intellij.execution.filters.Filter
|
||||
import com.intellij.execution.filters.OpenFileHyperlinkInfo
|
||||
import com.intellij.openapi.editor.markup.EffectType
|
||||
import com.intellij.openapi.editor.markup.TextAttributes
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.ui.JBColor
|
||||
import java.awt.Font
|
||||
|
||||
internal enum class ParsingState {
|
||||
NORMAL, PATH, CANCELED_PATH
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy-pasted from `com.android.tools.idea.gradle.project.build.output.GenericFileFilter`.
|
||||
*
|
||||
* Filter that highlights absolute path as hyperlinks in console output. This filter is effective in all console output, including build
|
||||
* output, sync output, run test output, built-in terminal emulator, etc. Therefore, we manually parse the string instead of using regex
|
||||
* for maximum performance.
|
||||
*/
|
||||
internal class GenericFileFilter(private val project: Project, private val localFileSystem: LocalFileSystem) : Filter {
|
||||
companion object {
|
||||
/**
|
||||
* Max filename considered during parsing. Do not confuse with file path, which may contain several file names separated by '/' or '\'.
|
||||
*
|
||||
* Modern popular FS do not allow file names longer than 255.*/
|
||||
const val FILENAME_MAX = 255
|
||||
|
||||
/**
|
||||
* Min path length to be considered.
|
||||
*
|
||||
* E.g. lonely slashes should be ignored.
|
||||
*/
|
||||
const val PATH_MIN = 2
|
||||
}
|
||||
|
||||
override fun applyFilter(line: String, entireLength: Int): Filter.Result? {
|
||||
val indexOffset = entireLength - line.length
|
||||
val items = mutableListOf<Filter.ResultItem>()
|
||||
var state = ParsingState.NORMAL
|
||||
var pathStartIndex = -1
|
||||
var lastPathSegmentStart = -1
|
||||
var candidateItem: Filter.ResultItem? = null
|
||||
var i = 0
|
||||
|
||||
fun addPreviousCandidate() {
|
||||
if (candidateItem != null) {
|
||||
items += candidateItem!!
|
||||
}
|
||||
candidateItem = null
|
||||
}
|
||||
|
||||
fun startPathMode(){
|
||||
state = ParsingState.PATH
|
||||
pathStartIndex = i
|
||||
lastPathSegmentStart = i
|
||||
addPreviousCandidate()
|
||||
}
|
||||
|
||||
fun startNormalMode() {
|
||||
state = ParsingState.NORMAL
|
||||
addPreviousCandidate()
|
||||
}
|
||||
|
||||
fun startCanceledMode() {
|
||||
state = ParsingState.CANCELED_PATH
|
||||
addPreviousCandidate()
|
||||
}
|
||||
|
||||
fun findValidResult(pathEndIndex: Int, lineNumber: Int, columnNumber: Int): Filter.ResultItem? {
|
||||
if (pathEndIndex - lastPathSegmentStart > FILENAME_MAX) return null
|
||||
if (pathEndIndex - pathStartIndex < PATH_MIN) return null
|
||||
|
||||
val path = line.substring(pathStartIndex, pathEndIndex)
|
||||
if (path == "/") {
|
||||
// Ignore single slashes, as these are probably referring to something
|
||||
// other than the file system root (e.g. progress indicators like "[10 / 1,000]").
|
||||
return null
|
||||
}
|
||||
val file = try {
|
||||
localFileSystem.findFileByPathIfCached(path)
|
||||
} catch (_: Throwable) {
|
||||
// We interpret any exception to mean the file is not found.
|
||||
null
|
||||
}
|
||||
return if (file != null) {
|
||||
Filter.ResultItem(
|
||||
indexOffset + pathStartIndex,
|
||||
indexOffset + i,
|
||||
OpenFileHyperlinkInfo(project, file, lineNumber, columnNumber),
|
||||
EMPTY_ATTRS,
|
||||
null,
|
||||
HOVERED_ATTRS,
|
||||
)
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun findValidResultWithNumbers(pathEndIndex: Int): Filter.ResultItem? {
|
||||
val lineNumber: Int
|
||||
var columnNumber = 1
|
||||
// Try to parse as "path: (line, column):"
|
||||
if (line.startsWith(" (", i + 1)) {
|
||||
i += 3
|
||||
lineNumber = line.takeWhileFromIndex(i) { it.isDigit() }?.also { i += it.length }.safeToIntOrDefault(1)
|
||||
if (line.startsWith(", ", i)) {
|
||||
i += 2
|
||||
columnNumber = line.takeWhileFromIndex(i) { it.isDigit() }?.also { i += it.length }.safeToIntOrDefault(1)
|
||||
if (line.startsWith("):", i)) {
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Try to parse as path:line:column:
|
||||
lineNumber = line.takeWhileFromIndex(i + 1) { it.isDigit() }?.also { i += it.length }.safeToIntOrDefault(1)
|
||||
columnNumber =
|
||||
if (line.getOrNull(++i) == ':')
|
||||
line.takeWhileFromIndex(++i) { it.isDigit() }?.also { i += it.length }.safeToIntOrDefault(1)
|
||||
else
|
||||
1
|
||||
}
|
||||
return findValidResult(pathEndIndex, lineNumber - 1, columnNumber - 1)
|
||||
}
|
||||
|
||||
while (i < line.length) {
|
||||
when (state) {
|
||||
ParsingState.NORMAL -> {
|
||||
when {
|
||||
line[i] == '/' -> {
|
||||
// Start parsing a Linux path
|
||||
startPathMode()
|
||||
}
|
||||
line[i] in 'A'..'Z' && (line.startsWith(":\\", startIndex = i + 1) || line.startsWith(":/", startIndex = i + 1) ) -> {
|
||||
// Start parsing a Windows path
|
||||
startPathMode()
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
}
|
||||
ParsingState.PATH -> {
|
||||
if ((i - lastPathSegmentStart) > FILENAME_MAX) {
|
||||
startCanceledMode()
|
||||
}
|
||||
else when {
|
||||
line[i] == '\\' || line[i] == '/' -> {
|
||||
lastPathSegmentStart = i + 1
|
||||
if (i - pathStartIndex > 1){
|
||||
val currentCandidate = findValidResult(i, 0, 0)
|
||||
if (currentCandidate == null) {
|
||||
/* This is not a valid path it means that continuing as a path no longer will result in a valid file, but this could be
|
||||
the the start of a new path. Need to move back up to 4 characters since the path could include a drive letter.
|
||||
*/
|
||||
if ((i - 3) > pathStartIndex && line[i - 1] == ':' && line[i - 2] in 'A'..'Z' && line[i - 3].isWhitespace()) {
|
||||
i -= 5
|
||||
startNormalMode()
|
||||
}
|
||||
else if ((i - 1) > pathStartIndex && line[i -1].isWhitespace()) {
|
||||
i -= 2
|
||||
startNormalMode()
|
||||
}
|
||||
else {
|
||||
startCanceledMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
line[i] == ':' -> {
|
||||
val previousI = i
|
||||
val longestCandidate = findValidResultWithNumbers(i)
|
||||
if (longestCandidate != null) {
|
||||
candidateItem = longestCandidate
|
||||
}
|
||||
else {
|
||||
// Could not parse numbers correctly (or is not a valid path, restore i)
|
||||
i = previousI
|
||||
// Could be the start of a windows path, move back in that case
|
||||
if (((i - 1) > pathStartIndex)
|
||||
&& (line[i - 1] in 'A'..'Z')
|
||||
&& ((i + 1) < line.length)
|
||||
&& (line[i + 1] == '/' || line[i + 1] == '\\')) {
|
||||
i -= 2
|
||||
}
|
||||
}
|
||||
startNormalMode()
|
||||
}
|
||||
// Paths can have white spaces and links get cut early (https://issuetracker.google.com/issues/136242040)
|
||||
// Or can be a valid path but be the prefix of a longer path (for example "/work projects/" and "/work projects 2" exist,
|
||||
// https://issuetracker.google.com/issues/167701951)
|
||||
line[i].isWhitespace() -> {
|
||||
val possibleCandidate = findValidResult(i, 0, 0)
|
||||
if (possibleCandidate != null) {
|
||||
candidateItem = possibleCandidate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ParsingState.CANCELED_PATH -> if (line[i].isWhitespace()) startNormalMode()
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (candidateItem != null) {
|
||||
items += candidateItem!!
|
||||
}
|
||||
if (items.isEmpty()) return null
|
||||
return Filter.Result(items)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.safeToIntOrDefault(default: Int): Int = StringUtil.parseInt(this, default)
|
||||
|
||||
private fun String.takeWhileFromIndex(index: Int, predicate: (Char) -> Boolean): String? {
|
||||
for (i in index until length) {
|
||||
if (!predicate(get(i))) {
|
||||
return if (i == index) null else substring(index, i)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private val EMPTY_ATTRS: TextAttributes = TextAttributes(null, null, null, null, Font.PLAIN)
|
||||
|
||||
private val HOVERED_ATTRS: TextAttributes = TextAttributes(null, null, JBColor.BLACK, EffectType.LINE_UNDERSCORE, Font.PLAIN)
|
||||
+5
@@ -13,6 +13,8 @@ import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.event.DocumentEvent
|
||||
import com.intellij.openapi.editor.event.DocumentListener
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.util.SystemProperties
|
||||
import com.intellij.util.asDisposable
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
@@ -145,6 +147,9 @@ class TerminalHyperlinkHighlighter private constructor(
|
||||
hyperlinkHighlighter.highlightHyperlinks(startOffset)
|
||||
}
|
||||
})
|
||||
if (Registry.`is`("terminal.generic.hyperlinks", false)) {
|
||||
hyperlinkHighlighter.addFilter(GenericFileFilter(project, LocalFileSystem.getInstance()))
|
||||
}
|
||||
return hyperlinkHighlighter
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user