mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-16 14:23:28 +07:00
terminal: improve hyperlink highlighting (clear previous hyperlinks before applying new ones; calculate filters in NBRA)
GitOrigin-RevId: d82c72d9cb09469d360529a1c862755d13a0e0cb
This commit is contained in:
committed by
intellij-monorepo-bot
parent
44d535e971
commit
c5436c5aa5
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user