@marimo-team/islands 0.23.15-dev3 → 0.23.15-dev31
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/dist/{ConnectedDataExplorerComponent-CU4fZJzG.js → ConnectedDataExplorerComponent-7p7rt9iw.js} +4 -4
- package/dist/{ErrorBoundary-DE6tzZf-.js → ErrorBoundary-B_CAG7_e.js} +1 -1
- package/dist/{ImageComparisonComponent-CHrI72em.js → ImageComparisonComponent-1i5IBhon.js} +173 -129
- package/dist/{chat-ui-DinOvbWu.js → chat-ui-CbyDW7Rb.js} +3103 -3103
- package/dist/{code-visibility-ScKKYzhB.js → code-visibility-M6TuquZp.js} +932 -850
- package/dist/{constants-T20xxyNf.js → debounce-BOD3DbfP.js} +1 -24
- package/dist/{formats-Dzx4J_z1.js → formats-BX3uDQbB.js} +1 -1
- package/dist/{glide-data-editor-CjTu7ukN.js → glide-data-editor-CwZz71BD.js} +3 -3
- package/dist/{html-to-image-_wGfk8V-.js → html-to-image-CtsZpKdg.js} +2251 -2216
- package/dist/{input-DtsN7xm-.js → input-BGPrFH3g.js} +2 -2
- package/dist/main.js +1081 -1088
- package/dist/{mermaid-BYqXy_NE.js → mermaid-UdmxG2PZ.js} +2 -2
- package/dist/{process-output-Mh4UrjwM.js → process-output-DApSJpc6.js} +1 -1
- package/dist/{reveal-component-Dp5KdjBI.js → reveal-component-CV1WyDIM.js} +618 -605
- package/dist/{spec-Cz-Bj1JI.js → spec-DSs9v0xx.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-CWNNlFEX.js → toDate-CKpRx4TS.js} +3 -4
- package/dist/{useAsyncData-KfHB8wQR.js → useAsyncData-BSOyAbac.js} +1 -1
- package/dist/{useDeepCompareMemoize-nJMtxhm4.js → useDeepCompareMemoize-BTLeWzuR.js} +1 -1
- package/dist/{useLifecycle-DegSo0lV.js → useLifecycle-C6wHjkhW.js} +1 -1
- package/dist/{useTheme-6eZ3GOTS.js → useTheme-Df_vGflw.js} +62 -27
- package/dist/{vega-component-DzyyM9fc.js → vega-component-DNHEV0u0.js} +6 -6
- package/package.json +1 -1
- package/src/components/chat/__tests__/chat-utils.test.ts +244 -1
- package/src/components/chat/__tests__/message-queue.test.tsx +121 -0
- package/src/components/chat/chat-panel.tsx +196 -67
- package/src/components/chat/chat-utils.ts +111 -2
- package/src/components/editor/SortableCell.tsx +6 -2
- package/src/components/editor/__tests__/output-persistence.test.tsx +241 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +153 -0
- package/src/components/editor/actions/pair-with-agent-commands.ts +109 -0
- package/src/components/editor/actions/pair-with-agent-modal.tsx +20 -64
- package/src/components/editor/ai/add-cell-with-ai.tsx +14 -5
- package/src/components/editor/ai/ai-completion-editor.tsx +4 -1
- package/src/components/editor/cell/cell-context-menu.tsx +7 -0
- package/src/components/editor/chrome/panels/packages-panel.tsx +11 -1
- package/src/components/editor/columns/__tests__/cell-column.test.tsx +105 -0
- package/src/components/editor/columns/cell-column.tsx +34 -4
- package/src/components/editor/columns/sortable-column.tsx +24 -4
- package/src/components/editor/navigation/__tests__/navigation.test.ts +33 -0
- package/src/components/editor/navigation/navigation.ts +8 -1
- package/src/components/editor/notebook-cell.tsx +203 -106
- package/src/components/editor/renderers/__tests__/cells-renderer.test.tsx +66 -0
- package/src/components/editor/renderers/cell-array.tsx +26 -13
- package/src/components/editor/renderers/cells-renderer.tsx +8 -2
- package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +29 -0
- package/src/components/editor/renderers/slides-layout/types.ts +4 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +19 -19
- package/src/components/slides/reveal-component.tsx +37 -4
- package/src/components/slides/slide-form.tsx +72 -0
- package/src/core/cells/__tests__/utils.test.ts +59 -0
- package/src/core/cells/utils.ts +51 -0
- package/src/core/codemirror/completion/__tests__/signature-hint.test.ts +97 -3
- package/src/core/codemirror/completion/signature-hint.ts +78 -11
- package/src/core/constants.ts +6 -0
- package/src/css/app/Cell.css +10 -0
- package/src/hooks/__tests__/useHotkey.test.tsx +88 -0
- package/src/hooks/useHotkey.ts +29 -4
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +6 -9
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +22 -61
- package/src/plugins/impl/anywidget/model-proxy.ts +0 -13
- package/src/plugins/impl/anywidget/widget-binding.ts +4 -34
- package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +53 -2
- package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +71 -0
- package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +71 -3
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -0
- package/src/theme/__tests__/useTheme.test.ts +68 -0
- package/src/theme/useTheme.ts +16 -1
|
@@ -33,11 +33,14 @@ import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
|
|
|
33
33
|
import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown";
|
|
34
34
|
import { useResolvedMarimoConfig } from "@/core/config/config";
|
|
35
35
|
import { CSSClasses, KnownQueryParams } from "@/core/constants";
|
|
36
|
-
import type {
|
|
36
|
+
import type { OutputMessage } from "@/core/kernel/messages";
|
|
37
37
|
import { kernelStateAtom } from "@/core/kernel/state";
|
|
38
38
|
import { useNotebookCodeAvailable } from "@/core/meta/code-visibility";
|
|
39
39
|
import { showCodeInRunModeAtom } from "@/core/meta/state";
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
publishedCellClasses,
|
|
42
|
+
shouldHidePublishedCell,
|
|
43
|
+
} from "@/core/cells/utils";
|
|
41
44
|
import { type AppMode, kioskModeAtom } from "@/core/mode";
|
|
42
45
|
import { useRequestClient } from "@/core/network/requests";
|
|
43
46
|
import type { CellConfig } from "@/core/network/types";
|
|
@@ -362,12 +365,13 @@ const VerticalCell = memo(
|
|
|
362
365
|
const className = cn(
|
|
363
366
|
"marimo-cell",
|
|
364
367
|
"hover-actions-parent empty:invisible",
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
368
|
+
published
|
|
369
|
+
? publishedCellClasses({ errored, stopped })
|
|
370
|
+
: {
|
|
371
|
+
"has-error": errored,
|
|
372
|
+
stopped: stopped,
|
|
373
|
+
borderless: isPureMarkdown,
|
|
374
|
+
},
|
|
371
375
|
);
|
|
372
376
|
|
|
373
377
|
// Read mode and show code
|
|
@@ -419,19 +423,15 @@ const VerticalCell = memo(
|
|
|
419
423
|
);
|
|
420
424
|
}
|
|
421
425
|
|
|
422
|
-
const outputIsError = isErrorMime(output?.mimetype);
|
|
423
426
|
// When show_tracebacks is enabled, show error outputs inline
|
|
424
427
|
// instead of hiding them
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
output
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
);
|
|
433
|
-
const hidden =
|
|
434
|
-
(errored || interrupted || stopped || outputIsError) && !hasTraceback;
|
|
428
|
+
const hidden = shouldHidePublishedCell({
|
|
429
|
+
errored,
|
|
430
|
+
interrupted,
|
|
431
|
+
stopped,
|
|
432
|
+
output,
|
|
433
|
+
showErrorTracebacks,
|
|
434
|
+
});
|
|
435
435
|
if (hidden) {
|
|
436
436
|
return null;
|
|
437
437
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
type CSSProperties,
|
|
4
5
|
startTransition,
|
|
5
6
|
useEffect,
|
|
6
7
|
useMemo,
|
|
@@ -26,6 +27,7 @@ import { Logger } from "@/utils/Logger";
|
|
|
26
27
|
import "./slides.css";
|
|
27
28
|
import "./reveal-slides.css";
|
|
28
29
|
import type {
|
|
30
|
+
DeckVerticalAlign,
|
|
29
31
|
SlideConfig,
|
|
30
32
|
SlidesLayout,
|
|
31
33
|
SlideType,
|
|
@@ -39,6 +41,7 @@ import {
|
|
|
39
41
|
type ComposedSubslide,
|
|
40
42
|
} from "./compose-slides";
|
|
41
43
|
import {
|
|
44
|
+
DEFAULT_DECK_VERTICAL_ALIGN,
|
|
42
45
|
DEFAULT_DECK_TRANSITION,
|
|
43
46
|
DEFAULT_SLIDE_TYPE,
|
|
44
47
|
SlideSidebar,
|
|
@@ -292,16 +295,38 @@ export function useParkedPreview(options: {
|
|
|
292
295
|
};
|
|
293
296
|
}
|
|
294
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Margin style that positions a slide's content vertically within the
|
|
300
|
+
* full-height slide. The content is a flex item, so the vertical margins decide
|
|
301
|
+
* where the free space lands: `auto` on both sides centers it, while pinning one
|
|
302
|
+
* side to `0` pushes content to the top or bottom. The horizontal `20px` keeps
|
|
303
|
+
* content off the slide edges regardless of alignment.
|
|
304
|
+
*/
|
|
305
|
+
function resolveSlideContentStyle(
|
|
306
|
+
verticalAlign: DeckVerticalAlign | undefined,
|
|
307
|
+
): CSSProperties {
|
|
308
|
+
switch (verticalAlign ?? DEFAULT_DECK_VERTICAL_ALIGN) {
|
|
309
|
+
case "top":
|
|
310
|
+
return { margin: "0 20px auto" };
|
|
311
|
+
case "bottom":
|
|
312
|
+
return { margin: "auto 20px 0" };
|
|
313
|
+
default:
|
|
314
|
+
return { margin: "auto 20px" };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
295
318
|
const SubslideView = ({
|
|
296
319
|
subslide,
|
|
297
320
|
resolveShowCode,
|
|
298
321
|
isEditable,
|
|
299
322
|
slideConfigs,
|
|
323
|
+
contentStyle,
|
|
300
324
|
}: {
|
|
301
325
|
subslide: ComposedSubslide<RuntimeCell>;
|
|
302
326
|
resolveShowCode: (cellId: CellId) => boolean;
|
|
303
327
|
isEditable: boolean;
|
|
304
328
|
slideConfigs: ReadonlyMap<CellId, SlideConfig>;
|
|
329
|
+
contentStyle: CSSProperties;
|
|
305
330
|
}) => {
|
|
306
331
|
const { slideLevel, cumulativeByBlock } = buildSubslideNotes(
|
|
307
332
|
subslide,
|
|
@@ -321,9 +346,7 @@ const SubslideView = ({
|
|
|
321
346
|
? "mo-slide-content flex flex-col gap-3"
|
|
322
347
|
: "mo-slide-content"
|
|
323
348
|
}
|
|
324
|
-
style={
|
|
325
|
-
margin: "auto 20px",
|
|
326
|
-
}}
|
|
349
|
+
style={contentStyle}
|
|
327
350
|
>
|
|
328
351
|
{subslide.blocks.map((block, i) => {
|
|
329
352
|
const rendered = block.cells.map((cell) => {
|
|
@@ -514,6 +537,10 @@ const RevealSlidesComponent = ({
|
|
|
514
537
|
);
|
|
515
538
|
|
|
516
539
|
const deckTransition = layout.deck?.transition ?? DEFAULT_DECK_TRANSITION;
|
|
540
|
+
const slideContentStyle = resolveSlideContentStyle(
|
|
541
|
+
layout.deck?.verticalAlign,
|
|
542
|
+
);
|
|
543
|
+
|
|
517
544
|
// Reveal's Notes plugin iframes the deck for the current/upcoming-slide
|
|
518
545
|
// previews. We load the same URL but as a read-only kiosk client with the
|
|
519
546
|
// app chrome hidden, which `<SlidesLayoutRenderer>` interprets the same as
|
|
@@ -536,6 +563,10 @@ const RevealSlidesComponent = ({
|
|
|
536
563
|
transition: deckTransition,
|
|
537
564
|
keyboardCondition: (event: KeyboardEvent) => !Events.fromInput(event),
|
|
538
565
|
url: kioskUrl,
|
|
566
|
+
// reveal.js auto-switches to its scroll view for mobile.
|
|
567
|
+
// We disable this mode because it rewrites the slide DOM that
|
|
568
|
+
// `@revealjs/react` owns, which crashes on `reveal.sync()` re-renders.
|
|
569
|
+
scrollActivationWidth: 0,
|
|
539
570
|
}),
|
|
540
571
|
[width, height, deckTransition, kioskUrl],
|
|
541
572
|
);
|
|
@@ -704,6 +735,7 @@ const RevealSlidesComponent = ({
|
|
|
704
735
|
resolveShowCode={resolveShowCode}
|
|
705
736
|
isEditable={isEditable}
|
|
706
737
|
slideConfigs={layout.cells}
|
|
738
|
+
contentStyle={slideContentStyle}
|
|
707
739
|
/>
|
|
708
740
|
);
|
|
709
741
|
}
|
|
@@ -717,6 +749,7 @@ const RevealSlidesComponent = ({
|
|
|
717
749
|
resolveShowCode={resolveShowCode}
|
|
718
750
|
isEditable={isEditable}
|
|
719
751
|
slideConfigs={layout.cells}
|
|
752
|
+
contentStyle={slideContentStyle}
|
|
720
753
|
/>
|
|
721
754
|
);
|
|
722
755
|
})}
|
|
@@ -743,7 +776,7 @@ const RevealSlidesComponent = ({
|
|
|
743
776
|
? "mo-slide-content flex flex-col gap-3"
|
|
744
777
|
: "mo-slide-content"
|
|
745
778
|
}
|
|
746
|
-
style={
|
|
779
|
+
style={slideContentStyle}
|
|
747
780
|
>
|
|
748
781
|
<ParkedPreviewContent
|
|
749
782
|
cell={parkedPreviewCell}
|
|
@@ -23,6 +23,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
|
23
23
|
import type { CellId } from "@/core/cells/ids";
|
|
24
24
|
import { cn } from "@/utils/cn";
|
|
25
25
|
import type {
|
|
26
|
+
DeckVerticalAlign,
|
|
26
27
|
DeckTransition,
|
|
27
28
|
SlidesLayout,
|
|
28
29
|
SlideType,
|
|
@@ -37,6 +38,7 @@ import { jotaiJsonStorage } from "@/utils/storage/jotai";
|
|
|
37
38
|
|
|
38
39
|
export const DEFAULT_SLIDE_TYPE: SlideType = "slide";
|
|
39
40
|
export const DEFAULT_DECK_TRANSITION: DeckTransition = "slide";
|
|
41
|
+
export const DEFAULT_DECK_VERTICAL_ALIGN: DeckVerticalAlign = "center";
|
|
40
42
|
const COLLAPSED_CONFIG_WIDTH = 36;
|
|
41
43
|
const slideConfigOpenAtom = atomWithStorage<boolean>(
|
|
42
44
|
"marimo:slides:config-open",
|
|
@@ -118,6 +120,30 @@ const DECK_TRANSITION_OPTIONS: DeckTransitionOption[] = [
|
|
|
118
120
|
{ value: "zoom", label: "Zoom", description: "Zoom into the next slide." },
|
|
119
121
|
];
|
|
120
122
|
|
|
123
|
+
interface DeckVerticalAlignOption {
|
|
124
|
+
value: DeckVerticalAlign;
|
|
125
|
+
label: string;
|
|
126
|
+
description: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const DECK_VERTICAL_ALIGN_OPTIONS: DeckVerticalAlignOption[] = [
|
|
130
|
+
{
|
|
131
|
+
value: "center",
|
|
132
|
+
label: "Center",
|
|
133
|
+
description: "Vertically center each slide's content.",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
value: "top",
|
|
137
|
+
label: "Top",
|
|
138
|
+
description: "Align content to the top, like the cell view.",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
value: "bottom",
|
|
142
|
+
label: "Bottom",
|
|
143
|
+
description: "Align content to the bottom of each slide.",
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
|
|
121
147
|
const SlidesForm = ({
|
|
122
148
|
layout,
|
|
123
149
|
setLayout,
|
|
@@ -300,6 +326,12 @@ const DeckConfigForm = ({
|
|
|
300
326
|
(opt) => opt.value === currentTransition,
|
|
301
327
|
)?.description;
|
|
302
328
|
|
|
329
|
+
const currentVerticalAlign: DeckVerticalAlign =
|
|
330
|
+
layout.deck?.verticalAlign ?? DEFAULT_DECK_VERTICAL_ALIGN;
|
|
331
|
+
const activeVerticalAlignDescription = DECK_VERTICAL_ALIGN_OPTIONS.find(
|
|
332
|
+
(opt) => opt.value === currentVerticalAlign,
|
|
333
|
+
)?.description;
|
|
334
|
+
|
|
303
335
|
const handleTransitionChange = (value: DeckTransition) => {
|
|
304
336
|
setLayout({
|
|
305
337
|
...layout,
|
|
@@ -307,6 +339,13 @@ const DeckConfigForm = ({
|
|
|
307
339
|
});
|
|
308
340
|
};
|
|
309
341
|
|
|
342
|
+
const handleVerticalAlignChange = (value: DeckVerticalAlign) => {
|
|
343
|
+
setLayout({
|
|
344
|
+
...layout,
|
|
345
|
+
deck: { ...layout.deck, verticalAlign: value },
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
|
|
310
349
|
return (
|
|
311
350
|
<div className="flex flex-col gap-3">
|
|
312
351
|
<div className="flex flex-col gap-1.5">
|
|
@@ -337,6 +376,39 @@ const DeckConfigForm = ({
|
|
|
337
376
|
<p className="text-xs text-foreground/70">{activeDescription}</p>
|
|
338
377
|
)}
|
|
339
378
|
</div>
|
|
379
|
+
<div className="flex flex-col gap-1.5">
|
|
380
|
+
<label
|
|
381
|
+
htmlFor="deck-vertical-align"
|
|
382
|
+
className="font-semibold text-sm text-foreground"
|
|
383
|
+
>
|
|
384
|
+
Vertical alignment
|
|
385
|
+
</label>
|
|
386
|
+
<Select
|
|
387
|
+
value={currentVerticalAlign}
|
|
388
|
+
onValueChange={(value) =>
|
|
389
|
+
handleVerticalAlignChange(value as DeckVerticalAlign)
|
|
390
|
+
}
|
|
391
|
+
>
|
|
392
|
+
<SelectTrigger
|
|
393
|
+
id="deck-vertical-align"
|
|
394
|
+
aria-label="Vertical alignment"
|
|
395
|
+
>
|
|
396
|
+
<SelectValue />
|
|
397
|
+
</SelectTrigger>
|
|
398
|
+
<SelectContent>
|
|
399
|
+
{DECK_VERTICAL_ALIGN_OPTIONS.map(({ value, label }) => (
|
|
400
|
+
<SelectItem key={value} value={value}>
|
|
401
|
+
{label}
|
|
402
|
+
</SelectItem>
|
|
403
|
+
))}
|
|
404
|
+
</SelectContent>
|
|
405
|
+
</Select>
|
|
406
|
+
{activeVerticalAlignDescription && (
|
|
407
|
+
<p className="text-xs text-foreground/70">
|
|
408
|
+
{activeVerticalAlignDescription}
|
|
409
|
+
</p>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
340
412
|
</div>
|
|
341
413
|
);
|
|
342
414
|
};
|
|
@@ -8,11 +8,13 @@ import {
|
|
|
8
8
|
} from "@/core/cells/cells";
|
|
9
9
|
import { CellId } from "@/core/cells/ids";
|
|
10
10
|
import type { CellData, CellRuntimeState } from "@/core/cells/types";
|
|
11
|
+
import type { OutputMessage } from "@/core/kernel/messages";
|
|
11
12
|
import { MultiColumn } from "@/utils/id-tree";
|
|
12
13
|
import {
|
|
13
14
|
disabledCellIds,
|
|
14
15
|
enabledCellIds,
|
|
15
16
|
isUninstantiated,
|
|
17
|
+
shouldHidePublishedCell,
|
|
16
18
|
staleCellIds,
|
|
17
19
|
} from "../utils";
|
|
18
20
|
|
|
@@ -426,3 +428,60 @@ describe("enabledCellIds", () => {
|
|
|
426
428
|
expect(result).toEqual([cellId2, cellId3]);
|
|
427
429
|
});
|
|
428
430
|
});
|
|
431
|
+
|
|
432
|
+
describe("shouldHidePublishedCell", () => {
|
|
433
|
+
const base = {
|
|
434
|
+
errored: false,
|
|
435
|
+
interrupted: false,
|
|
436
|
+
stopped: false,
|
|
437
|
+
output: null,
|
|
438
|
+
showErrorTracebacks: false,
|
|
439
|
+
};
|
|
440
|
+
const errorOutput = (traceback?: string): OutputMessage => ({
|
|
441
|
+
channel: "marimo-error",
|
|
442
|
+
mimetype: "application/vnd.marimo+error",
|
|
443
|
+
data: [
|
|
444
|
+
{ type: "exception", exception_type: "ValueError", msg: "!", traceback },
|
|
445
|
+
],
|
|
446
|
+
timestamp: 0,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("does not hide a healthy cell", () => {
|
|
450
|
+
expect(shouldHidePublishedCell(base)).toBe(false);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it.each(["errored", "interrupted", "stopped"] as const)(
|
|
454
|
+
"hides a %s cell",
|
|
455
|
+
(key) => {
|
|
456
|
+
expect(shouldHidePublishedCell({ ...base, [key]: true })).toBe(true);
|
|
457
|
+
},
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
it("hides error outputs", () => {
|
|
461
|
+
expect(shouldHidePublishedCell({ ...base, output: errorOutput() })).toBe(
|
|
462
|
+
true,
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("shows error outputs with a traceback when show_tracebacks is enabled", () => {
|
|
467
|
+
expect(
|
|
468
|
+
shouldHidePublishedCell({
|
|
469
|
+
...base,
|
|
470
|
+
errored: true,
|
|
471
|
+
output: errorOutput("Traceback (most recent call last) ..."),
|
|
472
|
+
showErrorTracebacks: true,
|
|
473
|
+
}),
|
|
474
|
+
).toBe(false);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("hides error outputs without a traceback even when show_tracebacks is enabled", () => {
|
|
478
|
+
expect(
|
|
479
|
+
shouldHidePublishedCell({
|
|
480
|
+
...base,
|
|
481
|
+
errored: true,
|
|
482
|
+
output: errorOutput(),
|
|
483
|
+
showErrorTracebacks: true,
|
|
484
|
+
}),
|
|
485
|
+
).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
});
|
package/src/core/cells/utils.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import type { EditorView } from "@codemirror/view";
|
|
4
4
|
import { Objects } from "@/utils/objects";
|
|
5
|
+
import type { MarimoError, OutputMessage } from "../kernel/messages";
|
|
6
|
+
import { isErrorMime } from "../mime";
|
|
5
7
|
import type { RuntimeState } from "../network/types";
|
|
6
8
|
import type { NotebookState } from "./cells";
|
|
7
9
|
import type { CellId } from "./ids";
|
|
@@ -196,3 +198,52 @@ export function cellStatusClasses({
|
|
|
196
198
|
stale: status === "disabled-transitively",
|
|
197
199
|
};
|
|
198
200
|
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Status classes for a published cell (read or present mode, output only).
|
|
204
|
+
*/
|
|
205
|
+
export function publishedCellClasses({
|
|
206
|
+
errored,
|
|
207
|
+
stopped,
|
|
208
|
+
}: {
|
|
209
|
+
errored: boolean;
|
|
210
|
+
stopped: boolean;
|
|
211
|
+
}) {
|
|
212
|
+
return {
|
|
213
|
+
published: true,
|
|
214
|
+
"has-error": errored,
|
|
215
|
+
stopped: stopped,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Whether a published cell (read or present mode) should be hidden.
|
|
221
|
+
*
|
|
222
|
+
* Errored, interrupted, and stopped cells (and error outputs) are hidden in
|
|
223
|
+
* published views, unless `show_tracebacks` is enabled and the output carries
|
|
224
|
+
* an exception traceback to display inline.
|
|
225
|
+
*/
|
|
226
|
+
export function shouldHidePublishedCell({
|
|
227
|
+
errored,
|
|
228
|
+
interrupted,
|
|
229
|
+
stopped,
|
|
230
|
+
output,
|
|
231
|
+
showErrorTracebacks,
|
|
232
|
+
}: {
|
|
233
|
+
errored: boolean;
|
|
234
|
+
interrupted: boolean;
|
|
235
|
+
stopped: boolean;
|
|
236
|
+
output: OutputMessage | null;
|
|
237
|
+
showErrorTracebacks: boolean;
|
|
238
|
+
}): boolean {
|
|
239
|
+
const outputIsError = isErrorMime(output?.mimetype);
|
|
240
|
+
const hasTraceback =
|
|
241
|
+
showErrorTracebacks &&
|
|
242
|
+
outputIsError &&
|
|
243
|
+
Array.isArray(output?.data) &&
|
|
244
|
+
output.data.some(
|
|
245
|
+
(e: MarimoError) =>
|
|
246
|
+
e.type === "exception" && "traceback" in e && e.traceback,
|
|
247
|
+
);
|
|
248
|
+
return (errored || interrupted || stopped || outputIsError) && !hasTraceback;
|
|
249
|
+
}
|
|
@@ -5,6 +5,7 @@ import { EditorView, type Tooltip } from "@codemirror/view";
|
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
6
|
import {
|
|
7
7
|
asSignatureHint,
|
|
8
|
+
closeSignatureHint,
|
|
8
9
|
setSignatureHintEffect,
|
|
9
10
|
signatureHintField,
|
|
10
11
|
} from "../signature-hint";
|
|
@@ -51,12 +52,79 @@ describe("signatureHintField", () => {
|
|
|
51
52
|
expect(state.field(signatureHintField)).toBeNull();
|
|
52
53
|
});
|
|
53
54
|
|
|
54
|
-
it("keeps and re-anchors the tooltip across edits", () => {
|
|
55
|
+
it("keeps and re-anchors the tooltip across edits inside the call", () => {
|
|
55
56
|
let state = stateWithHint("plt.plot(", 9);
|
|
56
|
-
// Insert before the tooltip position
|
|
57
|
-
|
|
57
|
+
// Insert before the tooltip position while the cursor stays inside the
|
|
58
|
+
// call; the anchor should shift but the hint should remain.
|
|
59
|
+
state = state.update({
|
|
60
|
+
changes: { from: 0, insert: "xy" },
|
|
61
|
+
selection: { anchor: 11 },
|
|
62
|
+
}).state;
|
|
58
63
|
expect(state.field(signatureHintField)?.pos).toBe(11);
|
|
59
64
|
});
|
|
65
|
+
|
|
66
|
+
it("dismisses the tooltip when the closing paren is typed", () => {
|
|
67
|
+
let state = stateWithHint("plt.plot(", 9);
|
|
68
|
+
// Type the closing paren; the cursor is now outside the call.
|
|
69
|
+
state = state.update({
|
|
70
|
+
changes: { from: 9, insert: ")" },
|
|
71
|
+
selection: { anchor: 10 },
|
|
72
|
+
}).state;
|
|
73
|
+
expect(state.field(signatureHintField)).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("dismisses the tooltip when the anchored call closes inside grouping parens", () => {
|
|
77
|
+
// Regression for the `(plt.plot())` case: the outer grouping paren must not
|
|
78
|
+
// keep the (now-closed) plt.plot hint alive.
|
|
79
|
+
let state = stateWithHint("(plt.plot(", 10);
|
|
80
|
+
// Close plt.plot's call; the outer `(` is still open but we've left the
|
|
81
|
+
// anchored call.
|
|
82
|
+
state = state.update({
|
|
83
|
+
changes: { from: 10, insert: ")" },
|
|
84
|
+
selection: { anchor: 11 },
|
|
85
|
+
}).state;
|
|
86
|
+
expect(state.field(signatureHintField)).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("keeps the tooltip while typing a nested call inside the anchored call", () => {
|
|
90
|
+
// Cursor inside the anchored call of `f(g(<cursor>`; opening/typing a nested
|
|
91
|
+
// call stays inside the anchored call, so the hint should remain.
|
|
92
|
+
let state = stateWithHint("f(g(", 4);
|
|
93
|
+
state = state.update({
|
|
94
|
+
changes: { from: 4, insert: "x(" },
|
|
95
|
+
selection: { anchor: 6 },
|
|
96
|
+
}).state;
|
|
97
|
+
expect(state.field(signatureHintField)?.pos).toBe(4);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("keeps the tooltip when a nested call closes inside the anchored call", () => {
|
|
101
|
+
const anchor = "f(".length;
|
|
102
|
+
let state = stateWithHint("f(g(x", anchor);
|
|
103
|
+
state = state.update({
|
|
104
|
+
changes: { from: 5, insert: ")" },
|
|
105
|
+
selection: { anchor: 6 },
|
|
106
|
+
}).state;
|
|
107
|
+
expect(state.field(signatureHintField)?.pos).toBe(anchor);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("dismisses the tooltip when the closing paren is typed in a large multi-line call", () => {
|
|
111
|
+
const anchor = "f(".length;
|
|
112
|
+
const prefix = `f(\n${" x,\n".repeat(25)}`;
|
|
113
|
+
let state = EditorState.create({
|
|
114
|
+
doc: prefix,
|
|
115
|
+
selection: { anchor: prefix.length },
|
|
116
|
+
extensions: [signatureHintField],
|
|
117
|
+
});
|
|
118
|
+
state = state.update({
|
|
119
|
+
effects: setSignatureHintEffect.of(fakeTooltip(anchor)),
|
|
120
|
+
}).state;
|
|
121
|
+
const head = prefix.length;
|
|
122
|
+
state = state.update({
|
|
123
|
+
changes: { from: head, insert: ")" },
|
|
124
|
+
selection: { anchor: head + 1 },
|
|
125
|
+
}).state;
|
|
126
|
+
expect(state.field(signatureHintField)).toBeNull();
|
|
127
|
+
});
|
|
60
128
|
});
|
|
61
129
|
|
|
62
130
|
describe("asSignatureHint", () => {
|
|
@@ -92,3 +160,29 @@ describe("asSignatureHint", () => {
|
|
|
92
160
|
expect(wrapped.above).toBe(true);
|
|
93
161
|
});
|
|
94
162
|
});
|
|
163
|
+
|
|
164
|
+
describe("closeSignatureHint", () => {
|
|
165
|
+
it("returns false when no hint is showing", () => {
|
|
166
|
+
const view = new EditorView({
|
|
167
|
+
state: EditorState.create({ extensions: [signatureHintField] }),
|
|
168
|
+
});
|
|
169
|
+
expect(closeSignatureHint(view)).toBe(false);
|
|
170
|
+
expect(view.state.field(signatureHintField)).toBeNull();
|
|
171
|
+
view.destroy();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("dismisses the hint and returns true when one is showing", () => {
|
|
175
|
+
const view = new EditorView({
|
|
176
|
+
state: EditorState.create({
|
|
177
|
+
doc: "plt.plot(",
|
|
178
|
+
extensions: [signatureHintField],
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
view.dispatch({ effects: setSignatureHintEffect.of(fakeTooltip(9)) });
|
|
182
|
+
expect(view.state.field(signatureHintField)?.pos).toBe(9);
|
|
183
|
+
|
|
184
|
+
expect(closeSignatureHint(view)).toBe(true);
|
|
185
|
+
expect(view.state.field(signatureHintField)).toBeNull();
|
|
186
|
+
view.destroy();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -1,12 +1,61 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import type { EditorState } from "@codemirror/state";
|
|
2
3
|
import { StateEffect, StateField } from "@codemirror/state";
|
|
3
|
-
import { showTooltip, type Tooltip } from "@codemirror/view";
|
|
4
|
+
import { type EditorView, showTooltip, type Tooltip } from "@codemirror/view";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Effect to set (or clear, with `null`) the floating signature hint.
|
|
7
8
|
*/
|
|
8
9
|
export const setSignatureHintEffect = StateEffect.define<Tooltip | null>();
|
|
9
10
|
|
|
11
|
+
// Bound the scan so large cells stay cheap on every keystroke.
|
|
12
|
+
const MAX_LINES_BACK = 20;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether the cursor is still inside the anchored call (just inside its `(`).
|
|
16
|
+
*
|
|
17
|
+
* Anchor-relative paren scan, bounded to {@link MAX_LINES_BACK} lines.
|
|
18
|
+
* Good enough for hint dismissal — not a full parse (ignores strings/comments).
|
|
19
|
+
*/
|
|
20
|
+
function isCursorInsideAnchoredCall(options: {
|
|
21
|
+
state: EditorState;
|
|
22
|
+
anchor: number;
|
|
23
|
+
head: number;
|
|
24
|
+
}): boolean {
|
|
25
|
+
const { state, anchor, head } = options;
|
|
26
|
+
if (head < anchor) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const headLine = state.doc.lineAt(head).number;
|
|
31
|
+
const anchorLine = state.doc.lineAt(anchor).number;
|
|
32
|
+
const startLine = Math.max(anchorLine, headLine - MAX_LINES_BACK + 1);
|
|
33
|
+
const from = Math.max(anchor, state.doc.line(startLine).from);
|
|
34
|
+
|
|
35
|
+
// If the anchor is outside the bounded window, assume its `(` is still open.
|
|
36
|
+
const assumedOpen = from > anchor;
|
|
37
|
+
let balance = assumedOpen ? 1 : 0;
|
|
38
|
+
const iter = state.doc.iterRange(from, head);
|
|
39
|
+
for (;;) {
|
|
40
|
+
const { value, done } = iter.next();
|
|
41
|
+
if (done) {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
for (const char of value) {
|
|
45
|
+
if (char === "(") {
|
|
46
|
+
balance++;
|
|
47
|
+
} else if (char === ")") {
|
|
48
|
+
balance--;
|
|
49
|
+
const closed = assumedOpen ? balance <= 0 : balance < 0;
|
|
50
|
+
if (closed) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
10
59
|
/**
|
|
11
60
|
* Wrap a tooltip so it renders like the completion popup's info box.
|
|
12
61
|
*
|
|
@@ -34,13 +83,7 @@ export function asSignatureHint(tooltip: Tooltip): Tooltip {
|
|
|
34
83
|
* Holds the floating "signature hint" shown after typing `(` or `,` inside a
|
|
35
84
|
* call on the non-LSP (Jedi) completion path.
|
|
36
85
|
*
|
|
37
|
-
* The LSP path has its own signature help; this fills the gap for users
|
|
38
|
-
* without a language server. The completion source (`pythonCompletionSource`)
|
|
39
|
-
* drives it: it dispatches `setSignatureHintEffect` with the tooltip when the
|
|
40
|
-
* backend returns a signature and with `null` otherwise. The hint is also
|
|
41
|
-
* cleared when the cursor moves via a selection-only change (e.g. clicking
|
|
42
|
-
* away or arrowing out of the call), and kept anchored across edits so it
|
|
43
|
-
* doesn't flicker while a fresh result is in flight.
|
|
86
|
+
* The LSP path has its own signature help; this fills the gap for users without a language server.
|
|
44
87
|
*/
|
|
45
88
|
export const signatureHintField = StateField.define<Tooltip | null>({
|
|
46
89
|
create: () => null,
|
|
@@ -57,12 +100,36 @@ export const signatureHintField = StateField.define<Tooltip | null>({
|
|
|
57
100
|
if (tr.selection && !tr.docChanged) {
|
|
58
101
|
return null;
|
|
59
102
|
}
|
|
60
|
-
//
|
|
61
|
-
//
|
|
103
|
+
// Dismiss once the cursor leaves the anchored call (e.g. the closing paren
|
|
104
|
+
// is typed). Otherwise keep the hint anchored across edits so it doesn't
|
|
105
|
+
// flicker while a fresh result is in flight; the completion source refreshes
|
|
106
|
+
// or clears it as results arrive.
|
|
62
107
|
if (tr.docChanged) {
|
|
63
|
-
|
|
108
|
+
const anchor = tr.changes.mapPos(tooltip.pos);
|
|
109
|
+
if (
|
|
110
|
+
!isCursorInsideAnchoredCall({
|
|
111
|
+
state: tr.state,
|
|
112
|
+
anchor,
|
|
113
|
+
head: tr.state.selection.main.head,
|
|
114
|
+
})
|
|
115
|
+
) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return { ...tooltip, pos: anchor };
|
|
64
119
|
}
|
|
65
120
|
return tooltip;
|
|
66
121
|
},
|
|
67
122
|
provide: (field) => showTooltip.from(field),
|
|
68
123
|
});
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Dismiss the floating signature hint if one is showing.
|
|
127
|
+
* Returns `true` if a hint was dismissed.
|
|
128
|
+
*/
|
|
129
|
+
export function closeSignatureHint(view: EditorView): boolean {
|
|
130
|
+
if (view.state.field(signatureHintField, false)) {
|
|
131
|
+
view.dispatch({ effects: setSignatureHintEffect.of(null) });
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
package/src/core/constants.ts
CHANGED
|
@@ -50,6 +50,12 @@ export const KnownQueryParams = {
|
|
|
50
50
|
* If false, the chrome will be hidden.
|
|
51
51
|
*/
|
|
52
52
|
showChrome: "show-chrome",
|
|
53
|
+
/**
|
|
54
|
+
* Override the display theme: `light`, `dark`, or `system`.
|
|
55
|
+
* Takes precedence over the notebook's saved `display.theme`.
|
|
56
|
+
* Ignored for embedded islands, which infer the theme from their host page.
|
|
57
|
+
*/
|
|
58
|
+
theme: "theme",
|
|
53
59
|
};
|
|
54
60
|
|
|
55
61
|
/**
|