terminal: improve hyperlink highlighting (clear previous hyperlinks before applying new ones; calculate filters in NBRA)

GitOrigin-RevId: d82c72d9cb09469d360529a1c862755d13a0e0cb
This commit is contained in:
Sergey Simonchik
2024-01-30 00:31:40 +01:00
committed by intellij-monorepo-bot
parent 44d535e971
commit c5436c5aa5
4 changed files with 152 additions and 66 deletions

View File

@@ -1,45 +0,0 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.terminal.exp
import com.intellij.execution.filters.CompositeFilter
import com.intellij.execution.filters.ConsoleFilterProvider
import com.intellij.execution.filters.Filter
import com.intellij.execution.impl.ConsoleViewUtil
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.project.Project
import com.intellij.psi.search.GlobalSearchScope
internal class CompositeFilterWrapper(private val project: Project, disposable: Disposable) {
@Volatile
private var cachedCompositeFilter: CompositeFilter? = null
init {
ConsoleFilterProvider.FILTER_PROVIDERS.addChangeListener({ cachedCompositeFilter = null }, disposable)
}
private fun createCompositeFilters(): List<Filter> {
if (project.isDefault) {
return emptyList()
}
return runReadAction {
if (project.isDisposed) {
return@runReadAction emptyList<Filter>()
}
ConsoleViewUtil.computeConsoleFilters(project, null, GlobalSearchScope.allScope(project))
}
}
val compositeFilter: CompositeFilter
get() {
cachedCompositeFilter?.let {
return it
}
val resultCompositeFilter = CompositeFilter(project, createCompositeFilters()).also {
it.setForceUseAllFilters(true)
}
cachedCompositeFilter = resultCompositeFilter
return resultCompositeFilter
}
}

View File

@@ -1,8 +1,6 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.terminal.exp
import com.intellij.execution.impl.EditorHyperlinkSupport
import com.intellij.execution.impl.ExpirableTokenProvider
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.DataKey
import com.intellij.openapi.application.ModalityState
@@ -19,6 +17,7 @@ import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jediterm.terminal.TextStyle
import org.jetbrains.plugins.terminal.exp.TerminalDataContextUtils.IS_OUTPUT_EDITOR_KEY
import org.jetbrains.plugins.terminal.exp.TerminalUiUtils.toTextAttributes
import org.jetbrains.plugins.terminal.exp.hyperlinks.TerminalHyperlinkHighlighter
import java.awt.Font
class TerminalOutputController(
@@ -40,9 +39,7 @@ class TerminalOutputController(
@Volatile
private var mouseAndContentListenersDisposable: Disposable? = null
private val hyperlinkFilterWrapper: CompositeFilterWrapper = CompositeFilterWrapper(project, session)
private var lastBlockWithHyperlinks: Pair<CommandBlock, ExpirableTokenProvider>? = null
private val hyperlinkHighlighter: TerminalHyperlinkHighlighter = TerminalHyperlinkHighlighter(project, outputModel, session)
init {
editor.putUserData(IS_OUTPUT_EDITOR_KEY, true)
@@ -208,7 +205,7 @@ class TerminalOutputController(
outputModel.putHighlightings(block, highlightings)
editor.document.replaceString(block.outputStartOffset, block.endOffset, output.text)
highlightHyperlinks(block)
hyperlinkHighlighter.highlightHyperlinks(block)
// Install decorations lazily, only if there is some text.
// ZSH prints '%' character on startup and then removing it immediately, so ignore this character to avoid blinking.
@@ -229,21 +226,6 @@ class TerminalOutputController(
caretPainter?.repaint()
}
private fun highlightHyperlinks(block: CommandBlock) {
val document = editor.document
val startLine = document.getLineNumber(block.outputStartOffset)
val endLine = document.getLineNumber(block.endOffset)
lastBlockWithHyperlinks?.let {
if (it.first == block) {
it.second.invalidateAll() // stop the previous highlighting of the same block
}
}
val expirableTokenProvider = ExpirableTokenProvider()
lastBlockWithHyperlinks = block to expirableTokenProvider
EditorHyperlinkSupport.get(editor).highlightHyperlinksLater(hyperlinkFilterWrapper.compositeFilter, startLine, endLine,
expirableTokenProvider.createExpirable())
}
private fun TextStyle.toTextAttributes(): TextAttributes = this.toTextAttributes(session.colorPalette)
private fun appendLineToBlock(block: CommandBlock, text: String, highlighting: HighlightingInfo) {

View File

@@ -0,0 +1,69 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.terminal.exp.hyperlinks
import com.intellij.execution.filters.CompositeFilter
import com.intellij.execution.filters.ConsoleFilterProvider
import com.intellij.execution.filters.Filter
import com.intellij.execution.impl.ConsoleViewUtil
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.project.Project
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.concurrency.AppExecutorUtil
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean
internal class CompositeFilterWrapper(private val project: Project, private val disposable: Disposable) {
private val filtersUpdatedListeners: MutableList<() -> Unit> = CopyOnWriteArrayList()
private val filtersComputationInProgress: AtomicBoolean = AtomicBoolean(false)
@Volatile
private var cachedFilter: CompositeFilter? = null
init {
ConsoleFilterProvider.FILTER_PROVIDERS.addChangeListener({
cachedFilter = null
scheduleFiltersComputation()
}, disposable)
scheduleFiltersComputation()
}
private fun scheduleFiltersComputation() {
if (filtersComputationInProgress.compareAndSet(false, true)) {
ReadAction
.nonBlocking<List<Filter>> { ConsoleViewUtil.computeConsoleFilters(project, null, GlobalSearchScope.allScope(project)) }
.expireWith(disposable)
.finishOnUiThread(ModalityState.defaultModalityState()) { filters: List<Filter> ->
filtersComputationInProgress.set(false)
cachedFilter = CompositeFilter(project, filters).also {
it.setForceUseAllFilters(true)
}
fireFiltersUpdated()
}.submit(AppExecutorUtil.getAppExecutorService())
}
}
fun addFiltersUpdatedListener(listener: () -> Unit) {
filtersUpdatedListeners.add(listener)
}
private fun fireFiltersUpdated() {
for (listener in filtersUpdatedListeners) {
listener()
}
}
/**
* @return [Filter] instance if cached. Otherwise, returns `null` and starts computing filters in background;
* when filters are ready, `filtersUpdated` event will be fired.
*
*/
fun getFilter(): CompositeFilter? {
cachedFilter?.let {
return it
}
scheduleFiltersComputation()
return null
}
}

View File

@@ -0,0 +1,80 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.terminal.exp.hyperlinks
import com.intellij.execution.impl.EditorHyperlinkSupport
import com.intellij.execution.impl.ExpirableTokenProvider
import com.intellij.openapi.Disposable
import com.intellij.openapi.editor.ex.DocumentEx
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.project.Project
import com.intellij.util.CommonProcessors.CollectProcessor
import com.intellij.util.FilteringProcessor
import com.intellij.util.Processor
import com.intellij.util.concurrency.annotations.RequiresEdt
import org.jetbrains.plugins.terminal.exp.CommandBlock
import org.jetbrains.plugins.terminal.exp.TerminalOutputModel
internal class TerminalHyperlinkHighlighter(project: Project,
private val outputModel: TerminalOutputModel,
parentDisposable: Disposable) {
private val filterWrapper: CompositeFilterWrapper = CompositeFilterWrapper(project, parentDisposable)
private var lastUpdatedBlockInfo: Pair<CommandBlock, ExpirableTokenProvider>? = null
private val editor: EditorEx
get() = outputModel.editor
private val document: DocumentEx
get() = outputModel.editor.document
private val hyperlinkSupport: EditorHyperlinkSupport
get() = EditorHyperlinkSupport.get(editor)
init {
filterWrapper.addFiltersUpdatedListener { rehighlightAll() }
}
private fun rehighlightAll() {
for (i in 0 until outputModel.getBlocksSize()) {
highlightHyperlinks(outputModel.getByIndex(i))
}
}
@RequiresEdt
fun highlightHyperlinks(block: CommandBlock) {
val filter = filterWrapper.getFilter() ?: return // if null, `rehighlightAll` will follow
lastUpdatedBlockInfo?.let {
if (it.first == block) {
it.second.invalidateAll() // stop the previous highlighting of the same block
}
}
val expirableTokenProvider = ExpirableTokenProvider()
lastUpdatedBlockInfo = block to expirableTokenProvider
clearHyperlinks(block.outputStartOffset, block.endOffset)
val startLine = document.getLineNumber(block.outputStartOffset)
val endLine = document.getLineNumber(block.endOffset)
hyperlinkSupport.highlightHyperlinksLater(filter, startLine, endLine, expirableTokenProvider.createExpirable())
}
private fun clearHyperlinks(startOffset: Int, endOffset: Int) {
for (highlighter in getHyperlinks(startOffset, endOffset)) {
hyperlinkSupport.removeHyperlink(highlighter)
}
}
private fun getHyperlinks(startOffset: Int, endOffset: Int): List<RangeHighlighter> {
val result: MutableList<RangeHighlighter> = ArrayList()
processHyperlinks(startOffset, endOffset, CollectProcessor(result))
return result
}
private fun processHyperlinks(startOffset: Int,
endOffset: Int,
processor: Processor<in RangeHighlighter>) {
editor.markupModel.processRangeHighlightersOverlappingWith(
startOffset, endOffset, FilteringProcessor({ it.isValid && EditorHyperlinkSupport.getHyperlinkInfo(it) != null }, processor))
}
}