@mui/internal-docs-infra 0.11.1-canary.8 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ChunkProvider/ChunkContext.d.mts +10 -0
- package/ChunkProvider/ChunkContext.mjs +15 -0
- package/ChunkProvider/ChunkProvider.d.mts +14 -0
- package/ChunkProvider/ChunkProvider.mjs +38 -0
- package/ChunkProvider/PreloadContext.d.mts +14 -0
- package/ChunkProvider/PreloadContext.mjs +18 -0
- package/ChunkProvider/PreloadProvider.d.mts +13 -0
- package/ChunkProvider/PreloadProvider.mjs +33 -0
- package/ChunkProvider/index.d.mts +7 -0
- package/ChunkProvider/index.mjs +7 -0
- package/ChunkProvider/types.d.mts +23 -0
- package/ChunkProvider/types.mjs +1 -0
- package/ChunkProvider/usePreload.d.mts +8 -0
- package/ChunkProvider/usePreload.mjs +21 -0
- package/CodeControllerContext/CodeControllerContext.d.mts +11 -0
- package/CodeControllerContext/CodeControllerContext.mjs +2 -1
- package/CodeHighlighter/CodeHighlighter.d.mts +15 -1
- package/CodeHighlighter/CodeHighlighter.mjs +97 -319
- package/CodeHighlighter/CodeHighlighterChunk.d.mts +42 -0
- package/CodeHighlighter/CodeHighlighterChunk.mjs +77 -0
- package/CodeHighlighter/CodeHighlighterClient.mjs +597 -128
- package/CodeHighlighter/CodeHighlighterContext.d.mts +57 -1
- package/CodeHighlighter/CodeHighlighterFallbackContext.d.mts +14 -2
- package/CodeHighlighter/CodeHighlighterFallbackContext.mjs +1 -3
- package/CodeHighlighter/CodeInitialSourceLoader.d.mts +10 -0
- package/CodeHighlighter/CodeInitialSourceLoader.mjs +108 -0
- package/CodeHighlighter/CodeSourceLoader.d.mts +11 -0
- package/CodeHighlighter/CodeSourceLoader.mjs +128 -0
- package/CodeHighlighter/buildCodeHighlighterChunkProps.d.mts +47 -0
- package/CodeHighlighter/buildCodeHighlighterChunkProps.mjs +61 -0
- package/CodeHighlighter/buildStringFallback.d.mts +29 -0
- package/CodeHighlighter/buildStringFallback.mjs +42 -0
- package/CodeHighlighter/codeToFallbackProps.d.mts +31 -2
- package/CodeHighlighter/codeToFallbackProps.mjs +347 -42
- package/CodeHighlighter/createClientProps.d.mts +17 -0
- package/CodeHighlighter/createClientProps.mjs +78 -0
- package/CodeHighlighter/errors.d.mts +6 -0
- package/CodeHighlighter/errors.mjs +10 -0
- package/CodeHighlighter/fallbackCompression.d.mts +96 -0
- package/CodeHighlighter/fallbackCompression.mjs +253 -0
- package/CodeHighlighter/fallbackFormat.d.mts +137 -0
- package/CodeHighlighter/fallbackFormat.mjs +422 -0
- package/CodeHighlighter/index.d.mts +4 -1
- package/CodeHighlighter/index.mjs +3 -1
- package/CodeHighlighter/mergeComments.d.mts +38 -0
- package/CodeHighlighter/mergeComments.mjs +80 -0
- package/CodeHighlighter/prepareInitialSource.d.mts +42 -0
- package/CodeHighlighter/prepareInitialSource.mjs +292 -0
- package/CodeHighlighter/resolveFallbackCritical.d.mts +23 -0
- package/CodeHighlighter/resolveFallbackCritical.mjs +44 -0
- package/CodeHighlighter/types.d.mts +272 -8
- package/CodeHighlighter/useCodeFallback.d.mts +94 -0
- package/CodeHighlighter/useCodeFallback.mjs +204 -0
- package/CodeHighlighter/useGrammarsReady.d.mts +18 -0
- package/CodeHighlighter/useGrammarsReady.mjs +45 -0
- package/CodeHighlighter/useSpeculativeCodePreload.d.mts +26 -0
- package/CodeHighlighter/useSpeculativeCodePreload.mjs +40 -0
- package/CodeHighlighter/useSpeculativeEditingPreload.d.mts +33 -0
- package/CodeHighlighter/useSpeculativeEditingPreload.mjs +58 -0
- package/CodeHighlighter/useSpeculativeGrammarPreload.d.mts +23 -0
- package/CodeHighlighter/useSpeculativeGrammarPreload.mjs +31 -0
- package/CodeHighlighter/useSpeculativeUseCodePreload.d.mts +22 -0
- package/CodeHighlighter/useSpeculativeUseCodePreload.mjs +41 -0
- package/CodeProvider/CodeContext.d.mts +47 -12
- package/CodeProvider/CodeContext.mjs +7 -0
- package/CodeProvider/CodeProvider.d.mts +4 -2
- package/CodeProvider/CodeProvider.mjs +40 -102
- package/CodeProvider/CodeProviderLazy.d.mts +40 -0
- package/CodeProvider/CodeProviderLazy.mjs +96 -0
- package/CodeProvider/constants.d.mts +26 -0
- package/CodeProvider/constants.mjs +24 -0
- package/CodeProvider/createParseSourceWorkerClient.d.mts +6 -0
- package/CodeProvider/createParseSourceWorkerClient.mjs +22 -2
- package/CodeProvider/index.d.mts +2 -1
- package/CodeProvider/index.mjs +9 -1
- package/CodeProvider/parseSourceWorker.mjs +33 -0
- package/CodeProvider/useCodeProviderValue.d.mts +54 -0
- package/CodeProvider/useCodeProviderValue.mjs +188 -0
- package/CoordinatedLazy/ChunkServerLoader.d.mts +25 -0
- package/CoordinatedLazy/ChunkServerLoader.mjs +97 -0
- package/CoordinatedLazy/CoordinatedContentContext.d.mts +15 -0
- package/CoordinatedLazy/CoordinatedContentContext.mjs +22 -0
- package/CoordinatedLazy/CoordinatedFallbackContext.d.mts +11 -0
- package/CoordinatedLazy/CoordinatedFallbackContext.mjs +13 -0
- package/CoordinatedLazy/CoordinatedGateContext.d.mts +14 -0
- package/CoordinatedLazy/CoordinatedGateContext.mjs +19 -0
- package/CoordinatedLazy/CoordinatedLazy.d.mts +14 -0
- package/CoordinatedLazy/CoordinatedLazy.mjs +86 -0
- package/CoordinatedLazy/CoordinatedLazyClient.d.mts +24 -0
- package/CoordinatedLazy/CoordinatedLazyClient.mjs +65 -0
- package/CoordinatedLazy/LazyContent.d.mts +26 -0
- package/CoordinatedLazy/LazyContent.mjs +80 -0
- package/CoordinatedLazy/LazyContentServer.d.mts +18 -0
- package/CoordinatedLazy/LazyContentServer.mjs +25 -0
- package/CoordinatedLazy/buildChunkRenderInputs.d.mts +8 -0
- package/CoordinatedLazy/buildChunkRenderInputs.mjs +35 -0
- package/CoordinatedLazy/createCoordinatedLazy.d.mts +32 -0
- package/CoordinatedLazy/createCoordinatedLazy.mjs +127 -0
- package/CoordinatedLazy/index.d.mts +14 -0
- package/CoordinatedLazy/index.mjs +18 -0
- package/CoordinatedLazy/resolveChunkRender.d.mts +26 -0
- package/CoordinatedLazy/resolveChunkRender.mjs +73 -0
- package/CoordinatedLazy/types.d.mts +408 -0
- package/CoordinatedLazy/types.mjs +1 -0
- package/CoordinatedLazy/useChunk.d.mts +30 -0
- package/CoordinatedLazy/useChunk.mjs +135 -0
- package/CoordinatedLazy/useCoordinatedFallback.d.mts +12 -0
- package/CoordinatedLazy/useCoordinatedFallback.mjs +40 -0
- package/CoordinatedLazy/useCoordinatedSwap.d.mts +16 -0
- package/CoordinatedLazy/useCoordinatedSwap.mjs +124 -0
- package/LICENSE +1 -1
- package/abstractCreateDemo/abstractCreateDemo.d.mts +54 -3
- package/abstractCreateDemo/abstractCreateDemo.mjs +47 -7
- package/abstractCreateDemo/resolveDemoFlag.d.mts +20 -0
- package/abstractCreateDemo/resolveDemoFlag.mjs +25 -0
- package/abstractCreateStream/abstractCreateStream.d.mts +18 -0
- package/abstractCreateStream/abstractCreateStream.mjs +45 -0
- package/abstractCreateStream/index.d.mts +2 -0
- package/abstractCreateStream/index.mjs +1 -0
- package/abstractCreateStream/types.d.mts +34 -0
- package/abstractCreateStream/types.mjs +1 -0
- package/abstractCreateTypes/TypeCode.mjs +12 -11
- package/abstractCreateTypes/typesToJsx.mjs +30 -9
- package/cli/ensureDemoClients.mjs +4 -148
- package/cli/ensureDemoPages.d.mts +45 -0
- package/cli/ensureDemoPages.mjs +99 -0
- package/cli/fileUtils/index.d.mts +11 -0
- package/cli/fileUtils/index.mjs +48 -0
- package/cli/findDemoIndexFiles.d.mts +15 -0
- package/cli/findDemoIndexFiles.mjs +121 -0
- package/cli/index.mjs +1 -1
- package/cli/loadNextConfig.d.mts +25 -0
- package/cli/loadNextConfig.mjs +60 -1
- package/cli/runBrowser.mjs +1 -1
- package/cli/runValidate.mjs +44 -1
- package/package.json +85 -5
- package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasis.mjs +30 -0
- package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasisLazy.d.mts +17 -0
- package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasisLazy.mjs +52 -0
- package/pipeline/hastUtils/frameFallbackFromSpans.d.mts +18 -0
- package/pipeline/hastUtils/frameFallbackFromSpans.mjs +24 -0
- package/pipeline/hastUtils/hast.d.mts +27 -0
- package/pipeline/hastUtils/hastCompression.d.mts +3 -1
- package/pipeline/hastUtils/hastCompression.mjs +9 -1
- package/pipeline/hastUtils/hastDecompress.mjs +10 -4
- package/pipeline/hastUtils/hastDictionary.mjs +9 -0
- package/pipeline/hastUtils/hastUtils.d.mts +4 -3
- package/pipeline/hastUtils/hastUtils.mjs +24 -12
- package/pipeline/hastUtils/index.d.mts +2 -1
- package/pipeline/hastUtils/index.mjs +2 -1
- package/pipeline/hastUtils/stripHighlightingSpans.d.mts +6 -2
- package/pipeline/hastUtils/stripHighlightingSpans.mjs +22 -10
- package/pipeline/lintJavascriptDemoFocus/lintJavascriptDemoFocus.mjs +10 -7
- package/pipeline/loadIsomorphicCodeVariant/applyCodeTransform.d.mts +31 -13
- package/pipeline/loadIsomorphicCodeVariant/applyCodeTransform.mjs +50 -55
- package/pipeline/loadIsomorphicCodeVariant/applyCodeTransformWithComments.d.mts +78 -0
- package/pipeline/loadIsomorphicCodeVariant/applyCodeTransformWithComments.mjs +405 -0
- package/pipeline/loadIsomorphicCodeVariant/computeHastDeltas.d.mts +5 -5
- package/pipeline/loadIsomorphicCodeVariant/computeHastDeltas.mjs +36 -66
- package/pipeline/loadIsomorphicCodeVariant/decodeHastSource.d.mts +23 -0
- package/pipeline/loadIsomorphicCodeVariant/decodeHastSource.mjs +92 -0
- package/pipeline/loadIsomorphicCodeVariant/decodeSource.d.mts +19 -0
- package/pipeline/loadIsomorphicCodeVariant/decodeSource.mjs +25 -0
- package/pipeline/loadIsomorphicCodeVariant/decodeSourceToText.d.mts +17 -0
- package/pipeline/loadIsomorphicCodeVariant/decodeSourceToText.mjs +26 -0
- package/pipeline/loadIsomorphicCodeVariant/diffHast.d.mts +26 -2
- package/pipeline/loadIsomorphicCodeVariant/diffHast.mjs +563 -19
- package/pipeline/loadIsomorphicCodeVariant/embedTransforms.d.mts +49 -0
- package/pipeline/loadIsomorphicCodeVariant/embedTransforms.mjs +152 -0
- package/pipeline/loadIsomorphicCodeVariant/findExpandingRanges.d.mts +51 -0
- package/pipeline/loadIsomorphicCodeVariant/findExpandingRanges.mjs +161 -0
- package/pipeline/loadIsomorphicCodeVariant/flattenCodeVariant.mjs +6 -3
- package/pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.d.mts +12 -0
- package/pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.mjs +44 -0
- package/pipeline/loadIsomorphicCodeVariant/getInitialVisibleSourceLines.d.mts +16 -0
- package/pipeline/loadIsomorphicCodeVariant/getInitialVisibleSourceLines.mjs +74 -0
- package/pipeline/loadIsomorphicCodeVariant/loadCodeFallback.mjs +17 -5
- package/pipeline/loadIsomorphicCodeVariant/loadIsomorphicCodeVariant.mjs +229 -15
- package/pipeline/loadIsomorphicCodeVariant/transformSource.d.mts +2 -2
- package/pipeline/loadIsomorphicCodeVariant/transformSource.mjs +56 -22
- package/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.d.mts +18 -0
- package/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.mjs +11 -7
- package/pipeline/loadServerTypes/hastTypeUtils.d.mts +2 -2
- package/pipeline/loadServerTypes/hastTypeUtils.mjs +4 -4
- package/pipeline/loadServerTypes/loadServerTypes.mjs +1 -1
- package/pipeline/loadServerTypesMeta/extractJSDocText.d.mts +14 -0
- package/pipeline/loadServerTypesMeta/extractJSDocText.mjs +60 -0
- package/pipeline/loadServerTypesMeta/processTypes.mjs +43 -46
- package/pipeline/loadServerTypesText/order.mjs +1 -1
- package/pipeline/loadServerTypesText/parseTypesMarkdown.mjs +3 -1
- package/pipeline/loaderUtils/index.d.mts +0 -1
- package/pipeline/loaderUtils/index.mjs +0 -1
- package/pipeline/loaderUtils/parseImportsAndComments.d.mts +5 -1
- package/pipeline/loaderUtils/parseImportsAndComments.mjs +19 -9
- package/pipeline/loaderUtils/resolveModulePath.mjs +23 -1
- package/pipeline/parseCreateFactoryCall/parseCreateFactoryCall.d.mts +12 -0
- package/pipeline/parseCreateFactoryCall/parseCreateFactoryCall.mjs +17 -13
- package/pipeline/parseSource/addLineGutters.mjs +45 -11
- package/pipeline/parseSource/calculateFrameRanges.d.mts +22 -0
- package/pipeline/parseSource/calculateFrameRanges.mjs +69 -25
- package/pipeline/parseSource/detectGrammarScopes.d.mts +13 -0
- package/pipeline/parseSource/detectGrammarScopes.mjs +35 -0
- package/pipeline/parseSource/extendSyntaxTokens.mjs +501 -43
- package/pipeline/parseSource/frameVisibility.d.mts +47 -0
- package/pipeline/parseSource/frameVisibility.mjs +114 -0
- package/pipeline/parseSource/grammarCache.d.mts +33 -0
- package/pipeline/parseSource/grammarCache.mjs +73 -0
- package/pipeline/parseSource/grammarLoaders.d.mts +14 -0
- package/pipeline/parseSource/grammarLoaders.mjs +24 -0
- package/pipeline/parseSource/grammarMaps.d.mts +21 -1
- package/pipeline/parseSource/grammarMaps.mjs +36 -0
- package/pipeline/parseSource/isFrameSpan.d.mts +19 -0
- package/pipeline/parseSource/isFrameSpan.mjs +24 -0
- package/pipeline/parseSource/parseSource.d.mts +41 -6
- package/pipeline/parseSource/parseSource.mjs +184 -36
- package/pipeline/parseSource/redistributeFrameFallbacks.d.mts +40 -0
- package/pipeline/parseSource/redistributeFrameFallbacks.mjs +138 -0
- package/pipeline/parseSource/restructureFrames.d.mts +5 -0
- package/pipeline/parseSource/restructureFrames.mjs +179 -16
- package/pipeline/syncPageIndex/metadataToMarkdown.mjs +6 -2
- package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.d.mts +26 -0
- package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.mjs +181 -114
- package/pipeline/transformHtmlCodeInline/removeSuffixFromHighlightedNodes.d.mts +12 -0
- package/pipeline/transformHtmlCodeInline/removeSuffixFromHighlightedNodes.mjs +52 -0
- package/pipeline/transformHtmlCodeInline/transformHtmlCodeInline.mjs +22 -1
- package/pipeline/transformTypescriptToJavascript/removeTypes.d.mts +5 -8
- package/pipeline/transformTypescriptToJavascript/removeTypes.mjs +27 -93
- package/useCode/EditableEngine.d.mts +233 -0
- package/useCode/EditableEngine.mjs +1712 -0
- package/useCode/EditingEngine.d.mts +13 -0
- package/useCode/EditingEngine.mjs +14 -0
- package/useCode/Pre.browser.mjs +5 -1
- package/useCode/Pre.d.mts +127 -1
- package/useCode/Pre.mjs +417 -165
- package/useCode/SourceEditingEngine.d.mts +50 -0
- package/useCode/SourceEditingEngine.mjs +461 -0
- package/useCode/TransformEngine.d.mts +39 -0
- package/useCode/TransformEngine.mjs +208 -0
- package/useCode/editingEngineCache.d.mts +29 -0
- package/useCode/editingEngineCache.mjs +68 -0
- package/useCode/sourceLineCounts.d.mts +80 -0
- package/useCode/sourceLineCounts.mjs +284 -0
- package/useCode/subscribeToggleNudge.d.mts +3 -0
- package/useCode/subscribeToggleNudge.mjs +95 -0
- package/useCode/transformEngineCache.d.mts +21 -0
- package/useCode/transformEngineCache.mjs +60 -0
- package/useCode/useCode.d.mts +140 -1
- package/useCode/useCode.mjs +250 -19
- package/useCode/useCodeUtils.d.mts +131 -20
- package/useCode/useCodeUtils.mjs +267 -194
- package/useCode/useCopyFunctionality.d.mts +13 -1
- package/useCode/useCopyFunctionality.mjs +39 -9
- package/useCode/useEditable.browser.mjs +10 -2
- package/useCode/useEditable.d.mts +27 -106
- package/useCode/useEditable.integration.browser.d.mts +1 -0
- package/useCode/useEditable.integration.browser.mjs +870 -0
- package/useCode/useEditable.mjs +198 -1247
- package/useCode/useEditableUtils.d.mts +50 -1
- package/useCode/useEditableUtils.mjs +29 -0
- package/useCode/useFileNavigation.d.mts +91 -3
- package/useCode/useFileNavigation.mjs +201 -41
- package/useCode/useHighlightGate.d.mts +17 -0
- package/useCode/useHighlightGate.mjs +147 -0
- package/useCode/useSourceEditing.d.mts +8 -0
- package/useCode/useSourceEditing.mjs +158 -314
- package/useCode/useSourceEnhancing.d.mts +5 -1
- package/useCode/useSourceEnhancing.mjs +22 -36
- package/useCode/useTransformManagement.d.mts +93 -5
- package/useCode/useTransformManagement.mjs +496 -28
- package/useCode/useTransitionPhase.d.mts +24 -0
- package/useCode/useTransitionPhase.mjs +49 -0
- package/useCode/useUIState.d.mts +2 -2
- package/useCode/useUIState.mjs +8 -8
- package/useCode/useVariantSelection.d.mts +130 -6
- package/useCode/useVariantSelection.mjs +529 -93
- package/useCodeWindow/useCodeWindow.d.mts +19 -2
- package/useCodeWindow/useCodeWindow.mjs +98 -71
- package/useCoordinated/coordinatePreference.d.mts +439 -0
- package/useCoordinated/coordinatePreference.mjs +951 -0
- package/useCoordinated/coordinatePreference.testUtils.d.mts +21 -0
- package/useCoordinated/coordinatePreference.testUtils.mjs +69 -0
- package/useCoordinated/createSettleGate.d.mts +96 -0
- package/useCoordinated/createSettleGate.mjs +171 -0
- package/useCoordinated/index.d.mts +8 -0
- package/useCoordinated/index.mjs +8 -0
- package/useCoordinated/layoutShiftGate.d.mts +24 -0
- package/useCoordinated/layoutShiftGate.mjs +79 -0
- package/useCoordinated/pageSettleGate.d.mts +11 -0
- package/useCoordinated/pageSettleGate.mjs +13 -0
- package/useCoordinated/scheduleTasks.d.mts +23 -0
- package/useCoordinated/scheduleTasks.mjs +45 -0
- package/useCoordinated/useCoordinated.d.mts +193 -0
- package/useCoordinated/useCoordinated.mjs +469 -0
- package/useCoordinated/useCoordinatedLazy.d.mts +17 -0
- package/useCoordinated/useCoordinatedLazy.mjs +38 -0
- package/useCoordinated/useCoordinatedLocalStorage.d.mts +16 -0
- package/useCoordinated/useCoordinatedLocalStorage.mjs +22 -0
- package/useCoordinated/useCoordinatedPreference.d.mts +20 -0
- package/useCoordinated/useCoordinatedPreference.mjs +26 -0
- package/useCoordinated/useSettleGate.d.mts +11 -0
- package/useCoordinated/useSettleGate.mjs +34 -0
- package/useDemo/exportVariant.d.mts +12 -5
- package/useDemo/exportVariant.mjs +59 -5
- package/useDemo/useDemo.d.mts +5 -2
- package/useScrollAnchor/useScrollAnchor.mjs +28 -5
- package/useStream/index.d.mts +6 -0
- package/useStream/index.mjs +6 -0
- package/useStream/streamChunks.d.mts +23 -0
- package/useStream/streamChunks.mjs +85 -0
- package/useStream/types.d.mts +45 -0
- package/useStream/types.mjs +1 -0
- package/useStream/useStream.d.mts +57 -0
- package/useStream/useStream.mjs +119 -0
- package/useStream/useStreamController.d.mts +15 -0
- package/useStream/useStreamController.mjs +90 -0
- package/withDocsInfra/withDocsInfra.d.mts +19 -0
- package/withDocsInfra/withDocsInfra.mjs +13 -5
- package/pipeline/loaderUtils/convertCommentsToOneIndexed.d.mts +0 -8
- package/pipeline/loaderUtils/convertCommentsToOneIndexed.mjs +0 -16
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
var _style;
|
|
2
|
+
/**
|
|
3
|
+
* Browser integration tests for `useEditable`: the full type → flush → re-highlight →
|
|
4
|
+
* restore cycle the docs live code editor runs on every keystroke. They cover the
|
|
5
|
+
* behaviors that only emerge when the highlighted DOM is replaced underneath the caret —
|
|
6
|
+
* caret stability while typing and deleting indents, scroll-anchor `onBoundary` firing at
|
|
7
|
+
* the visible top/bottom, focus retention on the first keystroke, and a selection's
|
|
8
|
+
* direction surviving a re-render.
|
|
9
|
+
*
|
|
10
|
+
* Unlike `useEditable.browser.ts` (which drives a static highlighted DOM with a `vi.fn()`
|
|
11
|
+
* onChange and only asserts the *text* handed to onChange), these tests wire `useEditable`
|
|
12
|
+
* to a real React component whose `onChange` updates source state, re-highlights it into
|
|
13
|
+
* the production `.line`/`pl-*` span structure, and lets the engine's `observeAndRestore`
|
|
14
|
+
* restore the caret.
|
|
15
|
+
*/
|
|
16
|
+
import * as React from 'react';
|
|
17
|
+
import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest';
|
|
18
|
+
import { render, cleanup, act } from '@testing-library/react';
|
|
19
|
+
import { userEvent } from 'vitest/browser';
|
|
20
|
+
import { useEditable, preloadEditableEngine } from "./useEditable.mjs";
|
|
21
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
await preloadEditableEngine();
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
cleanup();
|
|
27
|
+
document.body.innerHTML = '';
|
|
28
|
+
window.getSelection()?.removeAllRanges();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Minimal, deterministic re-highlighter
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Splits each line into tokens and wraps identifiers (`pl-smi`) and single-char
|
|
35
|
+
// operators (`pl-k`) in their own spans, leaving whitespace/punctuation as bare
|
|
36
|
+
// text. This reproduces the multi-span-per-line DOM shape that production
|
|
37
|
+
// highlighting produces — enough to surface caret-at-element-boundary bugs —
|
|
38
|
+
// without pulling in the async grammar/worker pipeline. Each line span keeps its
|
|
39
|
+
// trailing `\n` inside it, matching the production serializer.
|
|
40
|
+
const WORD = /[A-Za-z0-9_$]/;
|
|
41
|
+
const OPERATOR = new Set(['=', '+', '-', '*', '/', '<', '>', '!', '&', '|', '%', '?', ':']);
|
|
42
|
+
function highlightLine(lineText) {
|
|
43
|
+
const nodes = [];
|
|
44
|
+
let i = 0;
|
|
45
|
+
let key = 0;
|
|
46
|
+
while (i < lineText.length) {
|
|
47
|
+
const char = lineText[i];
|
|
48
|
+
if (WORD.test(char)) {
|
|
49
|
+
let j = i + 1;
|
|
50
|
+
while (j < lineText.length && WORD.test(lineText[j])) {
|
|
51
|
+
j += 1;
|
|
52
|
+
}
|
|
53
|
+
nodes.push(/*#__PURE__*/_jsx("span", {
|
|
54
|
+
className: "pl-smi",
|
|
55
|
+
children: lineText.slice(i, j)
|
|
56
|
+
}, key));
|
|
57
|
+
key += 1;
|
|
58
|
+
i = j;
|
|
59
|
+
} else if (OPERATOR.has(char)) {
|
|
60
|
+
nodes.push(/*#__PURE__*/_jsx("span", {
|
|
61
|
+
className: "pl-k",
|
|
62
|
+
children: char
|
|
63
|
+
}, key));
|
|
64
|
+
key += 1;
|
|
65
|
+
i += 1;
|
|
66
|
+
} else {
|
|
67
|
+
// Coalesce a run of bare characters (whitespace/punctuation) into one
|
|
68
|
+
// text node, as the highlighter would.
|
|
69
|
+
let j = i + 1;
|
|
70
|
+
while (j < lineText.length && !WORD.test(lineText[j]) && !OPERATOR.has(lineText[j])) {
|
|
71
|
+
j += 1;
|
|
72
|
+
}
|
|
73
|
+
nodes.push(lineText.slice(i, j));
|
|
74
|
+
i = j;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return nodes;
|
|
78
|
+
}
|
|
79
|
+
function Highlighted({
|
|
80
|
+
source
|
|
81
|
+
}) {
|
|
82
|
+
// Mirror the production live-editing structure: `.line` spans live inside a
|
|
83
|
+
// `.frame`, and the newline after each line is a SEPARATE sibling text node
|
|
84
|
+
// ("trailing newline" / gap node), NOT kept inside the line span. These gap
|
|
85
|
+
// nodes are exactly what `caretSelector` + `snapCaretOutOfGapNode` exist for.
|
|
86
|
+
// `source` always carries a trailing newline.
|
|
87
|
+
const withoutTrailing = source.endsWith('\n') ? source.slice(0, -1) : source;
|
|
88
|
+
const lines = withoutTrailing.split('\n');
|
|
89
|
+
return /*#__PURE__*/_jsx("code", {
|
|
90
|
+
children: /*#__PURE__*/_jsx("span", {
|
|
91
|
+
className: "frame",
|
|
92
|
+
"data-frame": "0",
|
|
93
|
+
"data-lined": "",
|
|
94
|
+
children: lines.map((line, index) => /*#__PURE__*/_jsxs(React.Fragment, {
|
|
95
|
+
children: [/*#__PURE__*/_jsx("span", {
|
|
96
|
+
className: "line",
|
|
97
|
+
"data-ln": index + 1,
|
|
98
|
+
children: highlightLine(line)
|
|
99
|
+
}), '\n']
|
|
100
|
+
}, index))
|
|
101
|
+
})
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function Editor({
|
|
105
|
+
initialSource,
|
|
106
|
+
options,
|
|
107
|
+
handleRef,
|
|
108
|
+
scroll
|
|
109
|
+
}) {
|
|
110
|
+
const [source, setSource] = React.useState(initialSource);
|
|
111
|
+
const [, forceRerender] = React.useReducer(count => count + 1, 0);
|
|
112
|
+
const ref = React.useRef(null);
|
|
113
|
+
const scrollRef = React.useRef(null);
|
|
114
|
+
const onChange = React.useMemo(() => vi.fn((text, _position) => {
|
|
115
|
+
setSource(text);
|
|
116
|
+
}), []);
|
|
117
|
+
useEditable(ref, onChange, options);
|
|
118
|
+
React.useEffect(() => {
|
|
119
|
+
handleRef.current = {
|
|
120
|
+
ref,
|
|
121
|
+
onChange,
|
|
122
|
+
getSource: () => onChange.mock.calls.length ? onChange.mock.calls[onChange.mock.calls.length - 1][0] : initialSource,
|
|
123
|
+
scrollEl: scrollRef.current,
|
|
124
|
+
rerender: forceRerender
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
const editor = /*#__PURE__*/_jsxs(React.Fragment, {
|
|
128
|
+
children: [_style || (_style = /*#__PURE__*/_jsx("style", {
|
|
129
|
+
children: `
|
|
130
|
+
[data-testid="editor"] { line-height: 1.5; font-family: monospace; }
|
|
131
|
+
[data-testid="editor"] .frame[data-lined] { display: block; white-space: normal; line-height: 0; }
|
|
132
|
+
[data-testid="editor"] .frame[data-lined] .line { display: block; white-space: pre; line-height: initial; }
|
|
133
|
+
`
|
|
134
|
+
})), /*#__PURE__*/_jsx("pre", {
|
|
135
|
+
ref: ref,
|
|
136
|
+
style: {
|
|
137
|
+
tabSize: 2,
|
|
138
|
+
margin: 0
|
|
139
|
+
},
|
|
140
|
+
"data-testid": "editor",
|
|
141
|
+
children: /*#__PURE__*/_jsx(Highlighted, {
|
|
142
|
+
source: source
|
|
143
|
+
})
|
|
144
|
+
})]
|
|
145
|
+
});
|
|
146
|
+
if (!scroll) {
|
|
147
|
+
return editor;
|
|
148
|
+
}
|
|
149
|
+
// A short, scrollable viewport so we can detect "the view jumps" by reading
|
|
150
|
+
// scrollTop before/after an edit.
|
|
151
|
+
return /*#__PURE__*/_jsx("div", {
|
|
152
|
+
ref: scrollRef,
|
|
153
|
+
style: {
|
|
154
|
+
height: '60px',
|
|
155
|
+
overflow: 'auto',
|
|
156
|
+
fontFamily: 'monospace',
|
|
157
|
+
lineHeight: '20px'
|
|
158
|
+
},
|
|
159
|
+
"data-testid": "scroll",
|
|
160
|
+
children: editor
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Renders the editor and returns a handle once the engine has attached. */
|
|
165
|
+
async function setupEditor(initialSource, options = {}, opts = {}) {
|
|
166
|
+
const handleRef = {
|
|
167
|
+
current: null
|
|
168
|
+
};
|
|
169
|
+
render(/*#__PURE__*/_jsx(Editor, {
|
|
170
|
+
initialSource: initialSource,
|
|
171
|
+
options: options,
|
|
172
|
+
handleRef: handleRef,
|
|
173
|
+
scroll: opts.scroll
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
// Wait a frame for the layout effects (engine attach + contentEditable).
|
|
177
|
+
await act(async () => {
|
|
178
|
+
await new Promise(resolve => {
|
|
179
|
+
requestAnimationFrame(() => resolve());
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
const handle = handleRef.current;
|
|
183
|
+
const element = handle.ref.current;
|
|
184
|
+
return {
|
|
185
|
+
handle,
|
|
186
|
+
element
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Places the caret at an absolute character offset (counting newlines that live
|
|
192
|
+
* inside `.line` spans) and waits a frame so the engine captures the position.
|
|
193
|
+
*/
|
|
194
|
+
async function placeCaret(element, offset) {
|
|
195
|
+
element.focus();
|
|
196
|
+
const sel = window.getSelection();
|
|
197
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
198
|
+
let current = 0;
|
|
199
|
+
let node = walker.nextNode();
|
|
200
|
+
while (node) {
|
|
201
|
+
const len = node.textContent.length;
|
|
202
|
+
if (current + len >= offset) {
|
|
203
|
+
const range = document.createRange();
|
|
204
|
+
range.setStart(node, offset - current);
|
|
205
|
+
range.collapse(true);
|
|
206
|
+
sel.removeAllRanges();
|
|
207
|
+
sel.addRange(range);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
current += len;
|
|
211
|
+
node = walker.nextNode();
|
|
212
|
+
}
|
|
213
|
+
await act(async () => {
|
|
214
|
+
await new Promise(resolve => {
|
|
215
|
+
requestAnimationFrame(() => resolve());
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Computes the caret's (line, column) from the live Selection. */
|
|
221
|
+
function caretLineColumn(element) {
|
|
222
|
+
const sel = window.getSelection();
|
|
223
|
+
const range = sel.getRangeAt(0);
|
|
224
|
+
const until = document.createRange();
|
|
225
|
+
until.setStart(element, 0);
|
|
226
|
+
until.setEnd(range.startContainer, range.startOffset);
|
|
227
|
+
const text = until.toString();
|
|
228
|
+
const lines = text.split('\n');
|
|
229
|
+
return {
|
|
230
|
+
line: lines.length - 1,
|
|
231
|
+
column: lines[lines.length - 1].length,
|
|
232
|
+
position: text.length
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Lets queued microtasks / rAF callbacks / React effects settle. */
|
|
237
|
+
async function settle() {
|
|
238
|
+
await act(async () => {
|
|
239
|
+
await new Promise(resolve => {
|
|
240
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Builds an async preParse that re-highlights on a macrotask (worker-like). */
|
|
246
|
+
function asyncPreParse() {
|
|
247
|
+
return vi.fn((_text, _position) => new Promise(resolve => {
|
|
248
|
+
setTimeout(() => resolve(undefined), 0);
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A preParse whose resolution is controlled by the test — models the worker
|
|
254
|
+
* round-trip so we can inspect the DOM *during* the async gap (before the
|
|
255
|
+
* re-highlight commits) the way the user sees "the wrong thing for a second".
|
|
256
|
+
*/
|
|
257
|
+
function deferredPreParse() {
|
|
258
|
+
let resolveFn = null;
|
|
259
|
+
const preParse = vi.fn((_text, _position) => new Promise(resolve => {
|
|
260
|
+
resolveFn = () => resolve(undefined);
|
|
261
|
+
}));
|
|
262
|
+
return {
|
|
263
|
+
preParse,
|
|
264
|
+
resolvePending: async () => {
|
|
265
|
+
resolveFn?.();
|
|
266
|
+
resolveFn = null;
|
|
267
|
+
await settle();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
describe('useEditable — caret & selection across re-highlights', () => {
|
|
272
|
+
// -------------------------------------------------------------------------
|
|
273
|
+
// Caret stability when typing x, =, Backspace (must not jump to column 0)
|
|
274
|
+
// -------------------------------------------------------------------------
|
|
275
|
+
it('keeps the caret after the typed char when typing x, =, Backspace on a fresh line', async () => {
|
|
276
|
+
const {
|
|
277
|
+
handle,
|
|
278
|
+
element
|
|
279
|
+
} = await setupEditor('\n', {
|
|
280
|
+
indentation: 2,
|
|
281
|
+
caretSelector: '.line'
|
|
282
|
+
});
|
|
283
|
+
await placeCaret(element, 0);
|
|
284
|
+
await userEvent.keyboard('x');
|
|
285
|
+
await settle();
|
|
286
|
+
await userEvent.keyboard('=');
|
|
287
|
+
await settle();
|
|
288
|
+
await userEvent.keyboard('{Backspace}');
|
|
289
|
+
await settle();
|
|
290
|
+
expect(handle.getSource()).toBe('x\n');
|
|
291
|
+
expect(caretLineColumn(element)).toMatchObject({
|
|
292
|
+
line: 0,
|
|
293
|
+
column: 1
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
it('keeps the caret after the typed char through an async re-highlight (preParse)', async () => {
|
|
297
|
+
const {
|
|
298
|
+
handle,
|
|
299
|
+
element
|
|
300
|
+
} = await setupEditor('\n', {
|
|
301
|
+
indentation: 2,
|
|
302
|
+
caretSelector: '.line',
|
|
303
|
+
preParse: asyncPreParse()
|
|
304
|
+
});
|
|
305
|
+
await placeCaret(element, 0);
|
|
306
|
+
await userEvent.keyboard('x');
|
|
307
|
+
await settle();
|
|
308
|
+
await settle();
|
|
309
|
+
await userEvent.keyboard('=');
|
|
310
|
+
await settle();
|
|
311
|
+
await settle();
|
|
312
|
+
await userEvent.keyboard('{Backspace}');
|
|
313
|
+
await settle();
|
|
314
|
+
await settle();
|
|
315
|
+
expect(handle.getSource()).toBe('x\n');
|
|
316
|
+
expect(caretLineColumn(element)).toMatchObject({
|
|
317
|
+
line: 0,
|
|
318
|
+
column: 1
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
it('keeps the caret when typing = then Backspace at the end of an indented line', async () => {
|
|
322
|
+
// ` <p style=` then backspace the `=`. This mirrors typing an attribute.
|
|
323
|
+
const initial = 'function App() {\n return <p style\n}\n';
|
|
324
|
+
const {
|
|
325
|
+
handle,
|
|
326
|
+
element
|
|
327
|
+
} = await setupEditor(initial, {
|
|
328
|
+
indentation: 2,
|
|
329
|
+
caretSelector: '.line'
|
|
330
|
+
});
|
|
331
|
+
// caret at end of ` return <p style` (line 1)
|
|
332
|
+
const offset = 'function App() {\n return <p style'.length;
|
|
333
|
+
await placeCaret(element, offset);
|
|
334
|
+
await userEvent.keyboard('=');
|
|
335
|
+
await settle();
|
|
336
|
+
await userEvent.keyboard('{Backspace}');
|
|
337
|
+
await settle();
|
|
338
|
+
expect(handle.getSource()).toBe(initial);
|
|
339
|
+
// caret should be back at end of `style` (column 17), not column 0
|
|
340
|
+
expect(caretLineColumn(element)).toMatchObject({
|
|
341
|
+
line: 1,
|
|
342
|
+
column: ' return <p style'.length
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
it('keeps the caret at the end of an indented line in a collapsed (minColumn) gutter', async () => {
|
|
346
|
+
// The collapsed editor (clipped indent gutter) is where the real bug shows.
|
|
347
|
+
const initial = 'function foo() {\n doStuff()\n}\n';
|
|
348
|
+
const {
|
|
349
|
+
handle,
|
|
350
|
+
element
|
|
351
|
+
} = await setupEditor(initial, {
|
|
352
|
+
indentation: 2,
|
|
353
|
+
caretSelector: '.line',
|
|
354
|
+
minColumn: 2,
|
|
355
|
+
minRow: 1,
|
|
356
|
+
maxRow: 1,
|
|
357
|
+
onBoundary: vi.fn()
|
|
358
|
+
});
|
|
359
|
+
const offset = 'function foo() {\n doStuff()'.length; // end of line 1
|
|
360
|
+
await placeCaret(element, offset);
|
|
361
|
+
await userEvent.keyboard('x');
|
|
362
|
+
await settle();
|
|
363
|
+
await userEvent.keyboard('=');
|
|
364
|
+
await settle();
|
|
365
|
+
await userEvent.keyboard('{Backspace}');
|
|
366
|
+
await settle();
|
|
367
|
+
expect(handle.getSource()).toBe('function foo() {\n doStuff()x\n}\n');
|
|
368
|
+
expect(caretLineColumn(element)).toMatchObject({
|
|
369
|
+
line: 1,
|
|
370
|
+
column: ' doStuff()x'.length
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
it('keeps the caret in a collapsed gutter through an async re-highlight', async () => {
|
|
374
|
+
const initial = 'function foo() {\n doStuff()\n}\n';
|
|
375
|
+
const {
|
|
376
|
+
handle,
|
|
377
|
+
element
|
|
378
|
+
} = await setupEditor(initial, {
|
|
379
|
+
indentation: 2,
|
|
380
|
+
caretSelector: '.line',
|
|
381
|
+
minColumn: 2,
|
|
382
|
+
minRow: 1,
|
|
383
|
+
maxRow: 1,
|
|
384
|
+
onBoundary: vi.fn(),
|
|
385
|
+
preParse: asyncPreParse()
|
|
386
|
+
});
|
|
387
|
+
const offset = 'function foo() {\n doStuff()'.length;
|
|
388
|
+
await placeCaret(element, offset);
|
|
389
|
+
await userEvent.keyboard('x');
|
|
390
|
+
await settle();
|
|
391
|
+
await settle();
|
|
392
|
+
await userEvent.keyboard('=');
|
|
393
|
+
await settle();
|
|
394
|
+
await settle();
|
|
395
|
+
await userEvent.keyboard('{Backspace}');
|
|
396
|
+
await settle();
|
|
397
|
+
await settle();
|
|
398
|
+
expect(handle.getSource()).toBe('function foo() {\n doStuff()x\n}\n');
|
|
399
|
+
expect(caretLineColumn(element)).toMatchObject({
|
|
400
|
+
line: 1,
|
|
401
|
+
column: ' doStuff()x'.length
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
it('lands the caret at the line/gap boundary when editing at the end of a line', async () => {
|
|
405
|
+
// The real bug uses the `End` key to land the caret at the line end — which
|
|
406
|
+
// in the framed `.line` structure is the boundary with the inter-line gap
|
|
407
|
+
// node. Native typing there can flatten the spans / split across lines.
|
|
408
|
+
const initial = 'function foo() {\n doStuff()\n}\n';
|
|
409
|
+
const {
|
|
410
|
+
handle,
|
|
411
|
+
element
|
|
412
|
+
} = await setupEditor(initial, {
|
|
413
|
+
indentation: 2,
|
|
414
|
+
caretSelector: '.line'
|
|
415
|
+
});
|
|
416
|
+
// Put the caret somewhere on line 1, then End to the line end.
|
|
417
|
+
await placeCaret(element, 'function foo() {\n do'.length);
|
|
418
|
+
await userEvent.keyboard('{End}');
|
|
419
|
+
await settle();
|
|
420
|
+
await userEvent.keyboard('x');
|
|
421
|
+
await settle();
|
|
422
|
+
await userEvent.keyboard('=');
|
|
423
|
+
await settle();
|
|
424
|
+
await userEvent.keyboard('{Backspace}');
|
|
425
|
+
await settle();
|
|
426
|
+
expect(handle.getSource()).toBe('function foo() {\n doStuff()x\n}\n');
|
|
427
|
+
expect(caretLineColumn(element)).toMatchObject({
|
|
428
|
+
line: 1,
|
|
429
|
+
column: ' doStuff()x'.length
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// -------------------------------------------------------------------------
|
|
434
|
+
// Caret restoration when erasing the last indent on a clipped (collapsed-window) line
|
|
435
|
+
// -------------------------------------------------------------------------
|
|
436
|
+
it('restores the caret when backspacing the last indent of a blank clipped line (minColumn)', async () => {
|
|
437
|
+
// Simulate a collapsed window: indentation clipped to minColumn=2, the
|
|
438
|
+
// visible region is rows 1..3. Line 2 is a blank line with exactly 2 spaces.
|
|
439
|
+
const initial = 'function foo() {\n const a = 1;\n \n return a;\n}\n';
|
|
440
|
+
const {
|
|
441
|
+
element
|
|
442
|
+
} = await setupEditor(initial, {
|
|
443
|
+
indentation: 2,
|
|
444
|
+
caretSelector: '.line',
|
|
445
|
+
minColumn: 2,
|
|
446
|
+
minRow: 1,
|
|
447
|
+
maxRow: 3,
|
|
448
|
+
onBoundary: vi.fn()
|
|
449
|
+
}, {
|
|
450
|
+
scroll: true
|
|
451
|
+
});
|
|
452
|
+
// caret at end of the blank line 2 (column 2 == minColumn)
|
|
453
|
+
const offset = 'function foo() {\n const a = 1;\n '.length;
|
|
454
|
+
await placeCaret(element, offset);
|
|
455
|
+
const before = caretLineColumn(element);
|
|
456
|
+
await userEvent.keyboard('{Backspace}');
|
|
457
|
+
await settle();
|
|
458
|
+
const after = caretLineColumn(element);
|
|
459
|
+
// Assert the (arguably correct) behavior: stay on the same line, now empty.
|
|
460
|
+
expect(after.line).toBe(before.line);
|
|
461
|
+
});
|
|
462
|
+
it('restores the caret when backspacing an indent on a content line in a clipped gutter (minColumn)', async () => {
|
|
463
|
+
const initial = 'function foo() {\n const a = 1;\n}\n';
|
|
464
|
+
const {
|
|
465
|
+
element
|
|
466
|
+
} = await setupEditor(initial, {
|
|
467
|
+
indentation: 2,
|
|
468
|
+
caretSelector: '.line',
|
|
469
|
+
minColumn: 2,
|
|
470
|
+
minRow: 1,
|
|
471
|
+
maxRow: 1,
|
|
472
|
+
onBoundary: vi.fn()
|
|
473
|
+
}, {
|
|
474
|
+
scroll: true
|
|
475
|
+
});
|
|
476
|
+
// caret right after the 2-space indent on line 1 (` const a = 1;`)
|
|
477
|
+
const offset = 'function foo() {\n '.length;
|
|
478
|
+
await placeCaret(element, offset);
|
|
479
|
+
await userEvent.keyboard('{Backspace}');
|
|
480
|
+
await settle();
|
|
481
|
+
const after = caretLineColumn(element);
|
|
482
|
+
expect(after).toBeTruthy();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// -------------------------------------------------------------------------
|
|
486
|
+
// ArrowUp at the visible top fires onBoundary (scroll anchor)
|
|
487
|
+
// -------------------------------------------------------------------------
|
|
488
|
+
it('fires onBoundary on ArrowUp at the first row and ArrowDown at the last row', async () => {
|
|
489
|
+
const onBoundary = vi.fn();
|
|
490
|
+
const initial = 'line0\nline1\nline2\nline3\nline4\n';
|
|
491
|
+
const {
|
|
492
|
+
element
|
|
493
|
+
} = await setupEditor(initial, {
|
|
494
|
+
indentation: 2,
|
|
495
|
+
caretSelector: '.line',
|
|
496
|
+
minRow: 2,
|
|
497
|
+
maxRow: 3,
|
|
498
|
+
onBoundary
|
|
499
|
+
}, {
|
|
500
|
+
scroll: true
|
|
501
|
+
});
|
|
502
|
+
// Caret on line 2 (minRow), then ArrowUp.
|
|
503
|
+
const upOffset = 'line0\nline1\n'.length + 2;
|
|
504
|
+
await placeCaret(element, upOffset);
|
|
505
|
+
await userEvent.keyboard('{ArrowUp}');
|
|
506
|
+
await settle();
|
|
507
|
+
const upCalls = onBoundary.mock.calls.length;
|
|
508
|
+
|
|
509
|
+
// Caret on line 3 (maxRow), then ArrowDown.
|
|
510
|
+
const downOffset = 'line0\nline1\nline2\n'.length + 2;
|
|
511
|
+
await placeCaret(element, downOffset);
|
|
512
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
513
|
+
await settle();
|
|
514
|
+
const downCalls = onBoundary.mock.calls.length - upCalls;
|
|
515
|
+
expect(upCalls).toBeGreaterThan(0); // ArrowUp must fire the boundary
|
|
516
|
+
expect(downCalls).toBeGreaterThan(0); // ArrowDown must fire the boundary
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// -------------------------------------------------------------------------
|
|
520
|
+
// Transient DOM vs committed source during an async re-highlight: backspacing the
|
|
521
|
+
// last indent collapses the line during the worker round-trip, before the committed
|
|
522
|
+
// source catches up.
|
|
523
|
+
// -------------------------------------------------------------------------
|
|
524
|
+
it('keeps the transient DOM consistent with the committed source when async-backspacing the last indent', async () => {
|
|
525
|
+
const {
|
|
526
|
+
preParse,
|
|
527
|
+
resolvePending
|
|
528
|
+
} = deferredPreParse();
|
|
529
|
+
const initial = 'function foo() {\n const a = 1;\n \n return a;\n}\n';
|
|
530
|
+
const {
|
|
531
|
+
element
|
|
532
|
+
} = await setupEditor(initial, {
|
|
533
|
+
indentation: 2,
|
|
534
|
+
caretSelector: '.line',
|
|
535
|
+
minColumn: 2,
|
|
536
|
+
minRow: 1,
|
|
537
|
+
maxRow: 3,
|
|
538
|
+
onBoundary: vi.fn(),
|
|
539
|
+
preParse
|
|
540
|
+
}, {
|
|
541
|
+
scroll: true
|
|
542
|
+
});
|
|
543
|
+
const offset = 'function foo() {\n const a = 1;\n '.length;
|
|
544
|
+
await placeCaret(element, offset);
|
|
545
|
+
|
|
546
|
+
// Compact structural signature: the frame's direct children, each as
|
|
547
|
+
// either `LINE(<text>)` or `TEXT(<text>)`, so we can see the mangled
|
|
548
|
+
// transient structure without vitest truncating a long innerHTML string.
|
|
549
|
+
const signature = () => {
|
|
550
|
+
const frame = element.querySelector('.frame');
|
|
551
|
+
return Array.from(frame.childNodes).map(node => {
|
|
552
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
553
|
+
return `TEXT(${JSON.stringify(node.textContent)})`;
|
|
554
|
+
}
|
|
555
|
+
const childEl = node;
|
|
556
|
+
if (childEl.classList?.contains('line')) {
|
|
557
|
+
return `LINE(${JSON.stringify(childEl.textContent)})`;
|
|
558
|
+
}
|
|
559
|
+
return `OTHER(${childEl.tagName})`;
|
|
560
|
+
});
|
|
561
|
+
};
|
|
562
|
+
await userEvent.keyboard('{Backspace}');
|
|
563
|
+
await settle();
|
|
564
|
+
|
|
565
|
+
// DURING the async gap (preParse not yet resolved): what does the DOM show?
|
|
566
|
+
const transientSig = signature();
|
|
567
|
+
await resolvePending();
|
|
568
|
+
await settle();
|
|
569
|
+
const finalSig = signature();
|
|
570
|
+
// The transient DOM the user sees during the async worker round-trip must already
|
|
571
|
+
// be structurally consistent with the final committed result — no wrong intermediate
|
|
572
|
+
// state (no dangling empty `.line` span, no dropped gap newline), so the signatures match.
|
|
573
|
+
expect(transientSig).toEqual(finalSig);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// -------------------------------------------------------------------------
|
|
577
|
+
// ArrowUp navigation across consecutive empty lines
|
|
578
|
+
// -------------------------------------------------------------------------
|
|
579
|
+
it('moves ArrowUp onto an empty line first, then the line above', async () => {
|
|
580
|
+
const initial = 'aaa\n\nbbb\nccc\n';
|
|
581
|
+
const {
|
|
582
|
+
element
|
|
583
|
+
} = await setupEditor(initial, {
|
|
584
|
+
indentation: 2,
|
|
585
|
+
caretSelector: '.line'
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// Start on "ccc" (line 3), column 1.
|
|
589
|
+
const start = 'aaa\n\nbbb\n'.length + 1;
|
|
590
|
+
await placeCaret(element, start);
|
|
591
|
+
await userEvent.keyboard('{ArrowUp}');
|
|
592
|
+
await settle();
|
|
593
|
+
const step1 = caretLineColumn(element); // expect line 2 ("bbb")
|
|
594
|
+
|
|
595
|
+
await userEvent.keyboard('{ArrowUp}');
|
|
596
|
+
await settle();
|
|
597
|
+
const step2 = caretLineColumn(element); // expect line 1 (empty)
|
|
598
|
+
|
|
599
|
+
await userEvent.keyboard('{ArrowUp}');
|
|
600
|
+
await settle();
|
|
601
|
+
const step3 = caretLineColumn(element); // expect line 0 ("aaa")
|
|
602
|
+
|
|
603
|
+
// At the engine level the empty line IS reachable and each step lands on
|
|
604
|
+
// the expected row (this passes in all 3 browsers). If the "weird" ArrowUp
|
|
605
|
+
// behavior shows up in the real docs, it is not in this engine path —
|
|
606
|
+
// suspect the real frame/visibleFrames render or layout, not navigation.
|
|
607
|
+
expect(step1.line).toBe(2);
|
|
608
|
+
expect(step2.line).toBe(1); // the empty line — reachable, not skipped
|
|
609
|
+
expect(step3.line).toBe(0);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// -------------------------------------------------------------------------
|
|
613
|
+
// New bug: TWO consecutive empty lines — ArrowUp skips both at once.
|
|
614
|
+
// -------------------------------------------------------------------------
|
|
615
|
+
it('stops ArrowUp on each of two consecutive empty lines (does not skip both)', async () => {
|
|
616
|
+
// Lines: 0 "aaa", 1 "" , 2 "", 3 "bbb", 4 "ccc".
|
|
617
|
+
const initial = 'aaa\n\n\nbbb\nccc\n';
|
|
618
|
+
const {
|
|
619
|
+
element
|
|
620
|
+
} = await setupEditor(initial, {
|
|
621
|
+
indentation: 2,
|
|
622
|
+
caretSelector: '.line'
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// Start on "bbb" (line 3), column 1.
|
|
626
|
+
const start = 'aaa\n\n\n'.length + 1;
|
|
627
|
+
await placeCaret(element, start);
|
|
628
|
+
await userEvent.keyboard('{ArrowUp}');
|
|
629
|
+
await settle();
|
|
630
|
+
const step1 = caretLineColumn(element); // expect line 2 (second empty line)
|
|
631
|
+
|
|
632
|
+
await userEvent.keyboard('{ArrowUp}');
|
|
633
|
+
await settle();
|
|
634
|
+
const step2 = caretLineColumn(element); // expect line 1 (first empty line)
|
|
635
|
+
|
|
636
|
+
await userEvent.keyboard('{ArrowUp}');
|
|
637
|
+
await settle();
|
|
638
|
+
const step3 = caretLineColumn(element); // expect line 0 ("aaa")
|
|
639
|
+
|
|
640
|
+
// DESIRED: ArrowUp stops on EACH line, including the zero-height empty ones.
|
|
641
|
+
// The bug skips both empty lines, so a single ArrowUp from "bbb" jumps
|
|
642
|
+
// straight to "aaa" (step1.line === 0).
|
|
643
|
+
const ctx = JSON.stringify({
|
|
644
|
+
step1,
|
|
645
|
+
step2,
|
|
646
|
+
step3
|
|
647
|
+
});
|
|
648
|
+
expect(step1.line, ctx).toBe(2); // second empty line
|
|
649
|
+
expect(step2.line, ctx).toBe(1); // first empty line
|
|
650
|
+
expect(step3.line, ctx).toBe(0); // "aaa"
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// -------------------------------------------------------------------------
|
|
654
|
+
// A BACKWARD Shift+Arrow selection keeps its focus at the top across a
|
|
655
|
+
// host re-render (the restore must not flip the focus to the bottom end).
|
|
656
|
+
// -------------------------------------------------------------------------
|
|
657
|
+
it('preserves a backward Shift+ArrowUp selection across a re-render, focus still at the top', async () => {
|
|
658
|
+
const initial = 'aaa\nbbb\nccc\nddd\n';
|
|
659
|
+
const {
|
|
660
|
+
handle,
|
|
661
|
+
element
|
|
662
|
+
} = await setupEditor(initial, {
|
|
663
|
+
indentation: 2,
|
|
664
|
+
caretSelector: '.line'
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// Reads the moving end (focus) and the fixed end (anchor) of the live
|
|
668
|
+
// selection as (line, column), so we can assert the selection DIRECTION —
|
|
669
|
+
// `caretLineColumn` only reports the forward-normalized range start.
|
|
670
|
+
const endPoint = (node, offset) => {
|
|
671
|
+
const until = document.createRange();
|
|
672
|
+
until.setStart(element, 0);
|
|
673
|
+
until.setEnd(node, offset);
|
|
674
|
+
const lines = until.toString().split('\n');
|
|
675
|
+
return {
|
|
676
|
+
line: lines.length - 1,
|
|
677
|
+
column: lines[lines.length - 1].length
|
|
678
|
+
};
|
|
679
|
+
};
|
|
680
|
+
const focusPoint = () => {
|
|
681
|
+
const sel = window.getSelection();
|
|
682
|
+
return endPoint(sel.focusNode, sel.focusOffset);
|
|
683
|
+
};
|
|
684
|
+
const anchorPoint = () => {
|
|
685
|
+
const sel = window.getSelection();
|
|
686
|
+
return endPoint(sel.anchorNode, sel.anchorOffset);
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// Start collapsed on "ddd" (line 3), column 0, then grow the selection
|
|
690
|
+
// UPWARD twice so the focus is above the anchor (a backward range).
|
|
691
|
+
await placeCaret(element, 'aaa\nbbb\nccc\n'.length);
|
|
692
|
+
await userEvent.keyboard('{Shift>}{ArrowUp}{ArrowUp}{/Shift}');
|
|
693
|
+
await settle();
|
|
694
|
+
|
|
695
|
+
// Anchor stays on line 3; the focus has climbed two lines up to line 1.
|
|
696
|
+
expect(anchorPoint().line, 'anchor before re-render').toBe(3);
|
|
697
|
+
expect(focusPoint().line, 'focus before re-render').toBe(1);
|
|
698
|
+
|
|
699
|
+
// An idle host re-render (e.g. an async re-highlight committing) re-runs the
|
|
700
|
+
// engine's caret/selection restore. The backward direction must survive it.
|
|
701
|
+
act(() => {
|
|
702
|
+
handle.rerender();
|
|
703
|
+
});
|
|
704
|
+
await settle();
|
|
705
|
+
expect(anchorPoint().line, 'anchor after re-render').toBe(3);
|
|
706
|
+
// The bug: the restore rebuilt a forward range, flipping the focus to the
|
|
707
|
+
// bottom (line 3). With the fix the focus stays at the top (line 1).
|
|
708
|
+
expect(focusPoint().line, 'focus after re-render').toBe(1);
|
|
709
|
+
|
|
710
|
+
// And the next Shift+ArrowUp must keep extending from the TOP — landing the
|
|
711
|
+
// focus on line 0 — rather than collapsing the selection from the bottom.
|
|
712
|
+
await userEvent.keyboard('{Shift>}{ArrowUp}{/Shift}');
|
|
713
|
+
await settle();
|
|
714
|
+
expect(focusPoint().line, 'focus after a third Shift+ArrowUp').toBe(0);
|
|
715
|
+
expect(anchorPoint().line, 'anchor unchanged after third Shift+ArrowUp').toBe(3);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// -------------------------------------------------------------------------
|
|
719
|
+
// Focus retention on the first keystroke (async preParse path)
|
|
720
|
+
// -------------------------------------------------------------------------
|
|
721
|
+
it('keeps focus after the first keystroke through an async re-highlight', async () => {
|
|
722
|
+
const {
|
|
723
|
+
element
|
|
724
|
+
} = await setupEditor('hello\n', {
|
|
725
|
+
indentation: 2,
|
|
726
|
+
caretSelector: '.line',
|
|
727
|
+
preParse: asyncPreParse()
|
|
728
|
+
});
|
|
729
|
+
await placeCaret(element, 5);
|
|
730
|
+
expect(document.activeElement).toBe(element);
|
|
731
|
+
await userEvent.keyboard('!');
|
|
732
|
+
await settle();
|
|
733
|
+
await settle();
|
|
734
|
+
expect(document.activeElement).toBe(element);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// -------------------------------------------------------------------------
|
|
738
|
+
// Single-bound paging: PageDown engages only when `maxRow` is set and PageUp
|
|
739
|
+
// only when `minRow` is set — mirroring ArrowDown (needs `maxRow`) / ArrowUp
|
|
740
|
+
// (needs `minRow`). With only the OPPOSITE bound present the page key has no
|
|
741
|
+
// fold in its direction, so it falls through to native and must NOT fire
|
|
742
|
+
// `onBoundary`. Latent through `Pre` (which always supplies both bounds), but
|
|
743
|
+
// `Options.minRow`/`maxRow` are independently optional.
|
|
744
|
+
// -------------------------------------------------------------------------
|
|
745
|
+
it('pages to the fold only for the bound in its own direction', async () => {
|
|
746
|
+
const initial = 'line0\nline1\nline2\nline3\nline4\n';
|
|
747
|
+
|
|
748
|
+
// Only `maxRow` (a bottom fold, no top fold): PageDown expands, PageUp is native.
|
|
749
|
+
{
|
|
750
|
+
const onBoundary = vi.fn();
|
|
751
|
+
const {
|
|
752
|
+
element
|
|
753
|
+
} = await setupEditor(initial, {
|
|
754
|
+
indentation: 2,
|
|
755
|
+
caretSelector: '.line',
|
|
756
|
+
maxRow: 2,
|
|
757
|
+
onBoundary
|
|
758
|
+
}, {
|
|
759
|
+
scroll: true
|
|
760
|
+
});
|
|
761
|
+
await placeCaret(element, 'line0\n'.length); // line 1, inside the window
|
|
762
|
+
await userEvent.keyboard('{PageUp}');
|
|
763
|
+
await settle();
|
|
764
|
+
expect(onBoundary, 'PageUp with no minRow stays native').not.toHaveBeenCalled();
|
|
765
|
+
await userEvent.keyboard('{PageDown}');
|
|
766
|
+
await settle();
|
|
767
|
+
expect(onBoundary, 'PageDown expands the bottom fold').toHaveBeenCalledTimes(1);
|
|
768
|
+
expect(caretLineColumn(element).line, 'caret jumped to the bottom edge').toBe(2);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Only `minRow` (a top fold, no bottom fold): PageUp expands, PageDown is native.
|
|
772
|
+
{
|
|
773
|
+
const onBoundary = vi.fn();
|
|
774
|
+
const {
|
|
775
|
+
element
|
|
776
|
+
} = await setupEditor(initial, {
|
|
777
|
+
indentation: 2,
|
|
778
|
+
caretSelector: '.line',
|
|
779
|
+
minRow: 2,
|
|
780
|
+
onBoundary
|
|
781
|
+
}, {
|
|
782
|
+
scroll: true
|
|
783
|
+
});
|
|
784
|
+
await placeCaret(element, 'line0\nline1\nline2\nline3\n'.length); // line 4, inside the window
|
|
785
|
+
await userEvent.keyboard('{PageDown}');
|
|
786
|
+
await settle();
|
|
787
|
+
expect(onBoundary, 'PageDown with no maxRow stays native').not.toHaveBeenCalled();
|
|
788
|
+
await userEvent.keyboard('{PageUp}');
|
|
789
|
+
await settle();
|
|
790
|
+
expect(onBoundary, 'PageUp expands the top fold').toHaveBeenCalledTimes(1);
|
|
791
|
+
expect(caretLineColumn(element).line, 'caret jumped to the top edge').toBe(2);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// ---------------------------------------------------------------------------
|
|
797
|
+
// deletedFromLineStart: whole-line removals only, not in-place collapses
|
|
798
|
+
// ---------------------------------------------------------------------------
|
|
799
|
+
// A selection delete reports `deletedFromLineStart` so the controlled
|
|
800
|
+
// comment/highlight map drops its anchor one line — the post-delete caret sits on
|
|
801
|
+
// a line that shifted up from below the deletion. That must be limited to
|
|
802
|
+
// deletions that removed WHOLE lines. A selection that ends mid-line collapses the
|
|
803
|
+
// spanned lines INTO the first line, which survives (emptied) under the caret;
|
|
804
|
+
// reporting the flag there drags a marker on that surviving line one line too high
|
|
805
|
+
// (the live-editor "the highlight shifts up instead of being deleted" bug). The
|
|
806
|
+
// source stands in for an @highlight block on lines 4-6 with blank padding (3, 7).
|
|
807
|
+
describe('useEditable — selection delete reports deletedFromLineStart only for whole-line removals', () => {
|
|
808
|
+
const SRC = 'const a = 1;\nconst b = 2;\n\ndoThing();\ndoOther();\ndoLast();\n\nconst c = 3;\n';
|
|
809
|
+
const lastPosition = handle => handle.onChange.mock.calls.at(-1)?.[1];
|
|
810
|
+
it('reports the flag when a column-0 selection removes whole lines', async () => {
|
|
811
|
+
const {
|
|
812
|
+
handle,
|
|
813
|
+
element
|
|
814
|
+
} = await setupEditor(SRC, {
|
|
815
|
+
indentation: 2,
|
|
816
|
+
caretSelector: '.line'
|
|
817
|
+
});
|
|
818
|
+
// Column 0 of the blank line 3 → column 0 of the blank line 7 (a line boundary):
|
|
819
|
+
// whole lines 3-6 are removed and line 7 shifts up under the caret.
|
|
820
|
+
await placeCaret(element, 26);
|
|
821
|
+
await userEvent.keyboard('{Shift>}{ArrowDown}{ArrowDown}{ArrowDown}{ArrowDown}{/Shift}');
|
|
822
|
+
await settle();
|
|
823
|
+
await userEvent.keyboard('{Backspace}');
|
|
824
|
+
await settle();
|
|
825
|
+
expect(handle.getSource()).toBe('const a = 1;\nconst b = 2;\n\nconst c = 3;\n');
|
|
826
|
+
expect(lastPosition(handle)?.deletedFromLineStart).toBe(true);
|
|
827
|
+
});
|
|
828
|
+
it('reports the flag when a column-0 selection stops on the region’s exclusive -end line', async () => {
|
|
829
|
+
const {
|
|
830
|
+
handle,
|
|
831
|
+
element
|
|
832
|
+
} = await setupEditor(SRC, {
|
|
833
|
+
indentation: 2,
|
|
834
|
+
caretSelector: '.line'
|
|
835
|
+
});
|
|
836
|
+
// Column 0 of the blank line 3 → column 0 of line 6 (still a line boundary), one
|
|
837
|
+
// ArrowDown short of scenario A. A range's @highlight-end is EXCLUSIVE — it sits
|
|
838
|
+
// on the line just below the last highlighted line — so selecting "from the line
|
|
839
|
+
// above through the last newline of the region" lands here, deleting the whole
|
|
840
|
+
// highlighted body (lines 4-5) while doLast() (the -end line) survives. This is
|
|
841
|
+
// still a whole-line removal, so the flag holds; the matching comment-map case is
|
|
842
|
+
// `useSourceEditing`'s "removes the highlight when the selection stops on the
|
|
843
|
+
// region’s exclusive -end line".
|
|
844
|
+
await placeCaret(element, 26);
|
|
845
|
+
await userEvent.keyboard('{Shift>}{ArrowDown}{ArrowDown}{ArrowDown}{/Shift}');
|
|
846
|
+
await settle();
|
|
847
|
+
await userEvent.keyboard('{Backspace}');
|
|
848
|
+
await settle();
|
|
849
|
+
expect(handle.getSource()).toBe('const a = 1;\nconst b = 2;\ndoLast();\n\nconst c = 3;\n');
|
|
850
|
+
expect(lastPosition(handle)?.deletedFromLineStart).toBe(true);
|
|
851
|
+
});
|
|
852
|
+
it('does NOT report the flag when the selection ends mid-line and collapses in place', async () => {
|
|
853
|
+
const {
|
|
854
|
+
handle,
|
|
855
|
+
element
|
|
856
|
+
} = await setupEditor(SRC, {
|
|
857
|
+
indentation: 2,
|
|
858
|
+
caretSelector: '.line'
|
|
859
|
+
});
|
|
860
|
+
// Column 0 of line 4 → the END of line 6 (mid-line, NOT a line boundary): the three
|
|
861
|
+
// region lines collapse into one empty line that survives under the caret.
|
|
862
|
+
await placeCaret(element, 27);
|
|
863
|
+
await userEvent.keyboard('{Shift>}{ArrowDown}{ArrowDown}{End}{/Shift}');
|
|
864
|
+
await settle();
|
|
865
|
+
await userEvent.keyboard('{Backspace}');
|
|
866
|
+
await settle();
|
|
867
|
+
expect(handle.getSource()).toBe('const a = 1;\nconst b = 2;\n\n\n\nconst c = 3;\n');
|
|
868
|
+
expect(lastPosition(handle)?.deletedFromLineStart).not.toBe(true);
|
|
869
|
+
});
|
|
870
|
+
});
|