mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 06:50:54 +07:00
[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:
committed by
intellij-monorepo-bot
parent
e3934392b4
commit
6b4b56a99a
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ dependencies {
|
||||
|
||||
testImplementation(compose.desktop.uiTestJUnit4)
|
||||
testImplementation(projects.ui)
|
||||
testImplementation(compose.desktop.currentOs)
|
||||
}
|
||||
|
||||
publicApiValidation {
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user