markstream-svelte 0.0.6 → 0.1.0-beta.2

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.
@@ -1,11 +1,11 @@
1
1
  <script lang="ts">
2
- import type { CodeBlockMonacoOptions, CodeBlockMonacoTheme } from '../types/monaco'
2
+ import type { CodeBlockOptions, CodeBlockTheme, CodeBlockThemeProp, CodeBlockThemes } from '../types/codeBlock'
3
3
  import type { SvelteRenderableNode, SvelteRenderContext } from './shared/node-helpers'
4
4
  import { onDestroy, onMount, tick } from 'svelte'
5
5
  import { useSafeI18n } from '../i18n/useSafeI18n'
6
- import { getUseMonaco } from '../optional/monaco'
6
+ import { getStreamDiffsRuntime } from '../optional/streamDiffs'
7
7
  import { hideTooltip, showTooltipForAnchor } from '../tooltip/singletonTooltip'
8
- import { getLanguageIcon, isLikelyIncompleteLanguageIdentifier, languageMap, normalizeLanguageIdentifier, resolveMonacoLanguageId } from '../utils/languageIcon'
8
+ import { getLanguageIcon, isLikelyIncompleteLanguageIdentifier, languageMap, normalizeLanguageIdentifier, resolveLanguageId } from '../utils/languageIcon'
9
9
  import HtmlPreviewFrame from './HtmlPreviewFrame.svelte'
10
10
  import PreCodeNode from './PreCodeNode.svelte'
11
11
  import { copyTextToClipboard, resolveCssSize } from './shared/rich-block-helpers'
@@ -17,10 +17,11 @@
17
17
  isDark?: boolean | undefined
18
18
  loading?: boolean | undefined
19
19
  stream?: boolean | undefined
20
- darkTheme?: CodeBlockMonacoTheme | undefined
21
- lightTheme?: CodeBlockMonacoTheme | undefined
22
- themes?: CodeBlockMonacoTheme[] | undefined
23
- monacoOptions?: CodeBlockMonacoOptions | undefined
20
+ codeBlockOptions?: CodeBlockOptions | undefined
21
+ theme?: CodeBlockThemeProp | undefined
22
+ darkTheme?: CodeBlockTheme | undefined
23
+ lightTheme?: CodeBlockTheme | undefined
24
+ themes?: CodeBlockThemes | undefined
24
25
  minWidth?: string | number | undefined
25
26
  maxWidth?: string | number | undefined
26
27
  isShowPreview?: boolean
@@ -42,10 +43,11 @@
42
43
  isDark = undefined,
43
44
  loading = undefined,
44
45
  stream = undefined,
46
+ codeBlockOptions = undefined,
47
+ theme = undefined,
45
48
  darkTheme = undefined,
46
49
  lightTheme = undefined,
47
50
  themes = undefined,
48
- monacoOptions = undefined,
49
51
  minWidth = undefined,
50
52
  maxWidth = undefined,
51
53
  isShowPreview = true,
@@ -56,24 +58,12 @@
56
58
  showPreviewButton = true,
57
59
  showCollapseButton = true,
58
60
  showFontSizeButtons = true,
59
- showLineNumbers = true,
61
+ showLineNumbers = undefined,
60
62
  htmlPreviewAllowScripts = false,
61
63
  htmlPreviewSandbox = undefined
62
64
  }: Props = $props()
63
65
 
64
66
  const { t } = useSafeI18n()
65
- const defaultDiffHideUnchangedRegions = Object.freeze({
66
- enabled: true,
67
- contextLineCount: 2,
68
- minimumLineCount: 4,
69
- revealLineCount: 5,
70
- })
71
- const disabledDiffHideUnchangedRegions = Object.freeze({
72
- enabled: false,
73
- contextLineCount: 0,
74
- minimumLineCount: Number.POSITIVE_INFINITY,
75
- revealLineCount: 0,
76
- })
77
67
  const streamingLanguageTokens = ['javascript', 'plaintext', 'shellscript', 'typescript']
78
68
  const defaultPreFallbackFontFamily = '"SF Mono", Monaco, Consolas, "Ubuntu Mono", "Liberation Mono", "Courier New", monospace'
79
69
 
@@ -127,8 +117,8 @@
127
117
 
128
118
  let editorHost: HTMLDivElement | null = $state(null)
129
119
  let helpers: any = $state(null)
130
- let runtimeMonacoOptions: Record<string, any> | null = $state(null)
131
- let ensureMonacoPromise: Promise<void> | null = $state(null)
120
+ let runtimeOptions: Record<string, any> | null = $state(null)
121
+ let ensureRuntimePromise: Promise<void> | null = $state(null)
132
122
  let editorReady = $state(false)
133
123
  let useFallback = $state(false)
134
124
  let fallbackLanguage = $state('')
@@ -150,6 +140,7 @@
150
140
  let lastLayoutWidth: number | null = $state(null)
151
141
  let lastLayoutHeight: number | null = $state(null)
152
142
  let lastThemeRequest = $state('')
143
+ let lastRuntimeInstallationConfig: unknown
153
144
  let languageRetryTimer: ReturnType<typeof setTimeout> | null = $state(null)
154
145
  let loadingSettledRefreshPromise: Promise<void> | null = $state(null)
155
146
  let loadingSettledRefreshTimer: ReturnType<typeof setTimeout> | null = $state(null)
@@ -160,7 +151,7 @@
160
151
 
161
152
  let rawLanguage = $derived(getString((node as any)?.language).trim())
162
153
  let canonicalLanguage = $derived(normalizeLanguageIdentifier(rawLanguage))
163
- let monacoLanguage = $derived(resolveMonacoLanguageId(canonicalLanguage || rawLanguage || 'plaintext'))
154
+ let runtimeLanguage = $derived(resolveLanguageId(canonicalLanguage || rawLanguage || 'plaintext'))
164
155
  let code = $derived(getResolvedCode(node))
165
156
  let diff = $derived(Boolean((node as any)?.diff))
166
157
  let originalCode = $derived(getString((node as any)?.originalCode))
@@ -170,15 +161,21 @@
170
161
  let resolvedStream = $derived(stream ?? context?.codeBlockStream ?? true)
171
162
  let resolvedIsDark = $derived(isDark ?? context?.isDark ?? false)
172
163
  let resolvedThemes = $derived(context?.codeBlockThemes)
173
- let mergedMonacoOptions = $derived({ ...(resolvedThemes?.monacoOptions || {}), ...(monacoOptions || {}) })
174
- let resolvedMonacoOptions = $derived(buildResolvedMonacoOptions())
175
- let requestedTheme = $derived(getThemeName(
176
- resolvedIsDark
177
- ? darkTheme ?? resolvedThemes?.darkTheme
178
- : lightTheme ?? resolvedThemes?.lightTheme,
179
- resolvedIsDark ? 'vitesse-dark' : 'vitesse-light',
180
- ))
181
- let defaultCodeFontSize = $derived(Number(mergedMonacoOptions.fontSize) || 12)
164
+ let resolvedCodeBlockOptions = $derived(codeBlockOptions ?? context?.codeBlockOptions)
165
+ let effectiveShowLineNumbers = $derived(showLineNumbers ?? resolvedCodeBlockOptions?.disableLineNumbers !== true)
166
+ let runtimeInstallationConfig = $derived.by(() => {
167
+ const parseDiffOptions = resolvedCodeBlockOptions?.parseDiffOptions
168
+ return {
169
+ options: { ...(resolvedCodeBlockOptions ?? {}) },
170
+ parseDiffOptions: parseDiffOptions && typeof parseDiffOptions === 'object'
171
+ ? { ...parseDiffOptions }
172
+ : parseDiffOptions,
173
+ showLineNumbers: effectiveShowLineNumbers,
174
+ }
175
+ })
176
+ let resolvedRuntimeOptions = $derived(buildResolvedRuntimeOptions())
177
+ let requestedTheme = $derived(resolveRequestedTheme())
178
+ let defaultCodeFontSize = $derived(readPositiveMetric(resolvedCodeBlockOptions?.fontSize) ?? 12)
182
179
  let minWidthValue = $derived(resolveCssSize(minWidth ?? resolvedThemes?.minWidth))
183
180
  let maxWidthValue = $derived(resolveCssSize(maxWidth ?? resolvedThemes?.maxWidth))
184
181
  let containerStyle = $derived([
@@ -200,8 +197,8 @@
200
197
  } as SvelteRenderableNode)
201
198
  let preFallbackStyle = $derived(buildPreFallbackStyle())
202
199
  let settledRefreshSignature = $derived(diff
203
- ? `${monacoLanguage}\0${originalCode}\0${updatedCode || code}`
204
- : `${monacoLanguage}\0${code}`)
200
+ ? `${runtimeLanguage}\0${originalCode}\0${updatedCode || code}`
201
+ : `${runtimeLanguage}\0${code}`)
205
202
 
206
203
  $effect(() => {
207
204
  if (useFallback && fallbackLanguage && rawLanguage !== fallbackLanguage && isLikelyIncompleteLanguageIdentifier(fallbackLanguage)) {
@@ -235,13 +232,21 @@
235
232
  void code
236
233
  void originalCode
237
234
  void updatedCode
238
- void monacoLanguage
235
+ void runtimeLanguage
239
236
  void requestedTheme
240
- void resolvedMonacoOptions
237
+ void resolvedRuntimeOptions
238
+ void resolvedCodeBlockOptions
239
+ void runtimeInstallationConfig
241
240
  void codeFontSize
242
241
  void expanded
243
- if (mounted)
242
+ if (mounted) {
243
+ if (lastRuntimeInstallationConfig !== runtimeInstallationConfig) {
244
+ lastRuntimeInstallationConfig = runtimeInstallationConfig
245
+ codeFontSize = defaultCodeFontSize
246
+ cleanupEditor()
247
+ }
244
248
  void syncEditor()
249
+ }
245
250
  })
246
251
 
247
252
  onMount(() => {
@@ -279,29 +284,30 @@
279
284
  }
280
285
 
281
286
  function getCodeLineHeight() {
282
- return readPositiveMetric(mergedMonacoOptions.lineHeight)
287
+ return readPositiveMetric(resolvedCodeBlockOptions?.lineHeight)
283
288
  ?? (codeFontSize === 12 ? 18 : Math.max(12, Math.round(codeFontSize * 1.5)))
284
289
  }
285
290
 
286
291
  function getCodePadding() {
287
- const padding = mergedMonacoOptions.padding as Record<string, unknown> | undefined
292
+ const padding = resolvedCodeBlockOptions?.padding
288
293
  const defaultPadding = diff ? 0 : 8
294
+ const value = readNonNegativeMetric(padding) ?? defaultPadding
289
295
  return {
290
- top: readNonNegativeMetric(padding?.top) ?? defaultPadding,
291
- bottom: readNonNegativeMetric(padding?.bottom) ?? defaultPadding,
296
+ top: value,
297
+ bottom: value,
292
298
  }
293
299
  }
294
300
 
295
301
  function getCodeFontFamily() {
296
- return typeof mergedMonacoOptions.fontFamily === 'string' && mergedMonacoOptions.fontFamily.trim()
297
- ? mergedMonacoOptions.fontFamily.trim()
302
+ return typeof resolvedCodeBlockOptions?.fontFamily === 'string' && resolvedCodeBlockOptions.fontFamily.trim()
303
+ ? resolvedCodeBlockOptions.fontFamily.trim()
298
304
  : defaultPreFallbackFontFamily
299
305
  }
300
306
 
301
307
  function buildPreFallbackStyle() {
302
308
  const padding = getCodePadding()
303
309
  const fontFamily = getCodeFontFamily()
304
- const tabSize = readPositiveMetric(mergedMonacoOptions.tabSize) ?? 4
310
+ const tabSize = readPositiveMetric(resolvedCodeBlockOptions?.tabSize) ?? 4
305
311
  const lineHeight = getCodeLineHeight()
306
312
  return [
307
313
  `--markstream-code-font-family: ${fontFamily}`,
@@ -317,181 +323,145 @@
317
323
  `padding-bottom: ${padding.bottom}px`,
318
324
  `padding-left: var(--markstream-code-padding-left, 52px)`,
319
325
  `tab-size: ${tabSize}`,
326
+ `max-height: ${getMaxHeightValue()}px`,
327
+ 'overflow: auto',
328
+ `white-space: ${resolvedCodeBlockOptions?.overflow === 'scroll' ? 'pre' : 'pre-wrap'}`,
320
329
  ].join('; ')
321
330
  }
322
331
 
323
- function getThemeName(theme: CodeBlockMonacoTheme | undefined, fallback: string) {
332
+ function isThemePair(value: unknown): value is { dark: string, light: string } {
333
+ return !!value && typeof value === 'object' && typeof (value as any).dark === 'string' && typeof (value as any).light === 'string'
334
+ }
335
+
336
+ function resolveRequestedTheme() {
324
337
  if (typeof theme === 'string' && theme)
325
338
  return theme
326
- if (theme && typeof theme === 'object' && typeof (theme as any).name === 'string')
327
- return String((theme as any).name)
328
- return fallback
339
+ if (isThemePair(theme))
340
+ return resolvedIsDark ? theme.dark : theme.light
341
+ const directTheme = resolvedIsDark ? darkTheme : lightTheme
342
+ if (directTheme)
343
+ return directTheme
344
+ if (themes)
345
+ return resolvedIsDark ? themes[0] : themes[1]
346
+ const contextTheme = resolvedIsDark ? resolvedThemes?.darkTheme : resolvedThemes?.lightTheme
347
+ if (contextTheme)
348
+ return contextTheme
349
+ if (resolvedThemes?.themes)
350
+ return resolvedIsDark ? resolvedThemes.themes[0] : resolvedThemes.themes[1]
351
+ return resolvedIsDark ? 'vitesse-dark' : 'vitesse-light'
329
352
  }
330
353
 
331
- function buildThemeList() {
332
- const list: CodeBlockMonacoTheme[] = ['vitesse-dark', 'vitesse-light']
333
- const add = (item: CodeBlockMonacoTheme | undefined) => {
334
- if (item)
335
- list.push(item)
336
- }
337
- add(darkTheme ?? resolvedThemes?.darkTheme)
338
- add(lightTheme ?? resolvedThemes?.lightTheme)
339
- for (const item of resolvedThemes?.themes || [])
340
- add(item)
341
- for (const item of themes || [])
342
- add(item)
343
- return list
354
+ function buildThemeList(): [dark: string, light: string] {
355
+ return [
356
+ darkTheme ?? themes?.[0] ?? resolvedThemes?.darkTheme ?? resolvedThemes?.themes?.[0] ?? 'vitesse-dark',
357
+ lightTheme ?? themes?.[1] ?? resolvedThemes?.lightTheme ?? resolvedThemes?.themes?.[1] ?? 'vitesse-light',
358
+ ]
344
359
  }
345
360
 
346
- function resolveDiffHideUnchangedRegionsOption(value: unknown) {
347
- if (typeof value === 'boolean')
348
- return value
349
- if (value && typeof value === 'object') {
350
- const raw = value as Record<string, unknown>
351
- return {
352
- ...defaultDiffHideUnchangedRegions,
353
- ...raw,
354
- enabled: raw.enabled ?? true,
355
- }
356
- }
357
- return { ...defaultDiffHideUnchangedRegions }
358
- }
361
+ function buildResolvedRuntimeOptions() {
362
+ const userOptions = { ...(resolvedCodeBlockOptions ?? {}) } as Record<string, any>
363
+ for (const key of [
364
+ 'maxHeight',
365
+ 'padding',
366
+ 'tabSize',
367
+ 'theme',
368
+ 'themes',
369
+ 'themeType',
370
+ 'language',
371
+ 'languages',
372
+ 'stream',
373
+ 'disableFileHeader',
374
+ 'onThemeChange',
375
+ 'renderCustomHeader',
376
+ 'renderHeaderMetadata',
377
+ 'renderHeaderPrefix',
378
+ ])
379
+ delete userOptions[key]
380
+
381
+ const parseDiffOptions = userOptions.parseDiffOptions && typeof userOptions.parseDiffOptions === 'object'
382
+ ? userOptions.parseDiffOptions as Record<string, unknown>
383
+ : {}
384
+ const nativeOptions = diff
385
+ ? {
386
+ diffStyle: 'split',
387
+ expandUnchanged: false,
388
+ collapsedContextThreshold: 5,
389
+ hunkSeparators: 'line-info',
390
+ ...userOptions,
391
+ parseDiffOptions: {
392
+ context: 2,
393
+ ...parseDiffOptions,
394
+ },
395
+ }
396
+ : userOptions
397
+ const configuredUnsafeCSS = typeof nativeOptions.unsafeCSS === 'string' ? nativeOptions.unsafeCSS : ''
359
398
 
360
- function buildResolvedMonacoOptions() {
361
- const raw = { ...mergedMonacoOptions } as Record<string, any>
362
- const maxHeight = expanded ? 900 : (raw.MAX_HEIGHT ?? 500)
363
- const baseOptions = {
364
- readOnly: true,
365
- minimap: { enabled: false },
366
- lineNumbers: 'on',
367
- wordWrap: 'on',
368
- wrappingIndent: 'same',
369
- revealDebounceMs: 75,
370
- }
371
- const padding = getCodePadding()
372
- const configuredUnsafeCSS = typeof raw.unsafeCSS === 'string' ? raw.unsafeCSS : ''
373
- const unsafeCSS = `[data-file], [data-diff] { --diffs-min-number-column-width-default: 4ch !important; }
374
- ${configuredUnsafeCSS}`.trim()
375
- const finalOptions = {
376
- MAX_HEIGHT: maxHeight,
399
+ return {
400
+ overflow: 'wrap',
401
+ ...nativeOptions,
402
+ MAX_HEIGHT: expanded ? 900 : (resolvedCodeBlockOptions?.maxHeight ?? 500),
377
403
  fontFamily: getCodeFontFamily(),
378
404
  fontSize: codeFontSize,
379
405
  lineHeight: getCodeLineHeight(),
380
- // stream-diffs expects a boolean (`disableLineNumbers: options.lineNumbers === false`);
381
- // passing 'off'/'on' strings would always be truthy and defeat showLineNumbers={false}.
382
- lineNumbers: showLineNumbers !== false,
383
- padding,
384
- unsafeCSS,
385
- // The component owns the streaming fallback and file header. Initialize
386
- // stream-diffs in final mode so the revealed surface has highlighting,
387
- // line numbers, and the same geometry as the fallback.
406
+ disableLineNumbers: !effectiveShowLineNumbers,
407
+ unsafeCSS: `[data-file], [data-diff] { --diffs-min-number-column-width-default: 2ch !important; }
408
+ ${configuredUnsafeCSS}`.trim(),
388
409
  disableFileHeader: true,
389
410
  stream: false,
390
411
  themes: buildThemeList(),
391
- }
392
-
393
- if (!diff) {
394
- return {
395
- ...baseOptions,
396
- ...raw,
397
- ...finalOptions,
398
- }
399
- }
400
-
401
- const diffHideUnchangedRegions = raw.diffHideUnchangedRegions === undefined
402
- ? { ...defaultDiffHideUnchangedRegions }
403
- : resolveDiffHideUnchangedRegionsOption(raw.diffHideUnchangedRegions)
404
- const hideUnchangedRegions = raw.hideUnchangedRegions === undefined
405
- ? undefined
406
- : resolveDiffHideUnchangedRegionsOption(raw.hideUnchangedRegions)
407
- const streamPreviewDiff = resolvedStream !== false && resolvedLoading !== false
408
- const activeDiffHideUnchangedRegions = streamPreviewDiff
409
- ? { ...disabledDiffHideUnchangedRegions }
410
- : diffHideUnchangedRegions
411
- const activeHideUnchangedRegions = streamPreviewDiff
412
- ? { ...disabledDiffHideUnchangedRegions }
413
- : hideUnchangedRegions
414
- const experimental = {
415
- ...((raw.experimental as Record<string, unknown> | undefined) ?? {}),
416
- }
417
- const diffUnchangedRegionStyle = raw.diffUnchangedRegionStyle ?? 'line-info'
418
- const diffDefaults = {
419
- maxComputationTime: 0,
420
- diffAlgorithm: 'legacy',
421
- ignoreTrimWhitespace: false,
422
- renderIndicators: true,
423
- diffUpdateThrottleMs: 120,
424
- renderLineHighlight: 'none',
425
- renderLineHighlightOnlyWhenFocus: true,
426
- selectionHighlight: false,
427
- occurrencesHighlight: 'off',
428
- matchBrackets: 'never',
429
- lineDecorationsWidth: 4,
430
- lineNumbersMinChars: 2,
431
- glyphMargin: false,
432
- renderOverviewRuler: false,
433
- overviewRulerBorder: false,
434
- hideCursorInOverviewRuler: true,
435
- scrollBeyondLastLine: false,
436
- diffHideUnchangedRegions: activeDiffHideUnchangedRegions,
437
- useInlineViewWhenSpaceIsLimited: raw.useInlineViewWhenSpaceIsLimited ?? false,
438
- diffLineStyle: 'background',
439
- diffAppearance: 'auto',
440
- diffUnchangedRegionStyle,
441
- diffHunkActionsOnHover: false,
442
- experimental,
443
- }
444
-
445
- return {
446
- ...baseOptions,
447
- ...diffDefaults,
448
- ...raw,
449
- experimental,
450
- ...(activeHideUnchangedRegions === undefined ? {} : { hideUnchangedRegions: activeHideUnchangedRegions }),
451
- diffHideUnchangedRegions: activeDiffHideUnchangedRegions,
452
- ...finalOptions,
412
+ themeType: resolvedIsDark ? 'dark' : 'light',
413
+ onThemeChange() {
414
+ syncEditorGeometryVars()
415
+ scheduleEditorHeightSync()
416
+ },
453
417
  }
454
418
  }
455
419
 
456
- function syncRuntimeMonacoOptions() {
420
+ function syncRuntimeOptions() {
457
421
  const nextOptions = {
458
- ...resolvedMonacoOptions,
422
+ ...resolvedRuntimeOptions,
459
423
  theme: requestedTheme,
460
424
  }
461
- if (!runtimeMonacoOptions) {
462
- runtimeMonacoOptions = nextOptions
463
- return runtimeMonacoOptions
425
+ if (!runtimeOptions) {
426
+ runtimeOptions = nextOptions
427
+ return runtimeOptions
464
428
  }
465
- for (const key of Object.keys(runtimeMonacoOptions)) {
429
+ for (const key of Object.keys(runtimeOptions)) {
466
430
  if (!(key in nextOptions))
467
- delete runtimeMonacoOptions[key]
431
+ delete runtimeOptions[key]
468
432
  }
469
- Object.assign(runtimeMonacoOptions, nextOptions)
470
- return runtimeMonacoOptions
433
+ Object.assign(runtimeOptions, nextOptions)
434
+ return runtimeOptions
471
435
  }
472
436
 
473
- async function ensureMonaco() {
437
+ async function ensureRuntime() {
474
438
  if (helpers || useFallback || typeof window === 'undefined')
475
439
  return
476
- if (ensureMonacoPromise)
477
- return ensureMonacoPromise
440
+ if (ensureRuntimePromise)
441
+ return ensureRuntimePromise
478
442
 
479
- ensureMonacoPromise = (async () => {
480
- const mod = await getUseMonaco()
481
- if (!mounted)
443
+ const runtimeId = lifecycleId
444
+ const pending = (async () => {
445
+ const mod = await getStreamDiffsRuntime()
446
+ if (!mounted || lifecycleId !== runtimeId)
482
447
  return
483
- if (!mod || typeof mod.useMonaco !== 'function') {
448
+ if (!mod || typeof mod.createCodeBlockRuntime !== 'function') {
484
449
  useFallback = true
485
450
  return
486
451
  }
487
452
 
488
- helpers = mod.useMonaco(syncRuntimeMonacoOptions())
453
+ helpers = mod.createCodeBlockRuntime(syncRuntimeOptions())
489
454
  await Promise.resolve(helpers.setTheme?.(requestedTheme))
455
+ if (!mounted || lifecycleId !== runtimeId)
456
+ return
490
457
  lastThemeRequest = requestedTheme
491
- })().finally(() => {
492
- ensureMonacoPromise = null
458
+ })()
459
+ const tracked = pending.finally(() => {
460
+ if (ensureRuntimePromise === tracked)
461
+ ensureRuntimePromise = null
493
462
  })
494
- return ensureMonacoPromise
463
+ ensureRuntimePromise = tracked
464
+ return tracked
495
465
  }
496
466
 
497
467
  function queueThemeSync() {
@@ -500,7 +470,7 @@ ${configuredUnsafeCSS}`.trim()
500
470
  lastThemeRequest = requestedTheme
501
471
  void Promise.resolve(helpers.setTheme?.(requestedTheme)).catch((error) => {
502
472
  if (typeof console !== 'undefined')
503
- console.warn('[markstream-svelte] Failed to apply Monaco theme:', error)
473
+ console.warn('[markstream-svelte] Failed to apply code-block theme:', error)
504
474
  })
505
475
  }
506
476
 
@@ -508,10 +478,18 @@ ${configuredUnsafeCSS}`.trim()
508
478
  if (!mounted || !shouldRender || !editorHost || collapsed || shouldDelayEditor || shouldDeferStreamingLanguage)
509
479
  return
510
480
 
511
- await ensureMonaco()
512
- if (!mounted || useFallback || !helpers)
481
+ const runtimeId = lifecycleId
482
+ try {
483
+ await ensureRuntime()
484
+ }
485
+ catch (error) {
486
+ if (mounted && lifecycleId === runtimeId)
487
+ markEditorFallback(error)
488
+ return
489
+ }
490
+ if (!mounted || lifecycleId !== runtimeId || useFallback || !helpers)
513
491
  return
514
- syncRuntimeMonacoOptions()
492
+ syncRuntimeOptions()
515
493
 
516
494
  const desiredKind: 'single' | 'diff' = diff ? 'diff' : 'single'
517
495
  const hasEditorView = desiredKind === 'diff'
@@ -528,45 +506,43 @@ ${configuredUnsafeCSS}`.trim()
528
506
  return
529
507
  }
530
508
 
509
+ const operationId = lifecycleId
531
510
  try {
532
511
  if (diff && typeof helpers.updateDiff === 'function')
533
- await Promise.resolve(helpers.updateDiff(originalCode, updatedCode || code, monacoLanguage))
512
+ await Promise.resolve(helpers.updateDiff(originalCode, updatedCode || code, runtimeLanguage))
534
513
  else if (typeof helpers.updateCode === 'function') {
535
- await Promise.resolve(helpers.updateCode(code, monacoLanguage))
514
+ await Promise.resolve(helpers.updateCode(code, runtimeLanguage))
536
515
  scheduleEditorTokenization()
537
516
  }
517
+ if (!editorReady && await prepareEditorHandoff(desiredKind, lifecycleId)) {
518
+ editorRevealed = true
519
+ fallbackRetired = true
520
+ editorReady = true
521
+ }
538
522
  queueThemeSync()
539
523
  applyEditorOptions()
540
524
  scheduleEditorHeightSync()
541
525
  }
542
526
  catch (error) {
543
- markEditorFallback(error)
527
+ if (mounted && lifecycleId === operationId)
528
+ markEditorFallback(error)
544
529
  }
545
530
  }
546
531
 
547
532
  function hasRenderedEditorDom(kind: 'single' | 'diff') {
548
533
  if (!editorHost)
549
534
  return false
550
- if (kind === 'diff') {
551
- return Boolean(editorHost.querySelector([
552
- '.monaco-diff-editor',
553
- 'diffs-container',
554
- '.stream-diffs-shell',
555
- '[data-stream-diffs-state]',
556
- ].join(',')))
557
- }
558
- return Boolean(editorHost.querySelector([
559
- '.monaco-editor',
535
+ // stream-diffs renders its surface inside these containers.
536
+ const selectors = [
560
537
  'diffs-container',
561
538
  '.stream-diffs-shell',
562
539
  '[data-stream-diffs-state]',
563
- ].join(',')))
540
+ ].join(',')
541
+ return Boolean(editorHost.querySelector(selectors))
564
542
  }
565
543
 
566
544
  function getVisualEditorSurface() {
567
545
  return editorHost?.querySelector<HTMLElement>([
568
- '.monaco-diff-editor',
569
- '.monaco-editor',
570
546
  'diffs-container',
571
547
  '[data-stream-diffs-state]',
572
548
  '.stream-diffs-shell',
@@ -595,9 +571,8 @@ ${configuredUnsafeCSS}`.trim()
595
571
 
596
572
  async function prepareEditorHandoff(kind: 'single' | 'diff', creationId: number) {
597
573
  await tick()
598
- // Time-box the handoff: if visual readiness can't be confirmed (e.g. a
599
- // hidden/zero-size container), reveal the editor anyway once its DOM is
600
- // mounted so the block never strands in the pre-fallback forever.
574
+ // Streaming updates retry this gate, so keep the fallback until the live
575
+ // surface has positive geometry.
601
576
  const deadline = Date.now() + 1500
602
577
  let attempt = 0
603
578
  while (Date.now() < deadline && attempt < 30) {
@@ -612,33 +587,42 @@ ${configuredUnsafeCSS}`.trim()
612
587
  return isEditorVisuallyReady(kind)
613
588
  }
614
589
  }
615
- return !!(mounted && editorHost && lifecycleId === creationId && hasRenderedEditorDom(kind))
590
+ return !!(mounted && editorHost && lifecycleId === creationId && isEditorVisuallyReady(kind))
616
591
  }
617
592
 
618
593
  async function recreateEditor(kind: 'single' | 'diff') {
619
594
  if (!editorHost || !helpers || createEditorPromise)
620
595
  return createEditorPromise
621
596
 
597
+ const activeHelpers = helpers
622
598
  const creationId = ++lifecycleId
623
599
  editorReady = false
624
- createEditorPromise = (async () => {
600
+ const pending = (async () => {
625
601
  try {
626
602
  cleanupEditor(false)
627
- if (!mounted || !editorHost || lifecycleId !== creationId)
603
+ if (!mounted || !editorHost || lifecycleId !== creationId || helpers !== activeHelpers)
628
604
  return
629
605
  editorHost.replaceChildren()
630
606
  lastLayoutWidth = null
631
607
  lastLayoutHeight = null
632
608
 
633
609
  editorStreamMode = false
634
- if (kind === 'diff' && typeof helpers.createDiffEditor === 'function') {
635
- await helpers.createDiffEditor(editorHost, originalCode, updatedCode || code, monacoLanguage)
636
- await Promise.resolve(helpers.updateDiff?.(originalCode, updatedCode || code, monacoLanguage))
610
+ if (kind === 'diff' && typeof activeHelpers.createDiffEditor === 'function') {
611
+ await activeHelpers.createDiffEditor(editorHost, originalCode, updatedCode || code, runtimeLanguage)
612
+ if (!mounted || lifecycleId !== creationId || helpers !== activeHelpers)
613
+ return
614
+ await Promise.resolve(activeHelpers.updateDiff?.(originalCode, updatedCode || code, runtimeLanguage))
615
+ if (!mounted || lifecycleId !== creationId || helpers !== activeHelpers)
616
+ return
637
617
  editorKind = 'diff'
638
618
  }
639
619
  else {
640
- await helpers.createEditor(editorHost, code, monacoLanguage)
641
- await Promise.resolve(helpers.updateCode?.(code, monacoLanguage))
620
+ await activeHelpers.createEditor(editorHost, code, runtimeLanguage)
621
+ if (!mounted || lifecycleId !== creationId || helpers !== activeHelpers)
622
+ return
623
+ await Promise.resolve(activeHelpers.updateCode?.(code, runtimeLanguage))
624
+ if (!mounted || lifecycleId !== creationId || helpers !== activeHelpers)
625
+ return
642
626
  editorKind = 'single'
643
627
  }
644
628
  applyEditorOptions()
@@ -655,15 +639,17 @@ ${configuredUnsafeCSS}`.trim()
655
639
  scheduleEditorHeightSync()
656
640
  }
657
641
  catch (error) {
658
- if (mounted) {
642
+ if (mounted && lifecycleId === creationId && helpers === activeHelpers) {
659
643
  markEditorFallback(error)
660
644
  }
661
645
  }
662
- })().finally(() => {
663
- createEditorPromise = null
646
+ })()
647
+ const tracked = pending.finally(() => {
648
+ if (createEditorPromise === tracked)
649
+ createEditorPromise = null
664
650
  })
665
-
666
- return createEditorPromise
651
+ createEditorPromise = tracked
652
+ return tracked
667
653
  }
668
654
 
669
655
  function refreshEditorAfterLoadingSettled() {
@@ -681,23 +667,23 @@ ${configuredUnsafeCSS}`.trim()
681
667
  }
682
668
  if (!mounted || !shouldRender || !editorHost || collapsed || shouldDelayEditor || shouldDeferStreamingLanguage)
683
669
  return
684
- await ensureMonaco()
670
+ await ensureRuntime()
685
671
  if (!mounted || useFallback || !helpers)
686
672
  return
687
- syncRuntimeMonacoOptions()
673
+ syncRuntimeOptions()
688
674
  const desiredKind: 'single' | 'diff' = diff ? 'diff' : 'single'
689
675
  if (!hasRenderedEditorDom(desiredKind) || editorKind !== desiredKind)
690
676
  await recreateEditor(desiredKind)
691
677
  if (!mounted || useFallback || !helpers || !hasRenderedEditorDom(desiredKind) || editorKind !== desiredKind)
692
678
  return
693
679
  if (diff) {
694
- await Promise.resolve(helpers.updateDiff?.(originalCode, updatedCode || code, monacoLanguage))
680
+ await Promise.resolve(helpers.updateDiff?.(originalCode, updatedCode || code, runtimeLanguage))
695
681
  helpers.refreshDiffPresentation?.()
696
682
  applyEditorOptions()
697
683
  scheduleEditorHeightSync()
698
684
  return
699
685
  }
700
- await Promise.resolve(helpers.updateCode?.(code, monacoLanguage))
686
+ await Promise.resolve(helpers.updateCode?.(code, runtimeLanguage))
701
687
  scheduleEditorTokenization(140, true)
702
688
  applyEditorOptions()
703
689
  scheduleEditorHeightSync()
@@ -724,6 +710,11 @@ ${configuredUnsafeCSS}`.trim()
724
710
  }
725
711
 
726
712
  function cleanupEditor(disposeHelpers = true) {
713
+ if (disposeHelpers) {
714
+ lifecycleId += 1
715
+ createEditorPromise = null
716
+ ensureRuntimePromise = null
717
+ }
727
718
  clearEditorHeightSyncBindings()
728
719
  cancelEditorHeightSync()
729
720
  try {
@@ -742,15 +733,15 @@ ${configuredUnsafeCSS}`.trim()
742
733
  lastLayoutHeight = null
743
734
  if (disposeHelpers) {
744
735
  helpers = null
745
- runtimeMonacoOptions = null
746
- ensureMonacoPromise = null
736
+ runtimeOptions = null
737
+ ensureRuntimePromise = null
747
738
  lastThemeRequest = ''
748
739
  }
749
740
  }
750
741
 
751
742
  function applyEditorOptions() {
752
743
  const target = diff ? helpers?.getDiffEditorView?.() : helpers?.getEditorView?.()
753
- target?.updateOptions?.({ fontSize: codeFontSize, automaticLayout: false })
744
+ target?.updateOptions?.({ fontSize: codeFontSize })
754
745
  syncEditorGeometryVars()
755
746
  scheduleEditorHeightSync()
756
747
  }
@@ -760,10 +751,10 @@ ${configuredUnsafeCSS}`.trim()
760
751
  function syncEditorGeometryVars() {
761
752
  if (!editorHost)
762
753
  return
763
- const tabSize = readPositiveMetric(mergedMonacoOptions.tabSize) ?? 4
754
+ const tabSize = readPositiveMetric(resolvedCodeBlockOptions?.tabSize) ?? 4
764
755
  editorHost.style.setProperty('--diffs-tab-size', String(tabSize))
765
- const rawPadding = mergedMonacoOptions.padding
766
- const hasConfiguredPadding = Boolean(rawPadding && typeof rawPadding === 'object')
756
+ const rawPadding = resolvedCodeBlockOptions?.padding
757
+ const hasConfiguredPadding = typeof rawPadding === 'number'
767
758
  if (hasConfiguredPadding)
768
759
  editorHost.style.setProperty('--diffs-gap-block', `${getCodePadding().top}px`)
769
760
  else
@@ -771,11 +762,7 @@ ${configuredUnsafeCSS}`.trim()
771
762
  }
772
763
 
773
764
  function getMaxHeightValue() {
774
- const raw = resolvedMonacoOptions.MAX_HEIGHT
775
- if (raw === 'none' || raw == null)
776
- return Number.POSITIVE_INFINITY
777
- const value = typeof raw === 'number' ? raw : Number.parseFloat(String(raw))
778
- return Number.isFinite(value) && value > 0 ? value : 500
765
+ return resolvedCodeBlockOptions?.maxHeight ?? 500
779
766
  }
780
767
 
781
768
  function scheduleEditorHeightSync() {
@@ -989,37 +976,19 @@ ${configuredUnsafeCSS}`.trim()
989
976
  if (hostRect.height <= 0)
990
977
  return null
991
978
 
992
- const selectors = [
993
- '.editor.original .view-lines .view-line',
994
- '.editor.modified .view-lines .view-line',
995
- '.editor.original .view-zones > div',
996
- '.editor.modified .view-zones > div',
997
- '.editor.original .margin-view-zones > div',
998
- '.editor.modified .margin-view-zones > div',
999
- '.editor.original .diff-hidden-lines',
1000
- '.editor.modified .diff-hidden-lines',
1001
- '.stream-monaco-diff-unchanged-bridge',
1002
- ]
1003
-
1004
- let bottom = 0
1005
- for (const node of Array.from(container.querySelectorAll<HTMLElement>(selectors.join(',')))) {
1006
- const style = window.getComputedStyle(node)
1007
- if (style.display === 'none' || style.visibility === 'hidden')
1008
- continue
1009
- if (Number.parseFloat(style.opacity || '1') <= 0.01)
1010
- continue
1011
- const rect = node.getBoundingClientRect()
1012
- if (rect.height <= 0 || rect.bottom <= hostRect.top)
1013
- continue
1014
- bottom = Math.max(bottom, rect.bottom - hostRect.top)
1015
- }
1016
-
1017
- if (bottom > 0)
1018
- return Math.ceil(bottom + 1)
1019
-
1020
- const diffRoot = container.querySelector<HTMLElement>('.monaco-diff-editor')
1021
- const diffHeight = diffRoot?.getBoundingClientRect?.().height ?? 0
1022
- return diffHeight > 0 ? Math.ceil(diffHeight + 1) : null
979
+ // stream-diffs renders its surface inside these containers. Measure the
980
+ // rendered shell so a diff block fills its real content height even when
981
+ // the adapter's getContentHeight isn't available yet.
982
+ const surface = container.querySelector<HTMLElement>([
983
+ 'diffs-container',
984
+ '.stream-diffs-shell',
985
+ '[data-stream-diffs-state]',
986
+ ].join(','))
987
+ if (!surface)
988
+ return null
989
+ const rect = surface.getBoundingClientRect()
990
+ const height = rect.bottom - hostRect.top
991
+ return height > 0 ? Math.ceil(height + 1) : null
1023
992
  }
1024
993
  catch {
1025
994
  return null
@@ -1073,7 +1042,7 @@ ${configuredUnsafeCSS}`.trim()
1073
1042
  {#if shouldRender}
1074
1043
  <div
1075
1044
  class:is-dark={resolvedIsDark}
1076
- class:is-plain-text={monacoLanguage === 'plaintext'}
1045
+ class:is-plain-text={runtimeLanguage === 'plaintext'}
1077
1046
  class:is-rendering={resolvedLoading}
1078
1047
  class:is-diff={diff}
1079
1048
  class="code-block-container"
@@ -1137,7 +1106,7 @@ ${configuredUnsafeCSS}`.trim()
1137
1106
  class="code-pre-fallback"
1138
1107
  enhanceable={false}
1139
1108
  node={preFallbackNode}
1140
- showLineNumbers={showLineNumbers !== false}
1109
+ showLineNumbers={effectiveShowLineNumbers}
1141
1110
  style={preFallbackStyle}
1142
1111
  />
1143
1112
  </div>