mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-246382 [markdown] use highlighter from the editor when calculating wrap position
(cherry picked from commit d1ff4df1b0bfeb9c8fdce3a32723f4f7475c0852) GitOrigin-RevId: 51ef13c7c908558f196c9559480af92d5dfdabec
This commit is contained in:
committed by
intellij-monorepo-bot
parent
d589bf6fe0
commit
ea78481233
@@ -1720,7 +1720,9 @@ com.intellij.openapi.editor.LineNumberConverter$Increasing
|
||||
- getMaxLineNumber(com.intellij.openapi.editor.Editor):java.lang.Integer
|
||||
com.intellij.openapi.editor.LineWrapPositionStrategy
|
||||
- a:calculateWrapPosition(com.intellij.openapi.editor.Document,com.intellij.openapi.project.Project,I,I,I,Z,Z):I
|
||||
- calculateWrapPosition(com.intellij.openapi.editor.Editor,I,I,I,Z,Z):I
|
||||
- canWrapLineAtOffset(java.lang.CharSequence,I):Z
|
||||
- getWrapDocument(com.intellij.openapi.editor.Editor,Z):com.intellij.openapi.editor.Document
|
||||
c:com.intellij.openapi.editor.LogicalPosition
|
||||
- java.lang.Comparable
|
||||
- f:column:I
|
||||
|
||||
+44
-1
@@ -29,7 +29,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
public interface LineWrapPositionStrategy {
|
||||
|
||||
/**
|
||||
* Allows to calculate the most appropriate position to wrap target line.
|
||||
* Allows calculating the most appropriate position to wrap target line.
|
||||
*
|
||||
* @param document target document which text is being processed
|
||||
* @param project target project
|
||||
@@ -51,6 +51,49 @@ public interface LineWrapPositionStrategy {
|
||||
boolean allowToBeyondMaxPreferredOffset, boolean isSoftWrap
|
||||
);
|
||||
|
||||
/**
|
||||
* Serves the same purposes as {@link #calculateWrapPosition(Document, Project, int, int, int, boolean, boolean)} but is given the
|
||||
* editor that shows the target text. Implement this method instead of the one above if the wrap position depends on the editor
|
||||
* context, e.g. on its {@link Editor#getHighlighter() highlighter} or settings.
|
||||
* <p/>
|
||||
* The document which text is being processed is derived from the given editor, see {@link #getWrapDocument(Editor, boolean)}.
|
||||
*
|
||||
* @param editor target editor which text is being processed
|
||||
* @param startOffset start offset to use with the given text holder (exclusive)
|
||||
* @param endOffset end offset to use with the given text holder (exclusive)
|
||||
* @param maxPreferredOffset this method is expected to do its best to return offset that belongs to
|
||||
* {@code (startOffset; maxPreferredOffset]} interval. However, it's allowed
|
||||
* to return value from {@code (maxPreferredOffset; endOffset)} interval
|
||||
* unless {@code 'allowToBeyondMaxPreferredOffset'} if {@code 'false'}
|
||||
* @param allowToBeyondMaxPreferredOffset indicates if it's allowed to return value from
|
||||
* {@code (maxPreferredOffset; endOffset]} interval in case of inability to
|
||||
* find appropriate offset from {@code (startOffset; maxPreferredOffset]} interval
|
||||
* @param isSoftWrap identifies if current request is for isSoftWrap wrap position
|
||||
* @return offset from {@code (startOffset; endOffset)} interval where
|
||||
* target line should be wrapped OR {@code -1} if no wrapping should be performed
|
||||
*/
|
||||
default int calculateWrapPosition(
|
||||
@NotNull Editor editor, int startOffset, int endOffset, int maxPreferredOffset,
|
||||
boolean allowToBeyondMaxPreferredOffset, boolean isSoftWrap
|
||||
) {
|
||||
return calculateWrapPosition(getWrapDocument(editor, isSoftWrap), editor.getProject(), startOffset, endOffset, maxPreferredOffset,
|
||||
allowToBeyondMaxPreferredOffset, isSoftWrap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows choosing the document that holds the text to wrap for the given editor.
|
||||
* <p/>
|
||||
* Soft wrapping is a UI-side layout activity, so it observes the document that the editor currently displays. Hard wrapping modifies
|
||||
* the text instead, so it has to work with the document that is authoritative for PSI, undo and persistence.
|
||||
*
|
||||
* @param editor target editor which text is being processed
|
||||
* @param isSoftWrap identifies if current request is for isSoftWrap wrap position
|
||||
* @return document to calculate wrap positions against for the given editor
|
||||
*/
|
||||
default @NotNull Document getWrapDocument(@NotNull Editor editor, boolean isSoftWrap) {
|
||||
return isSoftWrap ? editor.getElfDocument() : editor.getDocument();
|
||||
}
|
||||
|
||||
/**
|
||||
*The method ensures:
|
||||
* - No breaks within surrogate pairs to prevent visual errors.
|
||||
|
||||
+1
-1
@@ -152,7 +152,7 @@ public class AutoHardWrapHandler {
|
||||
Math.max(0, margin - FormatConstants.getReservedLineWrapWidthInColumns(editor)))
|
||||
);
|
||||
|
||||
int wrapOffset = strategy.calculateWrapPosition(document, project, startOffset, endOffset, maxPreferredOffset, true, false);
|
||||
int wrapOffset = strategy.calculateWrapPosition(editor, startOffset, endOffset, maxPreferredOffset, true, false);
|
||||
if (wrapOffset < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ public final class LineWrappingUtil {
|
||||
|
||||
// We know that current line exceeds right margin if control flow reaches this place, so, wrap it.
|
||||
int wrapOffset = strategy.calculateWrapPosition(
|
||||
document, project, Math.max(startLineOffset, startOffsetToUse), Math.min(endLineOffset, endOffsetToUse),
|
||||
editor, Math.max(startLineOffset, startOffsetToUse), Math.min(endLineOffset, endOffsetToUse),
|
||||
preferredWrapPosition, false, false
|
||||
);
|
||||
if (wrapOffset < 0 // No appropriate wrap position is found.
|
||||
|
||||
@@ -204,9 +204,8 @@ public final class SoftWrapEngine {
|
||||
if (position != -1) return position;
|
||||
}
|
||||
|
||||
int wrapOffset = myLineWrapPositionStrategy.calculateWrapPosition(myDocument, myEditor.getProject(),
|
||||
minOffset - 1, maxOffset + 1, maxOffset + 1,
|
||||
false, true);
|
||||
int wrapOffset = myLineWrapPositionStrategy.calculateWrapPosition(myEditor, minOffset - 1, maxOffset + 1,
|
||||
maxOffset + 1, false, true);
|
||||
if (wrapOffset < 0) return preferMinOffset ? minOffset : maxOffset;
|
||||
if (wrapOffset < minOffset) return minOffset;
|
||||
if (wrapOffset > maxOffset) return maxOffset;
|
||||
|
||||
+77
-152
@@ -2,14 +2,10 @@
|
||||
package org.intellij.plugins.markdown.editor
|
||||
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.GenericLineWrapPositionStrategy
|
||||
import com.intellij.openapi.editor.colors.impl.EmptyColorScheme
|
||||
import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter
|
||||
import com.intellij.openapi.editor.highlighter.HighlighterIterator
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.tree.IElementType
|
||||
import org.intellij.plugins.markdown.editor.MarkdownLineWrapPositionStrategy.Companion.MAX_LEXING_CONTEXT_LENGTH
|
||||
import org.intellij.plugins.markdown.highlighting.MarkdownSyntaxHighlighter
|
||||
import org.intellij.plugins.markdown.lang.MarkdownElementTypes
|
||||
import org.intellij.plugins.markdown.lang.MarkdownTokenTypes
|
||||
|
||||
class MarkdownLineWrapPositionStrategy : GenericLineWrapPositionStrategy() {
|
||||
@@ -36,171 +32,100 @@ class MarkdownLineWrapPositionStrategy : GenericLineWrapPositionStrategy() {
|
||||
addRule(Rule('{', WrapCondition.BEFORE))
|
||||
}
|
||||
|
||||
override fun calculateWrapPosition(document: Document, project: Project?, startOffset: Int, endOffset: Int, maxPreferredOffset: Int,
|
||||
allowToBeyondMaxPreferredOffset: Boolean, isSoftWrap: Boolean): Int {
|
||||
val position = super.calculateWrapPosition(document, project, startOffset, endOffset, maxPreferredOffset,
|
||||
override fun calculateWrapPosition(editor: Editor, startOffset: Int, endOffset: Int,
|
||||
maxPreferredOffset: Int, allowToBeyondMaxPreferredOffset: Boolean,
|
||||
isSoftWrap: Boolean): Int {
|
||||
val document = getWrapDocument(editor, isSoftWrap)
|
||||
val position = super.calculateWrapPosition(document, editor.project, startOffset, endOffset, maxPreferredOffset,
|
||||
allowToBeyondMaxPreferredOffset, isSoftWrap)
|
||||
if (position < 0) return position
|
||||
// Soft wrapping calls this method for every wrapped visual line, so the amount of text lexed per call must stay
|
||||
// bounded, or large documents freeze the editor. The token checks below are line-scoped ([findOnLine] stops at EOL),
|
||||
// and the Markdown constructs they detect (headers, tables, inline links) do not span blank lines, so lexing the
|
||||
// enclosing blank-line-delimited block is enough. All offsets inside the checks are relative to [contextStart].
|
||||
// See IJPL-250302 for details.
|
||||
val text = document.immutableCharSequence
|
||||
val contextStart = findContextStart(text, position)
|
||||
val contextEnd = findContextEnd(text, position)
|
||||
val highlighter = LexerEditorHighlighter(MarkdownSyntaxHighlighter(), EmptyColorScheme.getEmptyScheme())
|
||||
highlighter.setText(text.subSequence(contextStart, contextEnd))
|
||||
|
||||
val decision = classifyForbidden(highlighter, position - contextStart) ?: return position
|
||||
if (decision == ForbiddenDecision.NoWrap) return -1
|
||||
val forbiddenStart = contextStart + (decision as ForbiddenDecision.WrapBefore).offset
|
||||
if (forbiddenStart <= startOffset) return -1
|
||||
val retry = super.calculateWrapPosition(document, project, startOffset, forbiddenStart,
|
||||
val highlighter = editor.highlighter as? LexerEditorHighlighter ?: return if (isSoftWrap) position else -1
|
||||
val forbiddenStart = classifyForbidden(highlighter, document, position) ?: return position
|
||||
if (forbiddenStart == NEVER_WRAP || forbiddenStart <= startOffset) return -1
|
||||
val retry = super.calculateWrapPosition(document, editor.project, startOffset, forbiddenStart,
|
||||
minOf(maxPreferredOffset, forbiddenStart),
|
||||
allowToBeyondMaxPreferredOffset, isSoftWrap)
|
||||
return if (retry > 0) retry else forbiddenStart
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset of the first character of the blank-line-delimited block containing [position],
|
||||
* looking back at most [MAX_LEXING_CONTEXT_LENGTH] characters. For a line longer than the limit the block is cut
|
||||
* mid-line; the checks then degrade to "no forbidden construct found", which only affects the wrap position choice.
|
||||
* Returns the offset of the construct the wrap has to be moved in front of, [NEVER_WRAP] when no position on this
|
||||
* line is acceptable, or null when wrapping at [position] is fine.
|
||||
*/
|
||||
private fun findContextStart(text: CharSequence, position: Int): Int {
|
||||
val lowerBound = (position - MAX_LEXING_CONTEXT_LENGTH).coerceAtLeast(0)
|
||||
var blockStart = lowerBound
|
||||
var lineIsBlank = true
|
||||
for (offset in lowerBound until position) {
|
||||
when (text[offset]) {
|
||||
'\n' -> {
|
||||
if (lineIsBlank) blockStart = offset + 1
|
||||
lineIsBlank = true
|
||||
}
|
||||
' ', '\t' -> {}
|
||||
else -> lineIsBlank = false
|
||||
}
|
||||
}
|
||||
return blockStart
|
||||
}
|
||||
private fun classifyForbidden(highlighter: LexerEditorHighlighter, document: Document, position: Int): Int? {
|
||||
val iterator = highlighter.createIterator(position)
|
||||
if (iterator.atEnd()) return null
|
||||
val type = iterator.tokenType ?: return null
|
||||
|
||||
/**
|
||||
* Returns the end offset (exclusive) of the blank-line-delimited block containing,
|
||||
* looking ahead at most [MAX_LEXING_CONTEXT_LENGTH] characters.
|
||||
*/
|
||||
private fun findContextEnd(text: CharSequence, position: Int): Int {
|
||||
val upperBound = (position + MAX_LEXING_CONTEXT_LENGTH).coerceAtMost(text.length)
|
||||
var lineIsBlank = false // the line containing the wrap candidate is never blank
|
||||
for (offset in position until upperBound) {
|
||||
when (text[offset]) {
|
||||
'\n' -> {
|
||||
if (lineIsBlank) return offset
|
||||
lineIsBlank = true
|
||||
}
|
||||
' ', '\t' -> {}
|
||||
else -> lineIsBlank = false
|
||||
}
|
||||
}
|
||||
return upperBound
|
||||
}
|
||||
// A table row is one CELL token per cell, links inside included, so every position in it is inside the table.
|
||||
if (type in TABLE_TOKENS) return NEVER_WRAP
|
||||
|
||||
private sealed interface ForbiddenDecision {
|
||||
/** Wrapping at the candidate position is forbidden and there is no acceptable retry — return -1. */
|
||||
object NoWrap : ForbiddenDecision
|
||||
/** Wrapping at the candidate position is forbidden; retry with end clamped to [offset] (the construct's start). */
|
||||
data class WrapBefore(val offset: Int) : ForbiddenDecision
|
||||
}
|
||||
val line = document.getLineNumber(position)
|
||||
if (isSetextHeaderContent(highlighter, document, line)) return NEVER_WRAP
|
||||
|
||||
private fun classifyForbidden(highlighter: LexerEditorHighlighter, position: Int): ForbiddenDecision? {
|
||||
val iter = highlighter.createIterator(position)
|
||||
if (iter.atEnd()) return null
|
||||
val type = iter.tokenType ?: return null
|
||||
|
||||
// Headers and tables: leaf tokens at [position] are usually TEXT/WHITE_SPACE inside the construct, so
|
||||
// walk the current line for the construct-defining markers.
|
||||
if (isInsideHeader(highlighter, position) || isInsideTableRow(highlighter, position)) return ForbiddenDecision.NoWrap
|
||||
|
||||
// Inside a link destination / autolink / link title: clear-cut, walk back to the LBRACKET that opens the link.
|
||||
if (type in FORBIDDEN_INSIDE_LINK) {
|
||||
val linkStart = findOnLine(highlighter, position, Direction.BACKWARD) { it == MarkdownTokenTypes.LBRACKET }
|
||||
return ForbiddenDecision.WrapBefore(linkStart ?: iter.start)
|
||||
// The ATX marker and the `[` that opens a link are both found by one walk back over the line. The marker has to be
|
||||
// looked for whatever the token at [position] is, since the content of a header is plain text.
|
||||
var linkStart = NOT_FOUND
|
||||
val lineStart = maxOf(document.getLineStartOffset(line), position - MAX_SCAN_DISTANCE)
|
||||
val tokens = highlighter.createIterator(position)
|
||||
while (!tokens.atEnd() && tokens.start >= lineStart) {
|
||||
val walked = tokens.tokenType
|
||||
if (walked == MarkdownTokenTypes.ATX_HEADER) return NEVER_WRAP
|
||||
if (walked == MarkdownTokenTypes.LBRACKET && linkStart == NOT_FOUND) linkStart = tokens.start
|
||||
if (tokens.start == 0) break
|
||||
tokens.retreat()
|
||||
}
|
||||
|
||||
// Ambiguous brackets/parens: only forbidden when part of a `[…](…)` sequence on this line.
|
||||
// Boundary: wrap exactly at the opening `[` (iter.start == position) is allowed — we wrap BEFORE the link.
|
||||
if (type == MarkdownTokenTypes.LBRACKET && iter.start == position) return null
|
||||
if (type in LINK_BRACKETS) {
|
||||
if (!looksLikeInlineLink(highlighter, position)) return null
|
||||
val linkStart = findOnLine(highlighter, position, Direction.BACKWARD) { it == MarkdownTokenTypes.LBRACKET } ?: return null
|
||||
return ForbiddenDecision.WrapBefore(linkStart)
|
||||
// Inside a destination, an autolink or a title the wrap belongs in front of the whole link.
|
||||
if (type in FORBIDDEN_INSIDE_LINK) return if (linkStart == NOT_FOUND) iterator.start else linkStart
|
||||
// Ambiguous brackets/parens: forbidden only as part of a `[…](…)` sequence, and a wrap exactly at the opening
|
||||
// `[` is allowed, since that already wraps in front of the link.
|
||||
if (type !in LINK_BRACKETS || linkStart == NOT_FOUND ||
|
||||
(type == MarkdownTokenTypes.LBRACKET && iterator.start == position)) {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
return if (hasClosingParen(highlighter, position, document.getLineEndOffset(line))) linkStart else null
|
||||
}
|
||||
|
||||
private fun isInsideHeader(highlighter: LexerEditorHighlighter, position: Int): Boolean =
|
||||
findOnLine(highlighter, position, Direction.BACKWARD) { it in HEADER_TOKENS } != null
|
||||
|
||||
private fun isInsideTableRow(highlighter: LexerEditorHighlighter, position: Int): Boolean {
|
||||
val isSeparator: (IElementType) -> Boolean = { it == MarkdownTokenTypes.TABLE_SEPARATOR }
|
||||
return findOnLine(highlighter, position, Direction.BACKWARD, isSeparator) != null
|
||||
|| findOnLine(highlighter, position, Direction.FORWARD, isSeparator) != null
|
||||
private fun isSetextHeaderContent(highlighter: LexerEditorHighlighter, document: Document, line: Int): Boolean {
|
||||
if (line + 1 >= document.lineCount) return false
|
||||
val iterator = highlighter.createIterator(document.getLineStartOffset(line + 1))
|
||||
return !iterator.atEnd() && iterator.tokenType in SETEXT_UNDERLINES
|
||||
}
|
||||
|
||||
private fun looksLikeInlineLink(highlighter: LexerEditorHighlighter, position: Int): Boolean =
|
||||
findOnLine(highlighter, position, Direction.BACKWARD) { it == MarkdownTokenTypes.LBRACKET } != null &&
|
||||
findOnLine(highlighter, position, Direction.FORWARD) { it == MarkdownTokenTypes.RPAREN } != null
|
||||
|
||||
/**
|
||||
* Walks the highlighter's token stream from [position] in [direction], stopping at EOL or the buffer ends.
|
||||
* Returns the start offset of the first token whose type matches [match], or null if none is found before
|
||||
* the line/buffer boundary.
|
||||
*/
|
||||
private inline fun findOnLine(
|
||||
highlighter: LexerEditorHighlighter,
|
||||
position: Int,
|
||||
direction: Direction,
|
||||
match: (IElementType) -> Boolean,
|
||||
): Int? {
|
||||
val it: HighlighterIterator = highlighter.createIterator(position)
|
||||
while (!it.atEnd()) {
|
||||
val t = it.tokenType
|
||||
if (t == MarkdownTokenTypes.EOL) return null
|
||||
if (t != null && match(t)) return it.start
|
||||
when (direction) {
|
||||
Direction.BACKWARD -> {
|
||||
if (it.start == 0) return null
|
||||
it.retreat()
|
||||
}
|
||||
Direction.FORWARD -> it.advance()
|
||||
}
|
||||
private fun hasClosingParen(highlighter: LexerEditorHighlighter, position: Int, lineEnd: Int): Boolean {
|
||||
val end = minOf(lineEnd, position + MAX_SCAN_DISTANCE)
|
||||
val tokens = highlighter.createIterator(position)
|
||||
while (!tokens.atEnd() && tokens.start < end) {
|
||||
if (tokens.tokenType == MarkdownTokenTypes.RPAREN) return true
|
||||
tokens.advance()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private enum class Direction { BACKWARD, FORWARD }
|
||||
|
||||
companion object {
|
||||
private const val MAX_LEXING_CONTEXT_LENGTH = 4096
|
||||
|
||||
private val FORBIDDEN_INSIDE_LINK = setOf(
|
||||
MarkdownTokenTypes.URL,
|
||||
MarkdownTokenTypes.AUTOLINK,
|
||||
MarkdownTokenTypes.GFM_AUTOLINK,
|
||||
MarkdownTokenTypes.EMAIL_AUTOLINK,
|
||||
MarkdownTokenTypes.LINK_TITLE,
|
||||
)
|
||||
private val LINK_BRACKETS = setOf(
|
||||
MarkdownTokenTypes.LBRACKET,
|
||||
MarkdownTokenTypes.RBRACKET,
|
||||
MarkdownTokenTypes.LPAREN,
|
||||
MarkdownTokenTypes.RPAREN,
|
||||
)
|
||||
private val HEADER_TOKENS = setOf(
|
||||
MarkdownTokenTypes.ATX_HEADER,
|
||||
MarkdownTokenTypes.ATX_CONTENT,
|
||||
MarkdownTokenTypes.SETEXT_1,
|
||||
MarkdownTokenTypes.SETEXT_2,
|
||||
MarkdownTokenTypes.SETEXT_CONTENT,
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private const val NEVER_WRAP = -1
|
||||
private const val NOT_FOUND = -1
|
||||
private const val MAX_SCAN_DISTANCE = 4096
|
||||
|
||||
private val TABLE_TOKENS = setOf(MarkdownElementTypes.TABLE_CELL, MarkdownTokenTypes.TABLE_SEPARATOR)
|
||||
|
||||
private val FORBIDDEN_INSIDE_LINK = setOf(
|
||||
MarkdownTokenTypes.URL,
|
||||
MarkdownTokenTypes.AUTOLINK,
|
||||
MarkdownTokenTypes.GFM_AUTOLINK,
|
||||
MarkdownTokenTypes.EMAIL_AUTOLINK,
|
||||
MarkdownTokenTypes.LINK_TITLE,
|
||||
)
|
||||
|
||||
private val LINK_BRACKETS = setOf(
|
||||
MarkdownTokenTypes.LBRACKET,
|
||||
MarkdownTokenTypes.RBRACKET,
|
||||
MarkdownTokenTypes.LPAREN,
|
||||
MarkdownTokenTypes.RPAREN,
|
||||
)
|
||||
|
||||
private val SETEXT_UNDERLINES = setOf(
|
||||
MarkdownTokenTypes.SETEXT_1,
|
||||
MarkdownTokenTypes.SETEXT_2,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<caret>
|
||||
https://openziti.github.io/ziti/overview/details.html
|
||||
@@ -0,0 +1 @@
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaa<caret> https://openziti.github.io/ziti/overview/details.html
|
||||
@@ -0,0 +1,4 @@
|
||||
```
|
||||
# Gallia est omnis divisa in partes tres, quarum unam incolumnt Belgae,
|
||||
aliam a b c<caret>
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
```
|
||||
# Gallia est omnis divisa in partes tres, quarum unam incolumnt Belgae, aliam <caret>
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
baz asd as das synchronization<caret>das da sd as das d as d as d as d as d as d as d as d as da sd as d as d as d as d as d as da sd
|
||||
===
|
||||
@@ -0,0 +1,2 @@
|
||||
baz asd as das <caret>das da sd as das d as d as d as d as d as d as d as d as da sd as d as d as d as d as d as da sd
|
||||
===
|
||||
@@ -0,0 +1,2 @@
|
||||
Gallia est omnis divisa in partes tres, quarum unam | incolumnt Belgae,
|
||||
aliam a b c<caret>
|
||||
@@ -0,0 +1 @@
|
||||
Gallia est omnis divisa in partes tres, quarum unam | incolumnt Belgae, aliam <caret>
|
||||
+35
-5
@@ -26,22 +26,47 @@ class MarkdownSoftWrapPerformanceTest : BasePlatformTestCase() {
|
||||
}.runAsStressTest().start()
|
||||
}
|
||||
|
||||
fun `test soft wrap recalculation in a large html block`() {
|
||||
myFixture.configureByText("htmlBlock.md", generateHtmlBlockDocument())
|
||||
val editor = myFixture.editor
|
||||
EditorTestUtil.configureSoftWraps(editor, 120)
|
||||
val softWrapModel = editor.softWrapModel as SoftWrapModelImpl
|
||||
Benchmark.newBenchmark("full soft wrap recalculation in a large html block") {
|
||||
softWrapModel.applianceManager.reset()
|
||||
softWrapModel.applianceManager.recalculateIfNecessary("MarkdownSoftWrapPerformanceTest")
|
||||
assertNotEmpty(softWrapModel.registeredSoftWraps)
|
||||
}.runAsStressTest().start()
|
||||
}
|
||||
|
||||
private fun generateHtmlBlockDocument(): String {
|
||||
return buildString {
|
||||
append("<div class=\"wrapper\">\n")
|
||||
var wordIndex = 0
|
||||
repeat(2000) {
|
||||
append(" <p>")
|
||||
repeat(12) {
|
||||
append(LONG_WORDS[wordIndex % LONG_WORDS.size])
|
||||
append(' ')
|
||||
wordIndex++
|
||||
}
|
||||
append("</p>\n")
|
||||
}
|
||||
append("</div>\n")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ~1.4 MB of markdown built from long words, so that many soft wrap positions have no whitespace within
|
||||
* the quick look-back distance of [com.intellij.openapi.editor.impl.SoftWrapEngine] and the position
|
||||
* calculation is delegated to [org.intellij.plugins.markdown.editor.MarkdownLineWrapPositionStrategy].
|
||||
*/
|
||||
private fun generateLargeDocument(): String {
|
||||
val words = listOf(
|
||||
"internationalization", "misconfiguration", "straightforwardness", "characterization", "acknowledgements",
|
||||
"responsibilities", "incompatibilities", "synchronization", "parallelization", "representations",
|
||||
)
|
||||
return buildString {
|
||||
var wordIndex = 0
|
||||
repeat(600) { paragraphIndex ->
|
||||
repeat(8) { lineIndex ->
|
||||
repeat(14) {
|
||||
append(words[wordIndex % words.size])
|
||||
append(LONG_WORDS[wordIndex % LONG_WORDS.size])
|
||||
append(' ')
|
||||
wordIndex++
|
||||
}
|
||||
@@ -52,3 +77,8 @@ class MarkdownSoftWrapPerformanceTest : BasePlatformTestCase() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val LONG_WORDS = listOf(
|
||||
"internationalization", "misconfiguration", "straightforwardness", "characterization", "acknowledgements",
|
||||
"responsibilities", "incompatibilities", "synchronization", "parallelization", "representations",
|
||||
)
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ class MarkdownSoftWrapTest : BasePlatformTestCase() {
|
||||
/**
|
||||
* The wrap candidate falls inside the link, where wrapping is forbidden, so the strategy has to move
|
||||
* the soft wrap before the link start. The link is placed after several blank-line-separated paragraphs to check
|
||||
* that [org.intellij.plugins.markdown.editor.MarkdownLineWrapPositionStrategy] resolves tokens correctly when
|
||||
* the lexed context does not start at the beginning of the document.
|
||||
* that [com.intellij.markdown.frontend.editor.MarkdownLineWrapPositionStrategy] resolves offsets correctly when
|
||||
* the line under test does not start at the beginning of the document.
|
||||
*/
|
||||
fun `test soft wrap does not break a link located after the first block`() {
|
||||
val filler = "Introductory paragraph that occupies space before the block under test.\n\n".repeat(3)
|
||||
|
||||
@@ -63,6 +63,11 @@ public class MarkdownWrapTest extends BasePlatformTestCase {
|
||||
doTest("a b c");
|
||||
}
|
||||
|
||||
/** Fence content is literal, so a `#` at the start of a fenced line is not a header and the line is wrappable. */
|
||||
public void testWrapInCodeFenceWithHash() {
|
||||
doTest("a b c");
|
||||
}
|
||||
|
||||
public void testWrapInCodeFenceInQuotes() {
|
||||
doTest("a b c d e f");
|
||||
}
|
||||
@@ -75,15 +80,28 @@ public class MarkdownWrapTest extends BasePlatformTestCase {
|
||||
doTest("synchronization");
|
||||
}
|
||||
|
||||
public void testWrapInSetextHeader() {
|
||||
doTest("synchronization");
|
||||
}
|
||||
|
||||
|
||||
public void testWrapInTable() {
|
||||
doTest("synchronization");
|
||||
}
|
||||
|
||||
/** A `|` outside of a table does not make the line a table row, so wrapping is still allowed. */
|
||||
public void testWrapWithPipeInText() {
|
||||
doTest("a b c");
|
||||
}
|
||||
|
||||
public void testWrapDoesNotBreakLink() {
|
||||
doTest("a");
|
||||
}
|
||||
|
||||
public void testWrapDoesNotBreakBareUrl() {
|
||||
doTest("a");
|
||||
}
|
||||
|
||||
public void testWrapDoesNotBreakLinkWhenTypingInsideLink() {
|
||||
doTest("X");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user