[markdown] IJPL-163534 synchronize scrolling between editor and preview, Jewel part

closes https://github.com/JetBrains/intellij-community/pull/2908

(cherry picked from commit 75d53f772501fbb0d4f086cc5213dc577e2340de)


(cherry picked from commit 0f78a716d6091dd56d3e252b53793a016949745c)

IJ-MR-155570

GitOrigin-RevId: ae404d2d83ca6a03c66421ddb4b500bf24adfec8
This commit is contained in:
Alexander Kuznetsov
2025-01-14 17:18:11 +01:00
committed by intellij-monorepo-bot
parent e3934392b4
commit 6b4b56a99a
20 changed files with 1681 additions and 103 deletions

View File

@@ -178,6 +178,18 @@ f:org.jetbrains.jewel.markdown.MarkdownKt
- sf:LazyMarkdown(java.util.List,androidx.compose.ui.Modifier,androidx.compose.foundation.layout.PaddingValues,androidx.compose.foundation.lazy.LazyListState,Z,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function0,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,androidx.compose.runtime.Composer,I,I):V
- sf:Markdown(java.lang.String,androidx.compose.ui.Modifier,Z,Z,kotlinx.coroutines.CoroutineDispatcher,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function0,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.processing.MarkdownProcessor,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,androidx.compose.runtime.Composer,I,I):V
- sf:Markdown(java.util.List,java.lang.String,androidx.compose.ui.Modifier,Z,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function0,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,androidx.compose.runtime.Composer,I,I):V
org.jetbrains.jewel.markdown.MarkdownMode
f:org.jetbrains.jewel.markdown.MarkdownMode$EditorPreview
- org.jetbrains.jewel.markdown.MarkdownMode
- sf:$stable:I
- <init>(org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer):V
- f:getScrollingSynchronizer():org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer
f:org.jetbrains.jewel.markdown.MarkdownMode$Standalone
- org.jetbrains.jewel.markdown.MarkdownMode
- sf:$stable:I
- sf:INSTANCE:org.jetbrains.jewel.markdown.MarkdownMode$Standalone
f:org.jetbrains.jewel.markdown.MarkdownModeKt
- sf:WithMarkdownMode(org.jetbrains.jewel.markdown.MarkdownMode,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I):V
f:org.jetbrains.jewel.markdown.SemanticsKt
- sf:getRawMarkdown():androidx.compose.ui.semantics.SemanticsPropertyKey
- sf:getRawMarkdown(androidx.compose.ui.semantics.SemanticsPropertyReceiver):java.lang.String
@@ -200,9 +212,11 @@ org.jetbrains.jewel.markdown.extensions.MarkdownInlineRendererExtension
- a:render(org.jetbrains.jewel.markdown.InlineMarkdown$CustomNode,org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer,Z):V
f:org.jetbrains.jewel.markdown.extensions.MarkdownKt
- sf:getLocalMarkdownBlockRenderer():androidx.compose.runtime.ProvidableCompositionLocal
- sf:getLocalMarkdownMode():androidx.compose.runtime.ProvidableCompositionLocal
- sf:getLocalMarkdownProcessor():androidx.compose.runtime.ProvidableCompositionLocal
- sf:getLocalMarkdownStyling():androidx.compose.runtime.ProvidableCompositionLocal
- sf:getMarkdownBlockRenderer(org.jetbrains.jewel.foundation.theme.JewelTheme$Companion,androidx.compose.runtime.Composer,I):org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer
- sf:getMarkdownMode(org.jetbrains.jewel.foundation.theme.JewelTheme$Companion,androidx.compose.runtime.Composer,I):org.jetbrains.jewel.markdown.MarkdownMode
- sf:getMarkdownProcessor(org.jetbrains.jewel.foundation.theme.JewelTheme$Companion,androidx.compose.runtime.Composer,I):org.jetbrains.jewel.markdown.processing.MarkdownProcessor
- sf:getMarkdownStyling(org.jetbrains.jewel.foundation.theme.JewelTheme$Companion,androidx.compose.runtime.Composer,I):org.jetbrains.jewel.markdown.rendering.MarkdownStyling
org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension
@@ -221,8 +235,8 @@ f:org.jetbrains.jewel.markdown.processing.MarkdownParserFactory
f:org.jetbrains.jewel.markdown.processing.MarkdownProcessor
- sf:$stable:I
- <init>():V
- <init>(java.util.List,Z,org.commonmark.parser.Parser):V
- b:<init>(java.util.List,Z,org.commonmark.parser.Parser,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- <init>(java.util.List,org.jetbrains.jewel.markdown.MarkdownMode,org.commonmark.parser.Parser):V
- b:<init>(java.util.List,org.jetbrains.jewel.markdown.MarkdownMode,org.commonmark.parser.Parser,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- f:processChildren(org.commonmark.node.Node):java.util.List
- f:processMarkdownDocument(java.lang.String):java.util.List
c:org.jetbrains.jewel.markdown.rendering.DefaultInlineMarkdownRenderer
@@ -237,6 +251,7 @@ c:org.jetbrains.jewel.markdown.rendering.DefaultMarkdownBlockRenderer
- sf:$stable:I
- <init>(org.jetbrains.jewel.markdown.rendering.MarkdownStyling,java.util.List,org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer):V
- b:<init>(org.jetbrains.jewel.markdown.rendering.MarkdownStyling,java.util.List,org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- pf:MaybeScrollingContainer(Z,androidx.compose.ui.Modifier,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
- render(java.util.List,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function0,androidx.compose.runtime.Composer,I):V
- render(org.jetbrains.jewel.markdown.MarkdownBlock$BlockQuote,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$BlockQuote,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function0,androidx.compose.runtime.Composer,I):V
- render(org.jetbrains.jewel.markdown.MarkdownBlock$CodeBlock$FencedCodeBlock,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code$Fenced,androidx.compose.runtime.Composer,I):V
@@ -251,6 +266,7 @@ c:org.jetbrains.jewel.markdown.rendering.DefaultMarkdownBlockRenderer
- render(org.jetbrains.jewel.markdown.MarkdownBlock$ListItem,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function0,androidx.compose.runtime.Composer,I):V
- render(org.jetbrains.jewel.markdown.MarkdownBlock$Paragraph,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Paragraph,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function0,androidx.compose.runtime.Composer,I):V
- render(org.jetbrains.jewel.markdown.MarkdownBlock,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function0,androidx.compose.runtime.Composer,I):V
- render-EWr_ITI(org.jetbrains.jewel.markdown.MarkdownBlock$CodeBlock$FencedCodeBlock,java.lang.String,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code$Fenced,androidx.compose.runtime.Composer,I):V
- renderThematicBreak(org.jetbrains.jewel.markdown.rendering.MarkdownStyling$ThematicBreak,androidx.compose.runtime.Composer,I):V
org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer
- a:renderAsAnnotatedString(java.lang.Iterable,org.jetbrains.jewel.markdown.rendering.InlinesStyling,Z,kotlin.jvm.functions.Function1):androidx.compose.ui.text.AnnotatedString
@@ -545,3 +561,26 @@ org.jetbrains.jewel.markdown.rendering.WithUnderline
- a:getUnderlineColor-0d7_KjU():J
- a:getUnderlineGap-D9Ej5fM():F
- a:getUnderlineWidth-D9Ej5fM():F
f:org.jetbrains.jewel.markdown.scrolling.AutoScrollingUtilKt
- sf:AutoScrollableBlock(org.jetbrains.jewel.markdown.MarkdownBlock,org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer,androidx.compose.ui.Modifier,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
c:org.jetbrains.jewel.markdown.scrolling.ScrollSyncMarkdownBlockRenderer
- org.jetbrains.jewel.markdown.rendering.DefaultMarkdownBlockRenderer
- sf:$stable:I
- <init>(org.jetbrains.jewel.markdown.rendering.MarkdownStyling,java.util.List,org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer):V
- render(org.jetbrains.jewel.markdown.MarkdownBlock$CodeBlock$IndentedCodeBlock,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code$Indented,androidx.compose.runtime.Composer,I):V
- render(org.jetbrains.jewel.markdown.MarkdownBlock$Heading,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Heading$HN,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function0,androidx.compose.runtime.Composer,I):V
- render(org.jetbrains.jewel.markdown.MarkdownBlock$Paragraph,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Paragraph,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function0,androidx.compose.runtime.Composer,I):V
- render-EWr_ITI(org.jetbrains.jewel.markdown.MarkdownBlock$CodeBlock$FencedCodeBlock,java.lang.String,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code$Fenced,androidx.compose.runtime.Composer,I):V
a:org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer
- sf:$stable:I
- sf:Companion:org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer$Companion
- <init>():V
- a:acceptBlockSpans(org.jetbrains.jewel.markdown.MarkdownBlock,kotlin.ranges.IntRange):V
- a:acceptGlobalPosition(org.jetbrains.jewel.markdown.MarkdownBlock,androidx.compose.ui.layout.LayoutCoordinates):V
- a:acceptTextLayout(org.jetbrains.jewel.markdown.MarkdownBlock,androidx.compose.ui.text.TextLayoutResult):V
- pa:afterProcessing():V
- pa:beforeProcessing():V
- f:process(kotlin.jvm.functions.Function0):java.lang.Object
- a:scrollToLine(I,kotlin.coroutines.Continuation):java.lang.Object
f:org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer$Companion
- f:create(androidx.compose.foundation.gestures.ScrollableState):org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer

View File

@@ -222,6 +222,24 @@ public final class org/jetbrains/jewel/markdown/MarkdownKt {
public static final fun Markdown (Ljava/util/List;Ljava/lang/String;Landroidx/compose/ui/Modifier;ZZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer;Landroidx/compose/runtime/Composer;II)V
}
public abstract interface class org/jetbrains/jewel/markdown/MarkdownMode {
}
public final class org/jetbrains/jewel/markdown/MarkdownMode$EditorPreview : org/jetbrains/jewel/markdown/MarkdownMode {
public static final field $stable I
public fun <init> (Lorg/jetbrains/jewel/markdown/scrolling/ScrollingSynchronizer;)V
public final fun getScrollingSynchronizer ()Lorg/jetbrains/jewel/markdown/scrolling/ScrollingSynchronizer;
}
public final class org/jetbrains/jewel/markdown/MarkdownMode$Standalone : org/jetbrains/jewel/markdown/MarkdownMode {
public static final field $stable I
public static final field INSTANCE Lorg/jetbrains/jewel/markdown/MarkdownMode$Standalone;
}
public final class org/jetbrains/jewel/markdown/MarkdownModeKt {
public static final fun WithMarkdownMode (Lorg/jetbrains/jewel/markdown/MarkdownMode;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
}
public final class org/jetbrains/jewel/markdown/SemanticsKt {
public static final fun getRawMarkdown ()Landroidx/compose/ui/semantics/SemanticsPropertyKey;
public static final fun getRawMarkdown (Landroidx/compose/ui/semantics/SemanticsPropertyReceiver;)Ljava/lang/String;
@@ -258,9 +276,11 @@ public abstract interface class org/jetbrains/jewel/markdown/extensions/Markdown
public final class org/jetbrains/jewel/markdown/extensions/MarkdownKt {
public static final fun getLocalMarkdownBlockRenderer ()Landroidx/compose/runtime/ProvidableCompositionLocal;
public static final fun getLocalMarkdownMode ()Landroidx/compose/runtime/ProvidableCompositionLocal;
public static final fun getLocalMarkdownProcessor ()Landroidx/compose/runtime/ProvidableCompositionLocal;
public static final fun getLocalMarkdownStyling ()Landroidx/compose/runtime/ProvidableCompositionLocal;
public static final fun getMarkdownBlockRenderer (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer;
public static final fun getMarkdownMode (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/markdown/MarkdownMode;
public static final fun getMarkdownProcessor (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;
public static final fun getMarkdownStyling (Lorg/jetbrains/jewel/foundation/theme/JewelTheme$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;
}
@@ -299,8 +319,8 @@ public final class org/jetbrains/jewel/markdown/processing/MarkdownParserFactory
public final class org/jetbrains/jewel/markdown/processing/MarkdownProcessor {
public static final field $stable I
public fun <init> ()V
public fun <init> (Ljava/util/List;ZLorg/commonmark/parser/Parser;)V
public synthetic fun <init> (Ljava/util/List;ZLorg/commonmark/parser/Parser;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/util/List;Lorg/jetbrains/jewel/markdown/MarkdownMode;Lorg/commonmark/parser/Parser;)V
public synthetic fun <init> (Ljava/util/List;Lorg/jetbrains/jewel/markdown/MarkdownMode;Lorg/commonmark/parser/Parser;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun processChildren (Lorg/commonmark/node/Node;)Ljava/util/List;
public final fun processMarkdownDocument (Ljava/lang/String;)Ljava/util/List;
}
@@ -319,6 +339,7 @@ public class org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer
public static final field $stable I
public fun <init> (Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Ljava/util/List;Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;)V
public synthetic fun <init> (Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Ljava/util/List;Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
protected final fun MaybeScrollingContainer (ZLandroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public fun render (Ljava/util/List;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$BlockQuote;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$BlockQuote;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CodeBlock$FencedCodeBlock;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Code$Fenced;Landroidx/compose/runtime/Composer;I)V
@@ -333,6 +354,7 @@ public class org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer
public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$ListItem;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$Paragraph;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Paragraph;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
public fun render-EWr_ITI (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CodeBlock$FencedCodeBlock;Ljava/lang/String;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Code$Fenced;Landroidx/compose/runtime/Composer;I)V
public fun renderThematicBreak (Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$ThematicBreak;Landroidx/compose/runtime/Composer;I)V
}
@@ -751,3 +773,33 @@ public abstract interface class org/jetbrains/jewel/markdown/rendering/WithUnder
public abstract fun getUnderlineWidth-D9Ej5fM ()F
}
public final class org/jetbrains/jewel/markdown/scrolling/AutoScrollingUtilKt {
public static final fun AutoScrollableBlock (Lorg/jetbrains/jewel/markdown/MarkdownBlock;Lorg/jetbrains/jewel/markdown/scrolling/ScrollingSynchronizer;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
}
public class org/jetbrains/jewel/markdown/scrolling/ScrollSyncMarkdownBlockRenderer : org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer {
public static final field $stable I
public fun <init> (Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Ljava/util/List;Lorg/jetbrains/jewel/markdown/rendering/InlineMarkdownRenderer;)V
public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CodeBlock$IndentedCodeBlock;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Code$Indented;Landroidx/compose/runtime/Composer;I)V
public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$Heading;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Heading$HN;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
public fun render (Lorg/jetbrains/jewel/markdown/MarkdownBlock$Paragraph;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Paragraph;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V
public fun render-EWr_ITI (Lorg/jetbrains/jewel/markdown/MarkdownBlock$CodeBlock$FencedCodeBlock;Ljava/lang/String;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling$Code$Fenced;Landroidx/compose/runtime/Composer;I)V
}
public abstract class org/jetbrains/jewel/markdown/scrolling/ScrollingSynchronizer {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/markdown/scrolling/ScrollingSynchronizer$Companion;
public fun <init> ()V
public abstract fun acceptBlockSpans (Lorg/jetbrains/jewel/markdown/MarkdownBlock;Lkotlin/ranges/IntRange;)V
public abstract fun acceptGlobalPosition (Lorg/jetbrains/jewel/markdown/MarkdownBlock;Landroidx/compose/ui/layout/LayoutCoordinates;)V
public abstract fun acceptTextLayout (Lorg/jetbrains/jewel/markdown/MarkdownBlock;Landroidx/compose/ui/text/TextLayoutResult;)V
protected abstract fun afterProcessing ()V
protected abstract fun beforeProcessing ()V
public final fun process (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
public abstract fun scrollToLine (ILkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class org/jetbrains/jewel/markdown/scrolling/ScrollingSynchronizer$Companion {
public final fun create (Landroidx/compose/foundation/gestures/ScrollableState;)Lorg/jetbrains/jewel/markdown/scrolling/ScrollingSynchronizer;
}

View File

@@ -12,6 +12,7 @@ dependencies {
testImplementation(compose.desktop.uiTestJUnit4)
testImplementation(projects.ui)
testImplementation(compose.desktop.currentOs)
}
publicApiValidation {

View File

@@ -0,0 +1,36 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the
// Apache 2.0 license.
package org.jetbrains.jewel.markdown
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownMode
import org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer
/**
* Indicates possible scenarios of how markdown files are presented:
* - [Standalone] mode is the default scenario;
* - [EditorPreview] mode is intended for cases when the raw file can be edited, and changes are expected to affect
* rendered contents immediately.
*/
@ExperimentalJewelApi
public sealed interface MarkdownMode {
/** Default mode when only rendered contents of a file is shown to a user. */
public object Standalone : MarkdownMode
/**
* Mode that is intended for cases when the raw file can be edited, and changes are expected to affect rendered
* contents immediately.
*
* @param scrollingSynchronizer [ScrollingSynchronizer] that enables auto-scrolling in the preview to match the
* scrolling position in the editor and therefore show the same blocks that are currently visible in the editor.
*/
public class EditorPreview(public val scrollingSynchronizer: ScrollingSynchronizer?) : MarkdownMode
}
@ExperimentalJewelApi
@Composable
public fun WithMarkdownMode(mode: MarkdownMode, content: @Composable () -> Unit) {
CompositionLocalProvider(LocalMarkdownMode provides mode) { content() }
}

View File

@@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.staticCompositionLocalOf
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.markdown.MarkdownMode
import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer
import org.jetbrains.jewel.markdown.rendering.MarkdownStyling
@@ -28,3 +29,10 @@ public val LocalMarkdownBlockRenderer: ProvidableCompositionLocal<MarkdownBlockR
public val JewelTheme.Companion.markdownBlockRenderer: MarkdownBlockRenderer
@Composable get() = LocalMarkdownBlockRenderer.current
public val LocalMarkdownMode: ProvidableCompositionLocal<MarkdownMode> = staticCompositionLocalOf {
MarkdownMode.Standalone
}
public val JewelTheme.Companion.markdownMode: MarkdownMode
@Composable get() = LocalMarkdownMode.current

View File

@@ -14,6 +14,7 @@ import org.commonmark.node.ListItem
import org.commonmark.node.Node
import org.commonmark.node.OrderedList
import org.commonmark.node.Paragraph
import org.commonmark.node.SourceSpan
import org.commonmark.node.ThematicBreak
import org.commonmark.parser.Parser
import org.intellij.lang.annotations.Language
@@ -26,40 +27,47 @@ import org.jetbrains.jewel.markdown.InlineMarkdown
import org.jetbrains.jewel.markdown.MarkdownBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.CodeBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.ListBlock
import org.jetbrains.jewel.markdown.MarkdownMode
import org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension
import org.jetbrains.jewel.markdown.rendering.DefaultInlineMarkdownRenderer
import org.jetbrains.jewel.markdown.scrolling.ScrollingSynchronizer
/**
* Reads raw Markdown strings and processes them into a list of [MarkdownBlock].
*
* @param extensions Extensions to use when processing the Markdown (e.g., to support parsing custom block-level
* Markdown).
* @param editorMode Indicates whether the processor should be optimized for an editor/preview scenario, where it
* assumes small incremental changes as performed by a user typing. This means it will only update the changed blocks
* by keeping state in memory.
* @param markdownMode Indicates a scenario of how the file is going to be presented. Default is
* [MarkdownMode.Standalone]; set this to [MarkdownMode.EditorPreview] if this parser will be used in an editor
* scenario, where the raw Markdown is only ever going to change slightly but frequently (e.g., as the user types).
* This means it will only update the changed blocks by keeping state in memory.
*
* Default is `false`; set this to `true` if this parser will be used in an editor scenario, where the raw Markdown is
* only ever going to change slightly but frequently (e.g., as the user types).
* You can also pass a [ScrollingSynchronizer] to [MarkdownMode.EditorPreview] to enable auto-scrolling in the preview
* according to the position in the editor.
*
* **Attention:** do **not** reuse or share an instance of [MarkdownProcessor] that is in [editorMode]. Processing
* entirely different Markdown strings will defeat the purpose of the optimization. When in editor mode, the instance
* of [MarkdownProcessor] is **not** thread-safe!
* **Attention:** do **not** reuse or share an instance of [MarkdownProcessor] if [markdownMode] is
* [MarkdownMode.EditorPreview]. Processing entirely different Markdown strings will defeat the purpose of the
* optimization. When in editor mode, the instance of [MarkdownProcessor] is **not** thread-safe!
*
* @param commonMarkParser The CommonMark [Parser] used to parse the Markdown. By default it's a vanilla instance
* provided by the [MarkdownParserFactory], but you can provide your own if you need to customize the parser — e.g.,
* to ignore certain tags. If [optimizeEdits] is `true`, make sure you set
* to ignore certain tags. If [markdownMode] is `MarkdownMode.WithEditor`, make sure you set
* `includeSourceSpans(IncludeSourceSpans.BLOCKS)` on the parser.
*/
@ExperimentalJewelApi
public class MarkdownProcessor(
private val extensions: List<MarkdownProcessorExtension> = emptyList(),
private val editorMode: Boolean = false,
private val commonMarkParser: Parser = MarkdownParserFactory.create(editorMode, extensions),
private val markdownMode: MarkdownMode = MarkdownMode.Standalone,
private val commonMarkParser: Parser =
MarkdownParserFactory.create(markdownMode is MarkdownMode.EditorPreview, extensions),
) {
private var currentState = State(emptyList(), emptyList(), emptyList())
@TestOnly internal fun getCurrentIndexesInTest() = currentState.indexes
private val scrollingSynchronizer: ScrollingSynchronizer? =
(markdownMode as? MarkdownMode.EditorPreview)?.scrollingSynchronizer
/**
* Parses a Markdown document, translating from CommonMark 0.31.2 to a list of [MarkdownBlock]. Inline Markdown in
* leaf nodes is contained in [InlineMarkdown], which can be rendered to an
@@ -69,8 +77,15 @@ public class MarkdownProcessor(
* @see DefaultInlineMarkdownRenderer
*/
public fun processMarkdownDocument(@Language("Markdown") rawMarkdown: String): List<MarkdownBlock> {
if (scrollingSynchronizer == null) {
return doProcess(rawMarkdown)
}
return scrollingSynchronizer.process { doProcess(rawMarkdown) }
}
private fun doProcess(rawMarkdown: String): List<MarkdownBlock> {
val blocks =
if (editorMode) {
if (markdownMode is MarkdownMode.EditorPreview) {
processWithQuickEdits(rawMarkdown)
} else {
parseRawMarkdown(rawMarkdown)
@@ -154,6 +169,60 @@ public class MarkdownProcessor(
previousBlocks.subList(lastBlock, previousBlocks.size)
val newIndexes = previousIndexes.subList(0, firstBlock) + updatedIndexes + suffixIndexes
// Processor only re-parses the changed part of the document, which has two outcomes:
// 1. sourceSpans in updatedBlocks start from line index 0, not from the actual line
// the update part starts in the document;
// 2. sourceSpans in blocks after the changed part remain unchanged
// (therefore irrelevant too).
//
// Addressing the second outcome is easy, as all the lines there were just shifted by
// nLinesDelta.
for (i in lastBlock until newBlocks.size) {
newBlocks[i].traverseAll { node ->
node.sourceSpans =
node.sourceSpans.map { span ->
SourceSpan.of(span.lineIndex + nLinesDelta, span.columnIndex, span.inputIndex, span.length)
}
}
}
// The first outcome is a bit trickier. Consider a fresh new block with the following
// structure:
//
// indexes spans
// Block A [10-20] (0-10)
// block A1 [ n/a ] (0-2)
// block A2 [ n/a ] (3-10)
// Block B [21-30] (11-20)
// block B1 [ n/a ] (11-16)
// block B2 [ n/a ] (17-20)
//
// There are two updated blocks with two children each.
// Note that at this point the indexes are updated, yet they only exist for the topmost
// blocks.
// So, to calculate actual spans for, for example, block B2 (B2s), we need to also take into
// account
// the first index of the block B (Bi) and the first span of the block B (Bs) and use the
// formula
// B2s = (B2s - Bs) + Bi
for ((block, indexes) in updatedBlocks.zip(updatedIndexes)) {
val firstSpanLineIndex = block.sourceSpans.firstOrNull()?.lineIndex ?: continue
val firstIndex = indexes.first
block.traverseAll { node ->
node.sourceSpans =
node.sourceSpans.map { span ->
SourceSpan.of(
span.lineIndex - firstSpanLineIndex + firstIndex,
span.columnIndex,
span.inputIndex,
span.length,
)
}
}
}
currentState = State(newLines, newBlocks, newIndexes)
return newBlocks
@@ -186,8 +255,17 @@ public class MarkdownProcessor(
}
else -> null
}.also { block ->
if (scrollingSynchronizer != null && this is Block && block != null) {
postProcess(scrollingSynchronizer, this, block)
}
}
private fun postProcess(scrollingSynchronizer: ScrollingSynchronizer, block: Block, mdBlock: MarkdownBlock) {
val spans = block.sourceSpans.takeIf { it.isNotEmpty() } ?: return
scrollingSynchronizer.acceptBlockSpans(mdBlock, spans.first().lineIndex..spans.last().lineIndex)
}
private fun Paragraph.toMarkdownParagraph(): MarkdownBlock.Paragraph =
MarkdownBlock.Paragraph(readInlineContent().toList())
@@ -257,6 +335,11 @@ public class MarkdownProcessor(
}
}
private fun Node.traverseAll(action: (Node) -> Unit) {
action(this)
forEachChild { child -> child.traverseAll(action) }
}
private fun HtmlBlock.toMarkdownHtmlBlockOrNull(): MarkdownBlock.HtmlBlock? {
if (literal.isBlank()) return null
return MarkdownBlock.HtmlBlock(literal.trimEnd('\n'))

View File

@@ -365,7 +365,7 @@ public open class DefaultMarkdownBlockRenderer(
)
}
Code(block.content, mimeType, styling)
render(block, mimeType, styling)
if (styling.infoPosition.verticalAlignment == Alignment.Bottom) {
FencedBlockInfo(
@@ -381,16 +381,12 @@ public open class DefaultMarkdownBlockRenderer(
}
@Composable
private fun Code(content: String, mimeType: MimeType, styling: MarkdownStyling.Code.Fenced) {
val annotatedCode by
public open fun render(block: FencedCodeBlock, mimeType: MimeType, styling: MarkdownStyling.Code.Fenced) {
val content = block.content
val highlightedCode by
LocalCodeHighlighter.current.highlight(content, mimeType).collectAsState(AnnotatedString(content))
CodeText(annotatedCode, styling)
}
@Composable
private fun CodeText(annotatedCode: AnnotatedString, styling: MarkdownStyling.Code.Fenced) {
Text(
text = annotatedCode,
text = highlightedCode,
style = styling.editorTextStyle,
modifier =
Modifier.focusProperties { canFocus = false }
@@ -444,7 +440,7 @@ public open class DefaultMarkdownBlockRenderer(
}
@Composable
private fun MaybeScrollingContainer(
protected fun MaybeScrollingContainer(
isScrollable: Boolean,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,

View File

@@ -0,0 +1,45 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the
// Apache 2.0 license.
package org.jetbrains.jewel.markdown.scrolling
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import kotlin.math.abs
import org.jetbrains.jewel.markdown.MarkdownBlock
/**
* Use this composable as a wrapper to an actual block composable to enable scrolling to the block in an editor+preview
* combined mode with scrolling synchronization.
*
* @see [ScrollSyncMarkdownBlockRenderer]
*/
@Composable
public fun AutoScrollableBlock(
block: MarkdownBlock,
synchronizer: ScrollingSynchronizer,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
var previousPosition by remember(block) { mutableStateOf(Offset.Zero) }
Box(
modifier =
modifier.onGloballyPositioned { coordinates ->
val newPosition = coordinates.positionInRoot()
if (abs(previousPosition.y - newPosition.y) > 1.0) {
previousPosition = newPosition
synchronizer.acceptGlobalPosition(block, coordinates)
}
}
) {
content()
}
}

View File

@@ -0,0 +1,131 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the
// Apache 2.0 license.
package org.jetbrains.jewel.markdown.scrolling
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.text.AnnotatedString
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.foundation.code.MimeType
import org.jetbrains.jewel.foundation.code.highlighting.LocalCodeHighlighter
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.foundation.theme.LocalContentColor
import org.jetbrains.jewel.markdown.MarkdownBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.CodeBlock.FencedCodeBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.CodeBlock.IndentedCodeBlock
import org.jetbrains.jewel.markdown.MarkdownMode
import org.jetbrains.jewel.markdown.extensions.MarkdownRendererExtension
import org.jetbrains.jewel.markdown.extensions.markdownMode
import org.jetbrains.jewel.markdown.rendering.DefaultMarkdownBlockRenderer
import org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer
import org.jetbrains.jewel.markdown.rendering.MarkdownStyling
import org.jetbrains.jewel.ui.component.Text
@Suppress("unused") // used in intellij
@ExperimentalJewelApi
public open class ScrollSyncMarkdownBlockRenderer(
rootStyling: MarkdownStyling,
renderingExtensions: List<MarkdownRendererExtension>,
inlineRenderer: InlineMarkdownRenderer,
) : DefaultMarkdownBlockRenderer(rootStyling, renderingExtensions, inlineRenderer) {
@Composable
override fun render(
block: MarkdownBlock.Paragraph,
styling: MarkdownStyling.Paragraph,
enabled: Boolean,
onUrlClick: (String) -> Unit,
onTextClick: () -> Unit,
) {
val synchronizer =
(JewelTheme.markdownMode as? MarkdownMode.EditorPreview)?.scrollingSynchronizer
?: run {
super.render(block, styling, enabled, onUrlClick, onTextClick)
return
}
AutoScrollableBlock(block, synchronizer) { super.render(block, styling, enabled, onUrlClick, onTextClick) }
}
@Composable
override fun render(
block: MarkdownBlock.Heading,
styling: MarkdownStyling.Heading.HN,
enabled: Boolean,
onUrlClick: (String) -> Unit,
onTextClick: () -> Unit,
) {
val synchronizer =
(JewelTheme.markdownMode as? MarkdownMode.EditorPreview)?.scrollingSynchronizer
?: run {
super.render(block, styling, enabled, onUrlClick, onTextClick)
return
}
AutoScrollableBlock(block, synchronizer) { super.render(block, styling, enabled, onUrlClick, onTextClick) }
}
@Composable
override fun render(block: FencedCodeBlock, mimeType: MimeType, styling: MarkdownStyling.Code.Fenced) {
val synchronizer =
(JewelTheme.markdownMode as? MarkdownMode.EditorPreview)?.scrollingSynchronizer
?: run {
super.render(block, mimeType, styling)
return
}
val content = block.content
val highlightedCode by
LocalCodeHighlighter.current.highlight(content, mimeType).collectAsState(AnnotatedString(content))
val actualBlock by rememberUpdatedState(block)
AutoScrollableBlock(actualBlock, synchronizer) {
Text(
text = highlightedCode,
style = styling.editorTextStyle,
modifier =
Modifier.focusProperties { canFocus = false }
.pointerHoverIcon(PointerIcon.Default, overrideDescendants = true),
onTextLayout = { textLayoutResult -> synchronizer.acceptTextLayout(actualBlock, textLayoutResult) },
)
}
}
@Composable
override fun render(block: IndentedCodeBlock, styling: MarkdownStyling.Code.Indented) {
val scrollingSynchronizer =
(JewelTheme.markdownMode as? MarkdownMode.EditorPreview)?.scrollingSynchronizer
?: run {
super.render(block, styling)
return
}
MaybeScrollingContainer(
isScrollable = styling.scrollsHorizontally,
Modifier.background(styling.background, styling.shape)
.border(styling.borderWidth, styling.borderColor, styling.shape)
.then(if (styling.fillWidth) Modifier.fillMaxWidth() else Modifier),
) {
AutoScrollableBlock(block, scrollingSynchronizer, Modifier.padding(styling.padding)) {
Text(
text = block.content,
style = styling.editorTextStyle,
color = styling.editorTextStyle.color.takeOrElse { LocalContentColor.current },
modifier =
Modifier.focusProperties { canFocus = false }
.pointerHoverIcon(PointerIcon.Default, overrideDescendants = true),
onTextLayout = { textLayoutResult ->
scrollingSynchronizer.acceptTextLayout(block, textLayoutResult)
},
)
}
}
}
}

View File

@@ -0,0 +1,274 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the
// Apache 2.0 license.
package org.jetbrains.jewel.markdown.scrolling
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.text.TextLayoutResult
import java.util.TreeMap
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.foundation.util.myLogger
import org.jetbrains.jewel.markdown.MarkdownBlock
import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
/**
* To support synchronized scrolling between source and preview, we need to establish a mapping between source lines and
* coordinates of their presentation.
*
* For simplicity, let's suppose that the source code is immutable. [MarkdownProcessor] parses it and yields a list of
* [MarkdownBlock]s. Unfortunately, it doesn't contain any information about the source lines, as the need to keep them
* and reserve more heap is not strong enough (the hypothesis is that most users just need to read the .md file and not
* to edit it).
*
* However, [MarkdownProcessor] uses commonmark inside and takes the blocks this library returns to build
* [MarkdownBlock]s, and in the editor mode, commonmark blocks still hold the information about source lines.
* [acceptBlockSpans] can be implemented the way that remembers mappings between [MarkdownBlock]s and source lines these
* blocks span over.
*
* Next, Compose provides the callback [onGloballyPositioned] with precalculated global layout. [acceptGlobalPosition]
* can be implemented to remember mappings between [MarkdownBlock]s and global coordinates these blocks are rendered on.
*
* These two mappings are enough to make the synchronizer work. When a source code is scrolled to a line, an
* implementation can find a block containing the line (or the next one if there are no blocks on the line), then find
* this block's global layout and, finally, tell Compose to scroll to the topmost coordinate of the layout. This way, a
* user can observe the whole block in the preview, even if only a part of it is visible in the source view.
*
* For some blocks, however, it makes sense to scroll within their content. Code blocks make for a perfect example of
* it. They can contain a lot of lines, and at the same time, they're not soft-wrapped in a preview, every source line
* is mapped 1:1 to the preview, so scrolling inside a code block would be preferable (and natural) to support.
* [acceptTextLayout] serves the purpose of calculation every line's position within the composable. This information
* may, in turn, be used together with global positioning of the composable to compute the absolute position of a
* certain line in the preview.
*
* # Editing
*
* [MarkdownProcessor] always yields all the blocks that are present in the source, even in optimized mode, so
* [acceptBlockSpans] is not really affected by editing. [acceptGlobalPosition] is trickier, as it is not triggered on
* blocks preceding the change. [acceptTextLayout] is even more intricate, as it may or may not be triggered on blocks
* following the change. It implies that mappings should be adjusted accordingly. [beforeProcessing] and
* [afterProcessing] can help with that, as they're invoked before and after every re-parse, i.e. every change in the
* file. See [PerLine] as one of the possible implementations for [ScrollState].
*
* # Keep in mind
* - [acceptBlockSpans] accepts blocks in the **depth-first order**.
* - Between [beforeProcessing] and [afterProcessing] every single block is processed, [acceptBlockSpans] is triggered
* for every one of them.
* - [acceptGlobalPosition] is **always** triggered on the changed block and the blocks that follow the change.
* - [acceptTextLayout] is **always** triggered on the changed block, but **not always** on those located below the
* changed block. It's **not triggered** on blocks located above the change.
* - [acceptTextLayout] is triggered **before** [acceptGlobalPosition] for the same block.
*
* @see [MarkdownProcessor]
* @see [AutoScrollableBlock]
* @see [PerLine]
*/
@ExperimentalJewelApi
public abstract class ScrollingSynchronizer {
/** Scroll the preview to the position that match the given [sourceLine] the best. */
public abstract suspend fun scrollToLine(sourceLine: Int)
/**
* Called when [MarkdownProcessor] processes the raw markdown text. The processing itself is passed as an [action].
*/
public fun <T> process(action: () -> T): T {
beforeProcessing()
return try {
action()
} finally {
afterProcessing()
}
}
/** Called before [MarkdownProcessor] starts processing the raw markdown text. */
protected abstract fun beforeProcessing()
/** Called after [MarkdownProcessor] starts processing the raw markdown text. */
protected abstract fun afterProcessing()
/**
* Accept mapping between the markdown [block] and the [sourceRange] of lines containing this block. Called on every
* block after it was (re)parsed.
*/
public abstract fun acceptBlockSpans(block: MarkdownBlock, sourceRange: IntRange)
/**
* Accept mapping between the markdown [block] and the global [coordinates] of lines containing this block. Called
* on all blocks that require (re)positioning: on first composition, on a changed block, on unchanged blocks that
* are positioned below the changed block.
*/
public abstract fun acceptGlobalPosition(block: MarkdownBlock, coordinates: LayoutCoordinates)
/**
* Accept mapping between the markdown [block] and the [textLayout] of the text this block comprises. Called on all
* blocks that require adjusting text layout: on first composition, on a block with the changed text, and may be
* called on unchanged blocks that are positioned below the changed block.
*/
public abstract fun acceptTextLayout(block: MarkdownBlock, textLayout: TextLayoutResult)
public companion object {
public fun create(scrollState: ScrollableState): ScrollingSynchronizer? =
when (scrollState) {
is ScrollState -> PerLine(scrollState)
is LazyListState -> {
myLogger().warn("Synchronization for LazyListState is not supported yet")
null
}
else -> null
}
}
private class PerLine(private val scrollState: ScrollState) : ScrollingSynchronizer() {
private val lines2Blocks = TreeMap<Int, MarkdownBlock>()
private var blocks2LineRanges = mutableMapOf<MarkdownBlock, IntRange>()
private val blocks2Top = mutableMapOf<MarkdownBlock, Int>()
private val previousPositions = mutableMapOf<MarkdownBlock, Int>()
// Only used to clean up obsolete keys in the maps above;
// otherwise stale MarkdownBlocks will keep piling up on each typed key
private val actualBlocks = mutableSetOf<MarkdownBlock>()
// It'd be a bit more performant if there were a map mapping lines to offsets,
// and that was the initial approach,
// but this structure would be hard to maintain because of optimizations in Compose.
// Namely, text offsets may not be recalculated even if the block was repositioned.
// For example, if contents of one item in a Column change, it only causes relayout
// of the changed item, and not the items that follow, even though they are to be
// repositioned globally.
// Thus, even if lines that a block occupies change,
// relative offsets within the block can remain the same.
// But here, given there's guaranteed 1:1 source to preview lines mapping,
// the rules holds that, if a block hasn't changed, text offsets remain unchanged too,
// so this map always keeps relevant information.
private val blocks2TextOffsets = mutableMapOf<MarkdownBlock, List<Int>>()
override suspend fun scrollToLine(sourceLine: Int) {
val block = findBestBlockForLine(sourceLine) ?: return
val y = blocks2Top[block] ?: return
if (y < 0) return
val lineRange = blocks2LineRanges[block] ?: return
val textOffsets = blocks2TextOffsets[block]
// The line may be empty and represent no block,
// in this case scroll to the first line of the first block positioned after the line
val lineIndexInBlock = maxOf(0, sourceLine - lineRange.start)
val lineOffset = textOffsets?.get(lineIndexInBlock) ?: 0
scrollState.animateScrollTo(y + lineOffset)
}
private fun findBestBlockForLine(line: Int): MarkdownBlock? {
// The best block is the one **below** the line if there is no block that covers the
// line.
// Otherwise, when scrolling down the source, on empty lines preview will scroll in the
// opposite direction
val sm = lines2Blocks.subMap(line, Int.MAX_VALUE)
if (sm.isEmpty()) return null
// TODO use firstEntry() after switching to JDK 21
return sm.getValue(sm.firstKey())
}
override fun beforeProcessing() {
// acceptBlockSpans works on ALL the nodes, including those unchanged,
// so it will be fully rebuilt during processing anyway
lines2Blocks.clear()
blocks2LineRanges.clear()
}
override fun afterProcessing() {
blocks2LineRanges.keys.retainAll(actualBlocks)
blocks2Top.keys.retainAll(actualBlocks)
blocks2TextOffsets.keys.retainAll(actualBlocks)
previousPositions.keys.retainAll(actualBlocks)
actualBlocks.clear()
}
override fun acceptBlockSpans(block: MarkdownBlock, sourceRange: IntRange) {
for (line in sourceRange) {
// DFS -- keep the innermost block for the given line
lines2Blocks.putIfAbsent(line, block)
}
blocks2LineRanges[block] = sourceRange
actualBlocks += block
}
override fun acceptGlobalPosition(block: MarkdownBlock, coordinates: LayoutCoordinates) {
// coordinates are relative to the current viewport
// (which also means onPositionedGlobally is triggered when scrolling);
// to get the real absolute coordinates we need to consider scroll state
val y = coordinates.positionInRoot().y.toInt() + scrollState.value
// let's not recalculate internal structures on the preview scrolling -- more safety
val oldY = previousPositions[block]
if (oldY == null || y != oldY) {
blocks2Top[block] = y
previousPositions[block] = y
}
}
override fun acceptTextLayout(block: MarkdownBlock, textLayout: TextLayoutResult) {
if (block !is MarkdownBlock.CodeBlock) return
val sourceLines = blocks2LineRanges[block] ?: return
var y = 0
val list = mutableListOf<Int>()
if (block is MarkdownBlock.CodeBlock.FencedCodeBlock) {
// All source lines in the fenced code block,
// beside the first and the last ones, are mapped 1:1 onto preview
// code block:
//
// | source: | preview:
// __________________________________|_________________
// (first line) | ```language | <no mapping>
// | <line 1> | <line 1>
// | <line 2> | <line 2>
// | <line 3> | <line 3>
// | <line 4> | <line 4>
// | <line 5> | <line 5>
// (last line) | ``` | <no mapping>
//
// Some of the lines might be empty, and thus there are no spans for them.
// However, every empty line follows the 1:1 mapping rule,
// which means all of the lines in the range [first line + 1; last line - 1]
// have their counterparts in the preview, regardless of the content.
val openingLine = sourceLines.first()
val firstSourceLine = openingLine + 1
val closingLine = sourceLines.last()
// map the line with opening triple backticks
// to the topmost point of the block in the preview
list += y
for (i in firstSourceLine..<closingLine) {
list += y
val lineHeight =
textLayout.getLineBottom(i - firstSourceLine) - textLayout.getLineTop(i - firstSourceLine)
y += lineHeight.toInt()
}
// map the line with closing triple backticks
// to the bottommost point of the block in the preview
list += y
} else if (block is MarkdownBlock.CodeBlock.IndentedCodeBlock) {
// Indented code blocks don't have the empty last line,
// and the empty opening line is not counted:
//
// | source: | preview:
// __________________________________|_________________
// (first line) | <line 1> | <line 1>
// | <line 2> | <line 2>
// | <line 3> | <line 3>
// | <line 4> | <line 4>
// (last line) | <line 5> | <line 5>
for (i in sourceLines) {
list += y
val lineHeight =
textLayout.getLineBottom(i - sourceLines.first) - textLayout.getLineTop(i - sourceLines.first)
y += lineHeight.toInt()
}
}
blocks2TextOffsets[block] = list
}
}
}

View File

@@ -7,6 +7,7 @@ import org.commonmark.parser.IncludeSourceSpans
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.intellij.lang.annotations.Language
import org.jetbrains.jewel.markdown.MarkdownMode
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
@@ -25,14 +26,14 @@ private val rawMarkdown =
## Header 4
Paragraph 5
continue paragraph 5
```
line 6-1
line 6-2
```
Paragraph 7
Paragraph 8
continue p8
"""
@@ -46,7 +47,7 @@ class MarkdownProcessorOptimizeEditsTest {
@Test
fun `first blocks stay the same`() {
val processor = MarkdownProcessor(editorMode = true)
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(null))
val firstRun = processor.processWithQuickEdits(rawMarkdown)
val secondRun =
processor.processWithQuickEdits(
@@ -55,7 +56,7 @@ class MarkdownProcessorOptimizeEditsTest {
continue p0
# Header 1
Paragraph 2
* list item 3-1
* list item 3-2
"""
@@ -74,7 +75,7 @@ class MarkdownProcessorOptimizeEditsTest {
<li>list item 3-1</li>
<li>list item 3-2</li>
</ul>
"""
.trimIndent(),
secondRun,
@@ -83,7 +84,7 @@ class MarkdownProcessorOptimizeEditsTest {
@Test
fun `first block edited`() {
val processor = MarkdownProcessor(editorMode = true)
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(null))
val firstRun = processor.processWithQuickEdits(rawMarkdown)
val secondRun =
processor.processWithQuickEdits(
@@ -97,14 +98,14 @@ class MarkdownProcessorOptimizeEditsTest {
## Header 4
Paragraph 5
continue paragraph 5
```
line 6-1
line 6-2
```
Paragraph 7
Paragraph 8
continue p8
"""
@@ -129,7 +130,7 @@ class MarkdownProcessorOptimizeEditsTest {
<p>Paragraph 7</p>
<p>Paragraph 8
continue p8</p>
"""
.trimIndent(),
secondRun,
@@ -141,7 +142,7 @@ class MarkdownProcessorOptimizeEditsTest {
@Test
fun `last block edited`() {
val processor = MarkdownProcessor(editorMode = true)
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(null))
val firstRun = processor.processWithQuickEdits(rawMarkdown)
val secondRun =
processor.processWithQuickEdits(
@@ -156,14 +157,14 @@ class MarkdownProcessorOptimizeEditsTest {
## Header 4
Paragraph 5
continue paragraph 5
```
line 6-1
line 6-2
```
Paragraph 7
Paragraph *CHANGE*
continue p8
"""
@@ -189,7 +190,7 @@ class MarkdownProcessorOptimizeEditsTest {
<p>Paragraph 7</p>
<p>Paragraph <em>CHANGE</em>
continue p8</p>
"""
.trimIndent(),
secondRun,
@@ -202,7 +203,7 @@ class MarkdownProcessorOptimizeEditsTest {
@Test
fun `middle block edited`() {
val processor = MarkdownProcessor(editorMode = true)
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(null))
val firstRun = processor.processWithQuickEdits(rawMarkdown)
val secondRun =
processor.processWithQuickEdits(
@@ -217,14 +218,14 @@ class MarkdownProcessorOptimizeEditsTest {
## Header 4
Paragraph 5
continue paragraph 5
```
line 6-1
line 6-2
```
Paragraph 7
Paragraph 8
continue p8
"""
@@ -250,7 +251,7 @@ class MarkdownProcessorOptimizeEditsTest {
<p>Paragraph 7</p>
<p>Paragraph 8
continue p8</p>
"""
.trimIndent(),
secondRun,
@@ -265,7 +266,7 @@ class MarkdownProcessorOptimizeEditsTest {
@Test
fun `blocks merged`() {
val processor = MarkdownProcessor(editorMode = true)
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(null))
val firstRun = processor.processWithQuickEdits(rawMarkdown)
val secondRun =
processor.processWithQuickEdits(
@@ -280,8 +281,8 @@ class MarkdownProcessorOptimizeEditsTest {
## Header 4
Paragraph 5
continue paragraph 5
```
line 6-1
line 6-2
@@ -312,7 +313,7 @@ class MarkdownProcessorOptimizeEditsTest {
<p>Paragraph 7
Paragraph 8
continue p8</p>
"""
.trimIndent(),
secondRun,
@@ -324,7 +325,7 @@ class MarkdownProcessorOptimizeEditsTest {
@Test
fun `blocks split`() {
val processor = MarkdownProcessor(editorMode = true)
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(null))
val firstRun = processor.processWithQuickEdits(rawMarkdown)
val secondRun =
processor.processWithQuickEdits(
@@ -338,14 +339,14 @@ class MarkdownProcessorOptimizeEditsTest {
* list item 3-3
## Header 4
Paragraph 5
continue paragraph 5
```
line 6-1
line 6-2
```
Paragraph 7
Paragraph 8
continue p8
"""
@@ -371,7 +372,7 @@ class MarkdownProcessorOptimizeEditsTest {
<p>Paragraph 7</p>
<p>Paragraph 8
continue p8</p>
"""
.trimIndent(),
secondRun,
@@ -384,7 +385,7 @@ class MarkdownProcessorOptimizeEditsTest {
@Test
fun `blocks deleted`() {
val processor = MarkdownProcessor(editorMode = true)
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(null))
val firstRun = processor.processWithQuickEdits(rawMarkdown)
val secondRun =
processor.processWithQuickEdits(
@@ -401,7 +402,7 @@ class MarkdownProcessorOptimizeEditsTest {
line 6-2
```
Paragraph 7
Paragraph 8
continue p8
"""
@@ -424,7 +425,7 @@ class MarkdownProcessorOptimizeEditsTest {
<p>Paragraph 7</p>
<p>Paragraph 8
continue p8</p>
"""
.trimIndent(),
secondRun,
@@ -438,7 +439,7 @@ class MarkdownProcessorOptimizeEditsTest {
@Test
fun `blocks added`() {
val processor = MarkdownProcessor(editorMode = true)
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(null))
val firstRun = processor.processWithQuickEdits(rawMarkdown)
val secondDocument =
"""
@@ -450,20 +451,20 @@ class MarkdownProcessorOptimizeEditsTest {
* list item 3-2
* list item 3-3
## Header 4
*CHANGE*
Paragraph 5
continue paragraph 5
```
line 6-1
line 6-2
```
Paragraph 7
Paragraph 8
continue p8
"""
@@ -490,7 +491,7 @@ class MarkdownProcessorOptimizeEditsTest {
<p>Paragraph 7</p>
<p>Paragraph 8
continue p8</p>
"""
.trimIndent(),
secondRun,
@@ -505,7 +506,7 @@ class MarkdownProcessorOptimizeEditsTest {
@Test
fun `no changes`() {
val processor = MarkdownProcessor(editorMode = true)
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(null))
val firstRun = processor.processWithQuickEdits(rawMarkdown)
val secondRun = processor.processWithQuickEdits(rawMarkdown)
assertHtmlEquals(
@@ -528,7 +529,7 @@ class MarkdownProcessorOptimizeEditsTest {
<p>Paragraph 7</p>
<p>Paragraph 8
continue p8</p>
"""
.trimIndent(),
secondRun,
@@ -538,7 +539,7 @@ class MarkdownProcessorOptimizeEditsTest {
@Test
fun `empty line added`() {
val processor = MarkdownProcessor(editorMode = true)
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(null))
val firstRun = processor.processWithQuickEdits(rawMarkdown)
val secondRun = processor.processWithQuickEdits("\n" + rawMarkdown)
assertHtmlEquals(
@@ -561,7 +562,7 @@ class MarkdownProcessorOptimizeEditsTest {
<p>Paragraph 7</p>
<p>Paragraph 8
continue p8</p>
"""
.trimIndent(),
secondRun,
@@ -573,7 +574,7 @@ class MarkdownProcessorOptimizeEditsTest {
/** Regression https://github.com/JetBrains/jewel/issues/344 */
@Test
fun `content if empty`() {
val processor = MarkdownProcessor(editorMode = true)
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(null))
processor.processWithQuickEdits(rawMarkdown)
val secondRun = processor.processWithQuickEdits("")
assertHtmlEquals(
@@ -587,16 +588,16 @@ class MarkdownProcessorOptimizeEditsTest {
@Test
fun `chained changes`() {
val processor = MarkdownProcessor(editorMode = true)
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(null))
processor.processWithQuickEdits(
"""
# Header 0
# Header 1
# Header 2
# Header 3
# Header 4
# Header 5
@@ -613,10 +614,10 @@ class MarkdownProcessorOptimizeEditsTest {
# Header 0
# Header 1
some paragraph
# Header 2
# Header 3
# Header 7
@@ -631,8 +632,8 @@ class MarkdownProcessorOptimizeEditsTest {
"""
# Header 0
# Header 1
some paragraph
# Header 2
# Header 7
@@ -646,13 +647,13 @@ class MarkdownProcessorOptimizeEditsTest {
"""
# Header 0
# Header 1
some paragraph
# Header 2
# Header 7
- list item 1
- list item 2
# Header 8
@@ -683,7 +684,7 @@ class MarkdownProcessorOptimizeEditsTest {
</ul>
<h1>Header 8</h1>
<h1>Header 9</h1>
"""
.trimIndent(),
fifthRun,

View File

@@ -0,0 +1,895 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the
// Apache 2.0 license.
package org.jetbrains.jewel.markdown.scrolling
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.suspendCancellableCoroutine
import org.jetbrains.jewel.foundation.BorderColors
import org.jetbrains.jewel.foundation.GlobalColors
import org.jetbrains.jewel.foundation.GlobalMetrics
import org.jetbrains.jewel.foundation.OutlineColors
import org.jetbrains.jewel.foundation.TextColors
import org.jetbrains.jewel.foundation.code.highlighting.LocalCodeHighlighter
import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.foundation.theme.ThemeColorPalette
import org.jetbrains.jewel.foundation.theme.ThemeDefinition
import org.jetbrains.jewel.foundation.theme.ThemeIconData
import org.jetbrains.jewel.markdown.MarkdownBlock
import org.jetbrains.jewel.markdown.MarkdownMode
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownBlockRenderer
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownMode
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownProcessor
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownStyling
import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
import org.jetbrains.jewel.markdown.rendering.DefaultInlineMarkdownRenderer
import org.jetbrains.jewel.markdown.rendering.InlinesStyling
import org.jetbrains.jewel.markdown.rendering.MarkdownStyling
import org.jetbrains.jewel.ui.component.styling.DividerMetrics
import org.jetbrains.jewel.ui.component.styling.DividerStyle
import org.jetbrains.jewel.ui.component.styling.LocalDividerStyle
import org.jetbrains.jewel.ui.component.styling.LocalScrollbarStyle
import org.jetbrains.jewel.ui.component.styling.ScrollbarColors
import org.jetbrains.jewel.ui.component.styling.ScrollbarMetrics
import org.jetbrains.jewel.ui.component.styling.ScrollbarStyle
import org.jetbrains.jewel.ui.component.styling.ScrollbarVisibility
import org.jetbrains.jewel.ui.component.styling.TrackClickBehavior
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
@Suppress("LargeClass")
class ScrollingSynchronizerTest {
@OptIn(ExperimentalTestApi::class)
@Test
fun headings() {
val markdown =
"""
# Heading 1
## Heading 2
### Heading 3
"""
.trimIndent()
doTest(markdown) { scrollState, synchronizer ->
synchronizer.scrollToLine(0)
assertEquals(0, scrollState.value)
synchronizer.scrollToLine(1)
val h2Top = scrollState.value
assertTrue(h2Top > 0)
synchronizer.scrollToLine(2)
val h3Top = scrollState.value
assertTrue(h3Top > h2Top)
synchronizer.scrollToLine(1)
assertEquals(h2Top, scrollState.value)
synchronizer.scrollToLine(0)
synchronizer.scrollToLine(2)
assertEquals(h3Top, scrollState.value)
}
}
@OptIn(ExperimentalTestApi::class)
@Test
fun paragraphs() {
val markdown =
"""
p1
p2
p3
"""
.trimIndent()
doTest(markdown) { scrollState, synchronizer ->
synchronizer.scrollToLine(1)
val p2Top = scrollState.value
assertTrue(p2Top > 0)
synchronizer.scrollToLine(2)
assertEquals(p2Top, scrollState.value)
synchronizer.scrollToLine(3)
val p3Top = scrollState.value
assertTrue(p3Top > p2Top)
synchronizer.scrollToLine(4)
assertEquals(p3Top, scrollState.value)
synchronizer.scrollToLine(1)
assertEquals(p2Top, scrollState.value)
}
}
@OptIn(ExperimentalTestApi::class)
@Test
fun `empty spaces`() {
val markdown =
"""
# Heading 1
# Heading 2
## Heading 3
"""
.trimIndent()
doTest(markdown) { scrollState, synchronizer ->
synchronizer.scrollToLine(1)
val h2Top = scrollState.value
assertTrue(h2Top > 0)
synchronizer.scrollToLine(2)
assertEquals(h2Top, scrollState.value)
synchronizer.scrollToLine(3)
assertEquals(h2Top, scrollState.value)
synchronizer.scrollToLine(4)
val h3Top = scrollState.value
assertTrue(h3Top > h2Top)
synchronizer.scrollToLine(5)
assertEquals(h3Top, scrollState.value)
synchronizer.scrollToLine(6)
assertEquals(h3Top, scrollState.value)
synchronizer.scrollToLine(7)
assertEquals(h3Top, scrollState.value)
synchronizer.scrollToLine(8)
assertEquals(h3Top, scrollState.value)
synchronizer.scrollToLine(1)
assertEquals(h2Top, scrollState.value)
}
}
@OptIn(ExperimentalTestApi::class)
@Test
fun `unordered list`() {
val markdown =
"""
Items:
- item 1
- subitem A
- item 2
- item 3
"""
.trimIndent()
doTest(markdown) { scrollState, synchronizer ->
synchronizer.scrollToLine(1)
val i1Top = scrollState.value
assertTrue(i1Top > 0)
synchronizer.scrollToLine(2)
val siATop = scrollState.value
assertTrue(siATop > i1Top)
synchronizer.scrollToLine(3)
val i2Top = scrollState.value
assertTrue(i2Top > siATop)
synchronizer.scrollToLine(4)
val i3Top = scrollState.value
assertTrue(i3Top > i2Top)
synchronizer.scrollToLine(2)
assertEquals(siATop, scrollState.value)
}
}
@OptIn(ExperimentalTestApi::class)
@Test
fun `ordered list`() {
val markdown =
"""
Items:
1. item 1
1. subitem A
2. item 2
3. item 3
"""
.trimIndent()
doTest(markdown) { scrollState, synchronizer ->
synchronizer.scrollToLine(1)
val i1Top = scrollState.value
assertTrue(i1Top > 0)
synchronizer.scrollToLine(2)
val siATop = scrollState.value
assertTrue(siATop > i1Top)
synchronizer.scrollToLine(3)
val i2Top = scrollState.value
assertTrue(i2Top > siATop)
synchronizer.scrollToLine(4)
val i3Top = scrollState.value
assertTrue(i3Top > i2Top)
synchronizer.scrollToLine(2)
assertEquals(siATop, scrollState.value)
}
}
@OptIn(ExperimentalTestApi::class)
@Test
fun `fenced code block`() {
val markdown =
"""
```kotlin
package my.awesome.pkg
fun main() {
println("Hello world")
}
```
"""
.trimIndent()
doTest(markdown) { scrollState, synchronizer ->
synchronizer.scrollToLine(1)
val packageTop = scrollState.value
assertTrue(packageTop > 0)
synchronizer.scrollToLine(2)
val emptyLineTop = scrollState.value
assertTrue(emptyLineTop > packageTop)
synchronizer.scrollToLine(3)
val mainTop = scrollState.value
assertTrue(mainTop > emptyLineTop)
synchronizer.scrollToLine(4)
val printlnTop = scrollState.value
assertTrue(printlnTop > mainTop)
synchronizer.scrollToLine(5)
val rBracketTop = scrollState.value
assertTrue(rBracketTop > printlnTop)
synchronizer.scrollToLine(2)
assertEquals(emptyLineTop, scrollState.value)
assertSameDistance(
distance = CODE_TEXT_SIZE + 2,
packageTop,
emptyLineTop,
mainTop,
printlnTop,
rBracketTop,
)
}
}
@OptIn(ExperimentalTestApi::class)
@Test
fun `indented code block`() {
val markdown =
"""
Here starts the indented code block.
package my.awesome.pkg
fun main() {
println("Hello world")
}
"""
.trimIndent()
doTest(markdown) { scrollState, synchronizer ->
synchronizer.scrollToLine(2)
val packageTop = scrollState.value
assertTrue(packageTop > 0)
synchronizer.scrollToLine(3)
val emptyLineTop = scrollState.value
assertTrue(emptyLineTop > packageTop)
synchronizer.scrollToLine(4)
val mainTop = scrollState.value
assertTrue(mainTop > emptyLineTop)
synchronizer.scrollToLine(5)
val printlnTop = scrollState.value
assertTrue(printlnTop > mainTop)
synchronizer.scrollToLine(6)
val rBracketTop = scrollState.value
assertTrue(rBracketTop > printlnTop)
synchronizer.scrollToLine(3)
assertEquals(emptyLineTop, scrollState.value)
assertSameDistance(
distance = CODE_TEXT_SIZE + 2,
packageTop,
emptyLineTop,
mainTop,
printlnTop,
rBracketTop,
)
}
}
@OptIn(ExperimentalTestApi::class)
@Test
fun `add a block`() {
val firstRun =
"""
```kotlin
package my.awesome.pkg
fun main() {
println("Hello world")
}
```
"""
.trimIndent()
val secondRun =
"""
**CHANGE**
```kotlin
package my.awesome.pkg
fun main() {
println("Hello world")
}
```
"""
.trimIndent()
doTest(firstRun, secondRun) { scrollState, synchronizer ->
synchronizer.scrollToLine(3)
val packageTop = scrollState.value
assertTrue(packageTop > 0)
synchronizer.scrollToLine(4)
val emptyLineTop = scrollState.value
assertTrue(emptyLineTop > packageTop)
synchronizer.scrollToLine(5)
val mainTop = scrollState.value
assertTrue(mainTop > emptyLineTop)
synchronizer.scrollToLine(6)
val printlnTop = scrollState.value
assertTrue(printlnTop > mainTop)
synchronizer.scrollToLine(7)
val rBracketTop = scrollState.value
assertTrue(rBracketTop > printlnTop)
synchronizer.scrollToLine(4)
assertEquals(emptyLineTop, scrollState.value)
assertSameDistance(
distance = CODE_TEXT_SIZE + 2,
packageTop,
emptyLineTop,
mainTop,
printlnTop,
rBracketTop,
)
}
}
@OptIn(ExperimentalTestApi::class)
@Test
fun `remove a block`() {
val firstRun =
"""
**CHANGE**
```kotlin
package my.awesome.pkg
fun main() {
println("Hello world")
}
```
"""
.trimIndent()
val secondRun =
"""
```kotlin
package my.awesome.pkg
fun main() {
println("Hello world")
}
```
"""
.trimIndent()
doTest(firstRun, secondRun) { scrollState, synchronizer ->
synchronizer.scrollToLine(1)
val packageTop = scrollState.value
assertTrue(packageTop > 0)
synchronizer.scrollToLine(2)
val emptyLineTop = scrollState.value
assertTrue(emptyLineTop > packageTop)
synchronizer.scrollToLine(3)
val mainTop = scrollState.value
assertTrue(mainTop > emptyLineTop)
synchronizer.scrollToLine(4)
val printlnTop = scrollState.value
assertTrue(printlnTop > mainTop)
synchronizer.scrollToLine(5)
val rBracketTop = scrollState.value
assertTrue(rBracketTop > printlnTop)
synchronizer.scrollToLine(2)
assertEquals(emptyLineTop, scrollState.value)
assertSameDistance(
distance = CODE_TEXT_SIZE + 2,
packageTop,
emptyLineTop,
mainTop,
printlnTop,
rBracketTop,
)
}
}
@OptIn(ExperimentalTestApi::class)
@Test
fun `change a block`() {
val firstRun =
"""
```kotlin
package my.awesome.pkg
fun main() {
println("Hello world")
}
```
"""
.trimIndent()
val secondRun =
"""
```kotlin
package my.awesome.pkg
fun main() {
val name = "Steve"
println("Hello " + name)
}
```
"""
.trimIndent()
doTest(firstRun, secondRun) { scrollState, synchronizer ->
synchronizer.scrollToLine(1)
val packageTop = scrollState.value
assertTrue(packageTop > 0)
synchronizer.scrollToLine(2)
val emptyLineTop = scrollState.value
assertTrue(emptyLineTop > packageTop)
synchronizer.scrollToLine(3)
val mainTop = scrollState.value
assertTrue(mainTop > emptyLineTop)
synchronizer.scrollToLine(4)
val valTop = scrollState.value
assertTrue(valTop > mainTop)
synchronizer.scrollToLine(5)
val printlnTop = scrollState.value
assertTrue(printlnTop > mainTop)
synchronizer.scrollToLine(6)
val rBracketTop = scrollState.value
assertTrue(rBracketTop > printlnTop)
synchronizer.scrollToLine(2)
assertEquals(emptyLineTop, scrollState.value)
assertSameDistance(
distance = CODE_TEXT_SIZE + 2,
packageTop,
emptyLineTop,
mainTop,
valTop,
printlnTop,
rBracketTop,
)
}
}
@OptIn(ExperimentalTestApi::class)
@Test
fun `merge code blocks`() {
val firstRun =
"""
```kotlin
package my.awesome.pkg
fun main() {
println("Hello world")
}
```
```kotlin
fun foo() {
println("Foo")
}
```
"""
.trimIndent()
val secondRun =
"""
```kotlin
package my.awesome.pkg
fun main() {
println("Hello world")
}
fun foo() {
println("Foo")
}
```
"""
.trimIndent()
doTest(firstRun, secondRun) { scrollState, synchronizer ->
synchronizer.scrollToLine(1)
val packageTop = scrollState.value
assertTrue(packageTop > 0)
synchronizer.scrollToLine(2)
val emptyLine1Top = scrollState.value
assertTrue(emptyLine1Top > packageTop)
synchronizer.scrollToLine(3)
val mainTop = scrollState.value
assertTrue(mainTop > emptyLine1Top)
synchronizer.scrollToLine(4)
val println1Top = scrollState.value
assertTrue(println1Top > mainTop)
synchronizer.scrollToLine(5)
val rBracket1Top = scrollState.value
assertTrue(rBracket1Top > println1Top)
synchronizer.scrollToLine(6)
val emptyLine2Top = scrollState.value
assertTrue(emptyLine2Top > rBracket1Top)
synchronizer.scrollToLine(7)
val fooTop = scrollState.value
assertTrue(fooTop > emptyLine2Top)
synchronizer.scrollToLine(8)
val println2Top = scrollState.value
assertTrue(println2Top > fooTop)
synchronizer.scrollToLine(9)
val rBracket2Top = scrollState.value
assertTrue(rBracket2Top > println2Top)
synchronizer.scrollToLine(2)
assertEquals(emptyLine1Top, scrollState.value)
assertSameDistance(
distance = CODE_TEXT_SIZE + 2,
packageTop,
emptyLine1Top,
mainTop,
println1Top,
rBracket1Top,
emptyLine2Top,
fooTop,
println2Top,
rBracket2Top,
)
}
}
private fun assertSameDistance(distance: Int, vararg elements: Int) {
assertTrue(elements.size > 1)
for (i in 0..<elements.lastIndex) {
assertEquals(distance, elements[i + 1] - elements[i])
}
}
@OptIn(ExperimentalTestApi::class)
private fun doTest(
firstRun: String,
secondRun: String,
action: suspend (ScrollState, ScrollingSynchronizer) -> Unit,
) {
doTest(
yieldBlocks = {
processMarkdownDocument(firstRun)
processMarkdownDocument(secondRun)
},
action = action,
)
}
private fun doTest(markdown: String, action: suspend (ScrollState, ScrollingSynchronizer) -> Unit) {
doTest(yieldBlocks = { processMarkdownDocument(markdown) }, action = action)
}
@OptIn(ExperimentalTestApi::class)
private fun doTest(
yieldBlocks: MarkdownProcessor.() -> List<MarkdownBlock>,
action: suspend (ScrollState, ScrollingSynchronizer) -> Unit,
) = runComposeUiTest {
val scrollState = ScrollState(0)
val synchronizer = ScrollingSynchronizer.create(scrollState)!!
val markdownStyling: MarkdownStyling = createMarkdownStyling()
val renderer =
ScrollSyncMarkdownBlockRenderer(markdownStyling, emptyList(), DefaultInlineMarkdownRenderer(emptyList()))
val processor = MarkdownProcessor(markdownMode = MarkdownMode.EditorPreview(synchronizer))
runBlocking {
suspendCancellableCoroutine { cont ->
setContent {
CompositionLocalProvider(
LocalMarkdownStyling provides markdownStyling,
LocalMarkdownMode provides MarkdownMode.EditorPreview(synchronizer),
LocalMarkdownProcessor provides processor,
LocalMarkdownBlockRenderer provides renderer,
LocalCodeHighlighter provides NoOpCodeHighlighter,
LocalDividerStyle provides createDividerStyle(),
LocalScrollbarStyle provides createScrollbarStyle(),
LocalDensity provides createDensity(),
) {
JewelTheme(createThemeDefinition()) {
val blocks = processor.yieldBlocks()
renderer.render(blocks, true, {}, {})
}
}
// Can't test animateScrollTo without acquiring a composable scope;
val scope = rememberCoroutineScope()
scope.launch(Dispatchers.Default) {
runOnIdle {
scope.launch {
try {
action(scrollState, synchronizer)
} finally {
cont.resumeWith(Result.success(Unit))
}
}
}
}
}
}
}
}
private fun createDividerStyle() =
DividerStyle(color = Color.Black, metrics = DividerMetrics(thickness = 1.dp, startIndent = 0.dp))
private fun createScrollbarStyle() =
ScrollbarStyle(
colors =
ScrollbarColors(
thumbBackground = Color.Black,
thumbBorderActive = Color.Black,
thumbBackgroundActive = Color.Black,
thumbOpaqueBackground = Color.Black,
thumbOpaqueBackgroundHovered = Color.Black,
thumbBorder = Color.Black,
thumbOpaqueBorder = Color.Black,
thumbOpaqueBorderHovered = Color.Black,
trackBackground = Color.Black,
trackBackgroundExpanded = Color.Black,
trackOpaqueBackground = Color.Black,
trackOpaqueBackgroundHovered = Color.Black,
),
metrics = ScrollbarMetrics(thumbCornerSize = CornerSize(1.dp), minThumbLength = 1.dp),
trackClickBehavior = TrackClickBehavior.NextPage,
scrollbarVisibility =
ScrollbarVisibility.AlwaysVisible(
trackThickness = 1.dp,
trackPadding = PaddingValues(1.dp),
trackPaddingWithBorder = PaddingValues(1.dp),
thumbColorAnimationDuration = 500.milliseconds,
trackColorAnimationDuration = 500.milliseconds,
scrollbarBackgroundColorLight = Color.White,
scrollbarBackgroundColorDark = Color.White,
),
)
private fun createDensity() = Density(1f)
private fun createThemeDefinition(): ThemeDefinition {
return ThemeDefinition(
name = "Test",
isDark = false,
globalColors =
GlobalColors(
borders = BorderColors(normal = Color.Black, focused = Color.Black, disabled = Color.Black),
outlines =
OutlineColors(
focused = Color.Black,
focusedWarning = Color.Black,
focusedError = Color.Black,
warning = Color.Black,
error = Color.Black,
),
text =
TextColors(
normal = Color.Black,
selected = Color.Black,
disabled = Color.Black,
info = Color.Black,
error = Color.Black,
),
panelBackground = Color.White,
),
globalMetrics = GlobalMetrics(outlineWidth = 10.dp, rowHeight = 24.dp),
defaultTextStyle = TextStyle.Default,
editorTextStyle = TextStyle.Default,
consoleTextStyle = TextStyle.Default,
contentColor = Color.Black,
colorPalette = ThemeColorPalette.Empty,
iconData = ThemeIconData.Empty,
)
}
private fun createMarkdownStyling(): MarkdownStyling {
val mockSpanStyle = SpanStyle(Color.Black)
val inlinesStyling =
InlinesStyling(
textStyle = TextStyle.Default,
inlineCode = mockSpanStyle,
link = mockSpanStyle,
linkDisabled = mockSpanStyle,
linkFocused = mockSpanStyle,
linkHovered = mockSpanStyle,
linkPressed = mockSpanStyle,
linkVisited = mockSpanStyle,
emphasis = mockSpanStyle,
strongEmphasis = mockSpanStyle,
inlineHtml = mockSpanStyle,
renderInlineHtml = false,
)
return MarkdownStyling(
blockVerticalSpacing = 8.dp,
paragraph = MarkdownStyling.Paragraph(inlinesStyling),
heading =
MarkdownStyling.Heading(
h1 = MarkdownStyling.Heading.H1(inlinesStyling, 1.dp, Color.Black, 2.dp, PaddingValues(4.dp)),
h2 = MarkdownStyling.Heading.H2(inlinesStyling, 1.dp, Color.Black, 2.dp, PaddingValues(4.dp)),
h3 = MarkdownStyling.Heading.H3(inlinesStyling, 1.dp, Color.Black, 2.dp, PaddingValues(4.dp)),
h4 = MarkdownStyling.Heading.H4(inlinesStyling, 1.dp, Color.Black, 2.dp, PaddingValues(4.dp)),
h5 = MarkdownStyling.Heading.H5(inlinesStyling, 1.dp, Color.Black, 2.dp, PaddingValues(4.dp)),
h6 = MarkdownStyling.Heading.H6(inlinesStyling, 1.dp, Color.Black, 2.dp, PaddingValues(4.dp)),
),
blockQuote =
MarkdownStyling.BlockQuote(
padding = PaddingValues(4.dp),
lineWidth = 2.dp,
lineColor = Color.Gray,
pathEffect = null,
strokeCap = StrokeCap.Square,
textColor = Color.Black,
),
code =
MarkdownStyling.Code(
indented =
MarkdownStyling.Code.Indented(
editorTextStyle = TextStyle.Default.copy(fontSize = CODE_TEXT_SIZE.sp),
padding = PaddingValues(4.dp),
shape = RectangleShape,
background = Color.LightGray,
borderWidth = 0.dp,
borderColor = Color.DarkGray,
fillWidth = true,
scrollsHorizontally = true,
),
fenced =
MarkdownStyling.Code.Fenced(
editorTextStyle = TextStyle.Default.copy(fontSize = CODE_TEXT_SIZE.sp),
padding = PaddingValues(4.dp),
shape = RectangleShape,
background = Color.LightGray,
borderWidth = 0.dp,
borderColor = Color.DarkGray,
fillWidth = true,
scrollsHorizontally = true,
infoTextStyle = TextStyle.Default,
infoPadding = PaddingValues(2.dp),
infoPosition = MarkdownStyling.Code.Fenced.InfoPosition.TopStart,
),
),
list =
MarkdownStyling.List(
ordered =
MarkdownStyling.List.Ordered(
numberStyle = TextStyle.Default,
numberContentGap = 1.dp,
numberMinWidth = 2.dp,
numberTextAlign = TextAlign.Start,
itemVerticalSpacing = 4.dp,
itemVerticalSpacingTight = 2.dp,
padding = PaddingValues(4.dp),
),
unordered =
MarkdownStyling.List.Unordered(
bullet = '.',
bulletStyle = TextStyle.Default,
bulletContentGap = 1.dp,
itemVerticalSpacing = 4.dp,
itemVerticalSpacingTight = 2.dp,
padding = PaddingValues(4.dp),
),
),
image =
MarkdownStyling.Image(
alignment = Alignment.Center,
contentScale = ContentScale.Crop,
padding = PaddingValues(8.dp),
shape = RectangleShape,
background = Color.Transparent,
borderWidth = 1.dp,
borderColor = Color.LightGray,
),
thematicBreak =
MarkdownStyling.ThematicBreak(
padding = PaddingValues(4.dp),
lineWidth = 2.dp,
lineColor = Color.DarkGray,
),
htmlBlock =
MarkdownStyling.HtmlBlock(
textStyle = TextStyle.Default,
padding = PaddingValues(4.dp),
shape = RectangleShape,
background = Color.White,
borderWidth = 1.dp,
borderColor = Color.Gray,
fillWidth = true,
),
)
}
companion object {
private const val CODE_TEXT_SIZE = 10
}
}

View File

@@ -2,8 +2,8 @@ f:org.jetbrains.jewel.intui.markdown.bridge.BridgeMarkdownBlockRendererExtension
- sf:create(org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer$Companion,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,java.util.List,org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer):org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer
- bs:create$default(org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer$Companion,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,java.util.List,org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer,I,java.lang.Object):org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer
f:org.jetbrains.jewel.intui.markdown.bridge.BridgeProvideMarkdownStylingKt
- sf:ProvideMarkdownStyling(com.intellij.openapi.project.Project,java.lang.String,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.processing.MarkdownProcessor,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
- sf:ProvideMarkdownStyling(java.lang.String,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.processing.MarkdownProcessor,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
- sf:ProvideMarkdownStyling(com.intellij.openapi.project.Project,java.lang.String,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.MarkdownMode,org.jetbrains.jewel.markdown.processing.MarkdownProcessor,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
- sf:ProvideMarkdownStyling(java.lang.String,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.MarkdownMode,org.jetbrains.jewel.markdown.processing.MarkdownProcessor,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
f:org.jetbrains.jewel.intui.markdown.bridge.styling.BridgeMarkdownStylingKt
- sf:create(org.jetbrains.jewel.markdown.rendering.InlinesStyling$Companion,androidx.compose.ui.text.TextStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,Z):org.jetbrains.jewel.markdown.rendering.InlinesStyling
- sf:create(org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code$Companion,androidx.compose.ui.text.TextStyle,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code$Indented,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code$Fenced):org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code

View File

@@ -4,8 +4,8 @@ public final class org/jetbrains/jewel/intui/markdown/bridge/BridgeMarkdownBlock
}
public final class org/jetbrains/jewel/intui/markdown/bridge/BridgeProvideMarkdownStylingKt {
public static final fun ProvideMarkdownStyling (Lcom/intellij/openapi/project/Project;Ljava/lang/String;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static final fun ProvideMarkdownStyling (Ljava/lang/String;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer;Lorg/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static final fun ProvideMarkdownStyling (Lcom/intellij/openapi/project/Project;Ljava/lang/String;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Lorg/jetbrains/jewel/markdown/MarkdownMode;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static final fun ProvideMarkdownStyling (Ljava/lang/String;Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Lorg/jetbrains/jewel/markdown/MarkdownMode;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer;Lorg/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
}
public final class org/jetbrains/jewel/intui/markdown/bridge/styling/BridgeMarkdownStylingKt {

View File

@@ -12,7 +12,9 @@ import org.jetbrains.jewel.foundation.code.highlighting.LocalCodeHighlighter
import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.intui.markdown.bridge.styling.create
import org.jetbrains.jewel.markdown.MarkdownMode
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownBlockRenderer
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownMode
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownProcessor
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownStyling
import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
@@ -24,7 +26,8 @@ import org.jetbrains.jewel.markdown.rendering.MarkdownStyling
public fun ProvideMarkdownStyling(
themeName: String = JewelTheme.name,
markdownStyling: MarkdownStyling = remember(themeName) { MarkdownStyling.create() },
markdownProcessor: MarkdownProcessor = remember { MarkdownProcessor() },
markdownMode: MarkdownMode = MarkdownMode.Standalone,
markdownProcessor: MarkdownProcessor = remember { MarkdownProcessor(markdownMode = markdownMode) },
markdownBlockRenderer: MarkdownBlockRenderer =
remember(markdownStyling) { MarkdownBlockRenderer.create(markdownStyling) },
codeHighlighter: CodeHighlighter = remember { NoOpCodeHighlighter },
@@ -32,6 +35,7 @@ public fun ProvideMarkdownStyling(
) {
CompositionLocalProvider(
LocalMarkdownStyling provides markdownStyling,
LocalMarkdownMode provides markdownMode,
LocalMarkdownProcessor provides markdownProcessor,
LocalMarkdownBlockRenderer provides markdownBlockRenderer,
LocalCodeHighlighter provides codeHighlighter,
@@ -46,7 +50,8 @@ public fun ProvideMarkdownStyling(
project: Project,
themeName: String = JewelTheme.name,
markdownStyling: MarkdownStyling = remember(themeName) { MarkdownStyling.create() },
markdownProcessor: MarkdownProcessor = remember { MarkdownProcessor() },
markdownMode: MarkdownMode = remember { MarkdownMode.Standalone },
markdownProcessor: MarkdownProcessor = remember { MarkdownProcessor(markdownMode = markdownMode) },
markdownBlockRenderer: MarkdownBlockRenderer =
remember(markdownStyling) { MarkdownBlockRenderer.create(markdownStyling) },
content: @Composable () -> Unit,
@@ -56,6 +61,7 @@ public fun ProvideMarkdownStyling(
ProvideMarkdownStyling(
themeName = themeName,
markdownStyling = markdownStyling,
markdownMode = markdownMode,
markdownProcessor = markdownProcessor,
markdownBlockRenderer = markdownBlockRenderer,
codeHighlighter = codeHighlighter,

View File

@@ -4,8 +4,8 @@ f:org.jetbrains.jewel.intui.markdown.standalone.IntUiMarkdownBlockRendererExtens
- sf:light(org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer$Companion,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,java.util.List,org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer):org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer
- bs:light$default(org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer$Companion,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,java.util.List,org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer,I,java.lang.Object):org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer
f:org.jetbrains.jewel.intui.markdown.standalone.IntUiProvideMarkdownStylingKt
- sf:ProvideMarkdownStyling(org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter,org.jetbrains.jewel.markdown.processing.MarkdownProcessor,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
- sf:ProvideMarkdownStyling(Z,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.processing.MarkdownProcessor,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
- sf:ProvideMarkdownStyling(org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter,org.jetbrains.jewel.markdown.MarkdownMode,org.jetbrains.jewel.markdown.processing.MarkdownProcessor,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
- sf:ProvideMarkdownStyling(Z,org.jetbrains.jewel.markdown.rendering.MarkdownStyling,org.jetbrains.jewel.markdown.MarkdownMode,org.jetbrains.jewel.markdown.processing.MarkdownProcessor,org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer,org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
f:org.jetbrains.jewel.intui.markdown.standalone.styling.IntUiMarkdownStylingKt
- sf:dark(org.jetbrains.jewel.markdown.rendering.InlinesStyling$Companion,androidx.compose.ui.text.TextStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,androidx.compose.ui.text.SpanStyle,Z):org.jetbrains.jewel.markdown.rendering.InlinesStyling
- sf:dark(org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code$Companion,androidx.compose.ui.text.TextStyle,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code$Indented,org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code$Fenced):org.jetbrains.jewel.markdown.rendering.MarkdownStyling$Code

View File

@@ -6,8 +6,8 @@ public final class org/jetbrains/jewel/intui/markdown/standalone/IntUiMarkdownBl
}
public final class org/jetbrains/jewel/intui/markdown/standalone/IntUiProvideMarkdownStylingKt {
public static final fun ProvideMarkdownStyling (Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer;Lorg/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static final fun ProvideMarkdownStyling (ZLorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer;Lorg/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static final fun ProvideMarkdownStyling (Lorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer;Lorg/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter;Lorg/jetbrains/jewel/markdown/MarkdownMode;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
public static final fun ProvideMarkdownStyling (ZLorg/jetbrains/jewel/markdown/rendering/MarkdownStyling;Lorg/jetbrains/jewel/markdown/MarkdownMode;Lorg/jetbrains/jewel/markdown/processing/MarkdownProcessor;Lorg/jetbrains/jewel/markdown/rendering/MarkdownBlockRenderer;Lorg/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V
}
public final class org/jetbrains/jewel/intui/markdown/standalone/styling/IntUiMarkdownStylingKt {

View File

@@ -10,7 +10,9 @@ import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.intui.markdown.standalone.styling.dark
import org.jetbrains.jewel.intui.markdown.standalone.styling.light
import org.jetbrains.jewel.markdown.MarkdownMode
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownBlockRenderer
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownMode
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownProcessor
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownStyling
import org.jetbrains.jewel.markdown.processing.MarkdownProcessor
@@ -29,6 +31,7 @@ public fun ProvideMarkdownStyling(
MarkdownStyling.light()
}
},
markdownMode: MarkdownMode = remember { MarkdownMode.Standalone },
markdownProcessor: MarkdownProcessor = remember { MarkdownProcessor() },
markdownBlockRenderer: MarkdownBlockRenderer =
remember(markdownStyling) {
@@ -43,6 +46,7 @@ public fun ProvideMarkdownStyling(
) {
CompositionLocalProvider(
LocalMarkdownStyling provides markdownStyling,
LocalMarkdownMode provides markdownMode,
LocalMarkdownProcessor provides markdownProcessor,
LocalMarkdownBlockRenderer provides markdownBlockRenderer,
LocalCodeHighlighter provides codeHighlighter,
@@ -57,11 +61,13 @@ public fun ProvideMarkdownStyling(
markdownStyling: MarkdownStyling,
markdownBlockRenderer: MarkdownBlockRenderer,
codeHighlighter: CodeHighlighter,
markdownProcessor: MarkdownProcessor = remember { MarkdownProcessor() },
markdownMode: MarkdownMode = MarkdownMode.Standalone,
markdownProcessor: MarkdownProcessor = remember { MarkdownProcessor(markdownMode = markdownMode) },
content: @Composable () -> Unit,
) {
CompositionLocalProvider(
LocalMarkdownStyling provides markdownStyling,
LocalMarkdownMode provides markdownMode,
LocalMarkdownProcessor provides markdownProcessor,
LocalMarkdownBlockRenderer provides markdownBlockRenderer,
LocalCodeHighlighter provides codeHighlighter,

View File

@@ -9,6 +9,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.jetbrains.jewel.foundation.modifier.trackActivation
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.markdown.MarkdownMode
import org.jetbrains.jewel.markdown.WithMarkdownMode
import org.jetbrains.jewel.samples.standalone.view.markdown.JewelReadme
import org.jetbrains.jewel.samples.standalone.view.markdown.MarkdownEditor
import org.jetbrains.jewel.samples.standalone.view.markdown.MarkdownPreview
@@ -18,11 +20,13 @@ import org.jetbrains.jewel.ui.component.Divider
@Composable
internal fun MarkdownDemo() {
Row(Modifier.trackActivation().fillMaxSize().background(JewelTheme.globalColors.panelBackground)) {
val editorState = rememberTextFieldState(JewelReadme)
MarkdownEditor(state = editorState, modifier = Modifier.fillMaxHeight().weight(1f))
WithMarkdownMode(MarkdownMode.EditorPreview(scrollingSynchronizer = null)) {
val editorState = rememberTextFieldState(JewelReadme)
MarkdownEditor(state = editorState, modifier = Modifier.fillMaxHeight().weight(1f))
Divider(Orientation.Vertical, Modifier.fillMaxHeight())
Divider(Orientation.Vertical, Modifier.fillMaxHeight())
MarkdownPreview(modifier = Modifier.fillMaxHeight().weight(1f), rawMarkdown = editorState.text)
MarkdownPreview(modifier = Modifier.fillMaxHeight().weight(1f), rawMarkdown = editorState.text)
}
}
}

View File

@@ -28,6 +28,7 @@ import org.jetbrains.jewel.intui.markdown.standalone.styling.light
import org.jetbrains.jewel.markdown.LazyMarkdown
import org.jetbrains.jewel.markdown.MarkdownBlock
import org.jetbrains.jewel.markdown.extension.autolink.AutolinkProcessorExtension
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownMode
import org.jetbrains.jewel.markdown.extensions.github.alerts.AlertStyling
import org.jetbrains.jewel.markdown.extensions.github.alerts.GitHubAlertProcessorExtension
import org.jetbrains.jewel.markdown.extensions.github.alerts.GitHubAlertRendererExtension
@@ -45,11 +46,11 @@ internal fun MarkdownPreview(modifier: Modifier = Modifier, rawMarkdown: CharSeq
var markdownBlocks by remember { mutableStateOf(emptyList<MarkdownBlock>()) }
val extensions = remember { listOf(GitHubAlertProcessorExtension, AutolinkProcessorExtension) }
val markdownMode = LocalMarkdownMode.current
// We are doing this here for the sake of simplicity.
// In a real-world scenario you would be doing this outside your Composables,
// potentially involving ViewModels, dependency injection, etc.
val processor = remember { MarkdownProcessor(extensions, editorMode = true) }
val processor = remember { MarkdownProcessor(extensions, markdownMode = markdownMode) }
LaunchedEffect(rawMarkdown) {
// TODO you may want to debounce or drop on backpressure, in real usages. You should also