1
0
mirror of https://github.com/chylex/IntelliJ-Rainbow-Brackets.git synced 2025-05-11 03:34:04 +02:00

mirror changes from [Extract IndentGuideRenderer into a separate class](efe95372f2)

This commit is contained in:
张志豪 2021-08-13 11:08:59 +08:00
parent ca4212b955
commit b80a898619
2 changed files with 253 additions and 240 deletions
src/main/kotlin/com/github/izhangzhihao/rainbow/brackets/indents

View File

@ -0,0 +1,252 @@
package com.github.izhangzhihao.rainbow.brackets.indents
import com.github.izhangzhihao.rainbow.brackets.RainbowInfo
import com.github.izhangzhihao.rainbow.brackets.util.*
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.SoftWrap
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.editor.impl.view.EditorPainter
import com.intellij.openapi.editor.impl.view.VisualLinesIterator
import com.intellij.openapi.editor.markup.CustomHighlighterRenderer
import com.intellij.openapi.editor.markup.RangeHighlighter
import com.intellij.openapi.util.Condition
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.xml.XmlFile
import com.intellij.psi.xml.XmlTag
import com.intellij.psi.xml.XmlToken
import com.intellij.psi.xml.XmlTokenType
import com.intellij.ui.paint.LinePainter2D
import com.intellij.util.text.CharArrayUtil
import java.awt.Graphics
import java.awt.Graphics2D
class RainbowIndentGuideRenderer: CustomHighlighterRenderer {
override fun paint(editor: Editor, highlighter: RangeHighlighter, g: Graphics) {
if (editor !is EditorEx) return
val rainbowInfo = getRainbowInfo(editor, highlighter) ?: return
val startOffset = highlighter.startOffset
val doc = highlighter.document
if (startOffset >= doc.textLength) return
val endOffset = highlighter.endOffset
var off: Int
var startLine = doc.getLineNumber(startOffset)
val chars = doc.charsSequence
do {
val start = doc.getLineStartOffset(startLine)
val end = doc.getLineEndOffset(startLine)
off = CharArrayUtil.shiftForward(chars, start, end, " \t")
startLine--
} while (startLine > 1 && off < doc.textLength && chars[off] == '\n')
val startPosition = editor.offsetToVisualPosition(off)
val indentColumn = startPosition.column
if (indentColumn <= 0) return
val foldingModel = editor.foldingModel
if (foldingModel.isOffsetCollapsed(off)) return
val headerRegion = foldingModel.getCollapsedRegionAtOffset(doc.getLineEndOffset(doc.getLineNumber(off)))
val tailRegion = foldingModel.getCollapsedRegionAtOffset(doc.getLineStartOffset(doc.getLineNumber(endOffset)))
if (tailRegion != null && tailRegion === headerRegion) return
val guide = editor.indentsModel.caretIndentGuide
val selected = if (guide != null) {
val caretModel = editor.caretModel
val caretOffset = caretModel.offset
caretOffset in off until endOffset && caretModel.logicalPosition.column == indentColumn
} else false
val lineHeight = editor.getLineHeight()
val start = editor.visualPositionToXY(startPosition)
start.y += lineHeight
val endPosition = editor.offsetToVisualPosition(endOffset)
val end = editor.visualPositionToXY(endPosition)
var maxY = end.y
if (endPosition.line == editor.offsetToVisualPosition(doc.textLength).line) {
maxY += lineHeight
}
val clip = g.clipBounds
if (clip != null) {
if (clip.y >= maxY || clip.y + clip.height <= start.y) {
return
}
maxY = StrictMath.min(maxY, clip.y + clip.height)
}
if (start.y >= maxY) return
val targetX = Math.max(0, start.x + EditorPainter.getIndentGuideShift(editor)).toDouble()
g.color = if (selected) {
rainbowInfo.color
} else {
val defaultBackground = editor.colorsScheme.defaultBackground
rainbowInfo.color.alphaBlend(defaultBackground, 0.2f)
}
// There is a possible case that indent line intersects soft wrap-introduced text. Example:
// this is a long line <soft-wrap>
// that| is soft-wrapped
// |
// | <- vertical indent
//
// Also it's possible that no additional intersections are added because of soft wrap:
// this is a long line <soft-wrap>
// | that is soft-wrapped
// |
// | <- vertical indent
// We want to use the following approach then:
// 1. Show only active indent if it crosses soft wrap-introduced text;
// 2. Show indent as is if it doesn't intersect with soft wrap-introduced text;
val softWraps = editor.softWrapModel.registeredSoftWraps
if (selected || softWraps.isEmpty()) {
LinePainter2D.paint(g as Graphics2D, targetX, start.y.toDouble(), targetX, maxY - 1.toDouble())
} else {
var startY = start.y
var startVisualLine = startPosition.line + 1
if (clip != null && startY < clip.y) {
startY = clip.y
startVisualLine = editor.yToVisualLine(clip.y)
}
val it = VisualLinesIterator(editor as EditorImpl, startVisualLine)
while (!it.atEnd()) {
val currY: Int = it.y
if (currY >= startY) {
if (currY >= maxY) break
if (it.startsWithSoftWrap()) {
val softWrap: SoftWrap = softWraps[it.startOrPrevWrapIndex]
if (softWrap.indentInColumns < indentColumn) {
if (startY < currY) {
LinePainter2D.paint((g as Graphics2D), targetX, startY.toDouble(), targetX, currY - 1.toDouble())
}
startY = currY + lineHeight
}
}
}
it.advance()
}
if (startY < maxY) {
LinePainter2D.paint((g as Graphics2D), targetX, startY.toDouble(), targetX, maxY - 1.toDouble())
}
}
}
companion object {
private val XML_TAG_PARENT_CONDITION = Condition<PsiElement> { it is XmlTag }
private val XML_END_TAG_START_CONDITION: (PsiElement) -> Boolean = { element ->
element is XmlToken && element.tokenType == XmlTokenType.XML_END_TAG_START
}
private val XML_TAG_END_CONDITION: (PsiElement) -> Boolean = { element ->
element is XmlToken && element.tokenType == XmlTokenType.XML_TAG_END
}
private fun getRainbowInfo(editor: EditorEx, highlighter: RangeHighlighter): RainbowInfo? {
val virtualFile = editor.virtualFile?.takeIf { it.isValid } ?: return null
val document = editor.document
val project = editor.project ?: return null
val psiFile = PsiManager.getInstance(project).findFile(virtualFile) ?: return null
var element = try {
psiFile.findElementAt(highlighter.endOffset)?.parent ?: return null
} catch (e: Throwable) {
return null
}
var rainbowInfo = RainbowInfo.RAINBOW_INFO_KEY[element]
if (rainbowInfo == null && psiFile is XmlFile && element !is XmlTag) {
element = PsiTreeUtil.findFirstParent(element, true, XML_TAG_PARENT_CONDITION) ?: return null
rainbowInfo = RainbowInfo.RAINBOW_INFO_KEY[element] ?: return null
}
if (!element.isValid || !checkBoundary(document, element, highlighter)) {
return null
}
return rainbowInfo
}
private fun checkBoundary(document: Document, element: PsiElement, highlighter: RangeHighlighter): Boolean {
val elementStartLine = document.lineNumber(element.startOffset) ?: return false
val highlighterStartLine = document.lineNumber(highlighter.startOffset) ?: return false
var xmlStartTagEndLine: Int? = null
var xmlEndTagStartLine: Int? = null
val isValidStartBoundary = if (element is XmlTag) {
/*
* <tag // [*] element & highlighter start line
* | <- vertical indent
* > // [*] highlighter start/end line, start tag end line
* | <- vertical indent
* </tag // [*] highlighter start/end line, end tag start line
* | <- vertical indent
* > // [ ] element/highlighter end line
*/
xmlStartTagEndLine = element.getStartTagEndLineNumber(document)
xmlEndTagStartLine = element.getEndTagStartLineNumber(document)
highlighterStartLine == elementStartLine ||
highlighterStartLine == xmlStartTagEndLine ||
highlighterStartLine == xmlEndTagStartLine
} else {
/*
* Element start line > Highlighter start line:
* function foo(arg1, // highlighter start line
* | arg2) { // element start line
* | <- vertical indent
* } // element & highlighter end line
*/
elementStartLine >= highlighterStartLine
}
if (!isValidStartBoundary) {
return false
}
val elementEndLine = document.lineNumber(element.endOffset) ?: return false
val highlighterEndLine = document.lineNumber(highlighter.endOffset) ?: return false
val isValidEndBoundary = if (element is XmlTag) {
/*
* <tag // [ ] element & highlighter start line
* | <- vertical indent
* > // [*] highlighter start/end line, start tag end line
* | <- vertical indent
* </tag // [*] highlighter start/end line, end tag start line
* | <- vertical indent
* > // [*] element/highlighter end line
*/
highlighterEndLine == elementEndLine ||
highlighterEndLine == xmlStartTagEndLine ||
highlighterEndLine == xmlEndTagStartLine
} else {
/*
* Element end line != Highlighter end line:
* function foo() { // element & highlighter start line
* | <- vertical indent
* var bar = "bar"; // highlighter end line
* } // element end line
*/
elementEndLine == highlighterEndLine
}
if (!isValidEndBoundary) {
return false
}
return true
}
private fun XmlTag.getStartTagEndLineNumber(document: Document): Int? =
firstChild?.findNextSibling(XML_TAG_END_CONDITION)?.let { document.lineNumber(it.startOffset) }
private fun XmlTag.getEndTagStartLineNumber(document: Document): Int? =
lastChild?.findPrevSibling(XML_END_TAG_START_CONDITION)?.let { document.lineNumber(it.startOffset) }
}
}

View File

@ -1,25 +1,16 @@
package com.github.izhangzhihao.rainbow.brackets.indents
import com.github.izhangzhihao.rainbow.brackets.RainbowInfo
import com.github.izhangzhihao.rainbow.brackets.settings.RainbowSettings
import com.github.izhangzhihao.rainbow.brackets.util.*
import com.intellij.codeHighlighting.TextEditorHighlightingPass
import com.intellij.codeInsight.highlighting.BraceMatchingUtil
import com.intellij.codeInsight.highlighting.CodeBlockSupportHandler
import com.intellij.ide.actions.ToggleZenModeAction
import com.intellij.lang.Language
import com.intellij.lang.LanguageParserDefinitions
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.IndentGuideDescriptor
import com.intellij.openapi.editor.SoftWrap
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.editor.impl.view.EditorPainter
import com.intellij.openapi.editor.impl.view.VisualLinesIterator
import com.intellij.openapi.editor.markup.CustomHighlighterRenderer
import com.intellij.openapi.editor.markup.HighlighterTargetArea
import com.intellij.openapi.editor.markup.MarkupModel
import com.intellij.openapi.editor.markup.RangeHighlighter
@ -27,24 +18,14 @@ import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Condition
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.Segment
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.tree.TokenSet
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.xml.XmlFile
import com.intellij.psi.xml.XmlTag
import com.intellij.psi.xml.XmlToken
import com.intellij.psi.xml.XmlTokenType
import com.intellij.ui.paint.LinePainter2D
import com.intellij.util.DocumentUtil
import com.intellij.util.containers.IntStack
import com.intellij.util.text.CharArrayUtil
import java.awt.Graphics2D
import java.lang.StrictMath.abs
import java.lang.StrictMath.min
import java.util.*
@ -349,227 +330,7 @@ class RainbowIndentsPass internal constructor(
private val INDENT_HIGHLIGHTERS_IN_EDITOR_KEY = Key.create<MutableList<RangeHighlighter>>("_INDENT_HIGHLIGHTERS_IN_EDITOR_KEY_")
private val LAST_TIME_INDENTS_BUILT = Key.create<Long>("_LAST_TIME_INDENTS_BUILT_")
private val XML_TAG_PARENT_CONDITION = Condition<PsiElement> { it is XmlTag }
private val XML_END_TAG_START_CONDITION: (PsiElement) -> Boolean = { element ->
element is XmlToken && element.tokenType == XmlTokenType.XML_END_TAG_START
}
private val XML_TAG_END_CONDITION: (PsiElement) -> Boolean = { element ->
element is XmlToken && element.tokenType == XmlTokenType.XML_TAG_END
}
private val RENDERER: CustomHighlighterRenderer = CustomHighlighterRenderer renderer@{ editor, highlighter, g ->
if (editor !is EditorEx) return@renderer
val rainbowInfo = getRainbowInfo(editor, highlighter) ?: return@renderer
val startOffset = highlighter.startOffset
val doc = highlighter.document
if (startOffset >= doc.textLength) return@renderer
val endOffset = highlighter.endOffset
var off: Int
var startLine = doc.getLineNumber(startOffset)
val chars = doc.charsSequence
do {
val start = doc.getLineStartOffset(startLine)
val end = doc.getLineEndOffset(startLine)
off = CharArrayUtil.shiftForward(chars, start, end, " \t")
startLine--
} while (startLine > 1 && off < doc.textLength && chars[off] == '\n')
val startPosition = editor.offsetToVisualPosition(off)
val indentColumn = startPosition.column
if (indentColumn <= 0) return@renderer
val foldingModel = editor.foldingModel
if (foldingModel.isOffsetCollapsed(off)) return@renderer
val headerRegion = foldingModel.getCollapsedRegionAtOffset(doc.getLineEndOffset(doc.getLineNumber(off)))
val tailRegion = foldingModel.getCollapsedRegionAtOffset(doc.getLineStartOffset(doc.getLineNumber(endOffset)))
if (tailRegion != null && tailRegion === headerRegion) return@renderer
val guide = editor.indentsModel.caretIndentGuide
val selected = if (guide != null) {
val caretModel = editor.caretModel
val caretOffset = caretModel.offset
caretOffset in off until endOffset && caretModel.logicalPosition.column == indentColumn
} else false
val lineHeight = editor.getLineHeight()
val start = editor.visualPositionToXY(startPosition)
start.y += lineHeight
val endPosition = editor.offsetToVisualPosition(endOffset)
val end = editor.visualPositionToXY(endPosition)
var maxY = end.y
if (endPosition.line == editor.offsetToVisualPosition(doc.textLength).line) {
maxY += lineHeight
}
val clip = g.clipBounds
if (clip != null) {
if (clip.y >= maxY || clip.y + clip.height <= start.y) {
return@renderer
}
maxY = min(maxY, clip.y + clip.height)
}
if (start.y >= maxY) return@renderer
val targetX = Math.max(0, start.x + EditorPainter.getIndentGuideShift(editor)).toDouble()
g.color = if (selected) {
rainbowInfo.color
} else {
val defaultBackground = editor.colorsScheme.defaultBackground
rainbowInfo.color.alphaBlend(defaultBackground, 0.2f)
}
// There is a possible case that indent line intersects soft wrap-introduced text. Example:
// this is a long line <soft-wrap>
// that| is soft-wrapped
// |
// | <- vertical indent
//
// Also it's possible that no additional intersections are added because of soft wrap:
// this is a long line <soft-wrap>
// | that is soft-wrapped
// |
// | <- vertical indent
// We want to use the following approach then:
// 1. Show only active indent if it crosses soft wrap-introduced text;
// 2. Show indent as is if it doesn't intersect with soft wrap-introduced text;
val softWraps = editor.softWrapModel.registeredSoftWraps
if (selected || softWraps.isEmpty()) {
LinePainter2D.paint(g as Graphics2D, targetX, start.y.toDouble(), targetX, maxY - 1.toDouble())
} else {
var startY = start.y
var startVisualLine = startPosition.line + 1
if (clip != null && startY < clip.y) {
startY = clip.y
startVisualLine = editor.yToVisualLine(clip.y)
}
val it = VisualLinesIterator(editor as EditorImpl, startVisualLine)
while (!it.atEnd()) {
val currY: Int = it.y
if (currY >= startY) {
if (currY >= maxY) break
if (it.startsWithSoftWrap()) {
val softWrap: SoftWrap = softWraps[it.startOrPrevWrapIndex]
if (softWrap.indentInColumns < indentColumn) {
if (startY < currY) {
LinePainter2D.paint((g as Graphics2D), targetX, startY.toDouble(), targetX, currY - 1.toDouble())
}
startY = currY + lineHeight
}
}
}
it.advance()
}
if (startY < maxY) {
LinePainter2D.paint((g as Graphics2D), targetX, startY.toDouble(), targetX, maxY - 1.toDouble())
}
}
}
private fun getRainbowInfo(editor: EditorEx, highlighter: RangeHighlighter): RainbowInfo? {
val virtualFile = editor.virtualFile?.takeIf { it.isValid } ?: return null
val document = editor.document
val project = editor.project ?: return null
val psiFile = PsiManager.getInstance(project).findFile(virtualFile) ?: return null
var element = try {
psiFile.findElementAt(highlighter.endOffset)?.parent ?: return null
} catch (e: Throwable) {
return null
}
var rainbowInfo = RainbowInfo.RAINBOW_INFO_KEY[element]
if (rainbowInfo == null && psiFile is XmlFile && element !is XmlTag) {
element = PsiTreeUtil.findFirstParent(element, true, XML_TAG_PARENT_CONDITION) ?: return null
rainbowInfo = RainbowInfo.RAINBOW_INFO_KEY[element] ?: return null
}
if (!element.isValid || !checkBoundary(document, element, highlighter)) {
return null
}
return rainbowInfo
}
private fun checkBoundary(document: Document, element: PsiElement, highlighter: RangeHighlighter): Boolean {
val elementStartLine = document.lineNumber(element.startOffset) ?: return false
val highlighterStartLine = document.lineNumber(highlighter.startOffset) ?: return false
var xmlStartTagEndLine: Int? = null
var xmlEndTagStartLine: Int? = null
val isValidStartBoundary = if (element is XmlTag) {
/*
* <tag // [*] element & highlighter start line
* | <- vertical indent
* > // [*] highlighter start/end line, start tag end line
* | <- vertical indent
* </tag // [*] highlighter start/end line, end tag start line
* | <- vertical indent
* > // [ ] element/highlighter end line
*/
xmlStartTagEndLine = element.getStartTagEndLineNumber(document)
xmlEndTagStartLine = element.getEndTagStartLineNumber(document)
highlighterStartLine == elementStartLine ||
highlighterStartLine == xmlStartTagEndLine ||
highlighterStartLine == xmlEndTagStartLine
} else {
/*
* Element start line > Highlighter start line:
* function foo(arg1, // highlighter start line
* | arg2) { // element start line
* | <- vertical indent
* } // element & highlighter end line
*/
elementStartLine >= highlighterStartLine
}
if (!isValidStartBoundary) {
return false
}
val elementEndLine = document.lineNumber(element.endOffset) ?: return false
val highlighterEndLine = document.lineNumber(highlighter.endOffset) ?: return false
val isValidEndBoundary = if (element is XmlTag) {
/*
* <tag // [ ] element & highlighter start line
* | <- vertical indent
* > // [*] highlighter start/end line, start tag end line
* | <- vertical indent
* </tag // [*] highlighter start/end line, end tag start line
* | <- vertical indent
* > // [*] element/highlighter end line
*/
highlighterEndLine == elementEndLine ||
highlighterEndLine == xmlStartTagEndLine ||
highlighterEndLine == xmlEndTagStartLine
} else {
/*
* Element end line != Highlighter end line:
* function foo() { // element & highlighter start line
* | <- vertical indent
* var bar = "bar"; // highlighter end line
* } // element end line
*/
elementEndLine == highlighterEndLine
}
if (!isValidEndBoundary) {
return false
}
return true
}
private fun XmlTag.getStartTagEndLineNumber(document: Document): Int? =
firstChild?.findNextSibling(XML_TAG_END_CONDITION)?.let { document.lineNumber(it.startOffset) }
private fun XmlTag.getEndTagStartLineNumber(document: Document): Int? =
lastChild?.findPrevSibling(XML_END_TAG_START_CONDITION)?.let { document.lineNumber(it.startOffset) }
private val RENDERER = RainbowIndentGuideRenderer()
private fun isRainbowIndentGuidesShown(project: Project): Boolean {
if (RainbowSettings.instance.disableRainbowIndentsInZenMode && isZenModeEnabled(project)) {