@marimo-team/islands 0.18.2 → 0.18.3
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/{constants-DWBOe162.js → constants-D_G8vnDk.js} +5 -4
- package/dist/{formats-7RSCCoSI.js → formats-4m4HuHTj.js} +1 -1
- package/dist/{glide-data-editor-D-Ia_Jsv.js → glide-data-editor-DXF8E-QD.js} +2 -2
- package/dist/main.js +32 -29
- package/dist/{types-Dunk85GC.js → types-DclGb0Yh.js} +1 -1
- package/dist/{vega-component-kU4hFYYJ.js → vega-component-HUc7bIGs.js} +2 -2
- package/package.json +1 -1
- package/src/components/app-config/user-config-form.tsx +14 -1
- package/src/components/editor/chrome/components/contribute-snippet-button.tsx +22 -103
- package/src/components/editor/controls/duplicate-shortcut-banner.tsx +50 -0
- package/src/components/editor/controls/keyboard-shortcuts.tsx +25 -2
- package/src/components/editor/notebook-cell.tsx +4 -3
- package/src/components/editor/output/__tests__/ansi-reduce.test.ts +6 -6
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +3 -3
- package/src/components/pages/home-page.tsx +6 -0
- package/src/components/scratchpad/scratchpad.tsx +2 -1
- package/src/core/constants.ts +10 -0
- package/src/core/layout/useTogglePresenting.ts +69 -25
- package/src/core/state/__mocks__/mocks.ts +1 -0
- package/src/hooks/__tests__/useDuplicateShortcuts.test.ts +449 -0
- package/src/hooks/useDuplicateShortcuts.ts +145 -0
- package/src/plugins/impl/NumberPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/NumberPlugin.test.tsx +1 -1
- package/src/plugins/layout/NavigationMenuPlugin.tsx +24 -22
- package/src/utils/__tests__/json-parser.test.ts +1 -1
|
@@ -17,6 +17,7 @@ import { HTMLCellId, SCRATCH_CELL_ID } from "@/core/cells/ids";
|
|
|
17
17
|
import { DEFAULT_CELL_NAME } from "@/core/cells/names";
|
|
18
18
|
import type { LanguageAdapterType } from "@/core/codemirror/language/types";
|
|
19
19
|
import { useResolvedMarimoConfig } from "@/core/config/config";
|
|
20
|
+
import { CSSClasses } from "@/core/constants";
|
|
20
21
|
import { useRequestClient } from "@/core/network/requests";
|
|
21
22
|
import type { CellConfig } from "@/core/network/types";
|
|
22
23
|
import { LazyAnyLanguageCodeMirror } from "@/plugins/impl/code/LazyAnyLanguageCodeMirror";
|
|
@@ -149,7 +150,7 @@ export const ScratchPad: React.FC = () => {
|
|
|
149
150
|
<OutputArea
|
|
150
151
|
allowExpand={false}
|
|
151
152
|
output={output}
|
|
152
|
-
className=
|
|
153
|
+
className={CSSClasses.outputArea}
|
|
153
154
|
cellId={cellId}
|
|
154
155
|
stale={false}
|
|
155
156
|
loading={false}
|
package/src/core/constants.ts
CHANGED
|
@@ -50,3 +50,13 @@ export const KnownQueryParams = {
|
|
|
50
50
|
*/
|
|
51
51
|
showChrome: "show-chrome",
|
|
52
52
|
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* CSS class names used throughout the application
|
|
56
|
+
*/
|
|
57
|
+
export const CSSClasses = {
|
|
58
|
+
/**
|
|
59
|
+
* Class name for cell output areas
|
|
60
|
+
*/
|
|
61
|
+
outputArea: "output-area",
|
|
62
|
+
};
|
|
@@ -2,47 +2,91 @@
|
|
|
2
2
|
|
|
3
3
|
import { useSetAtom } from "jotai";
|
|
4
4
|
import { useCallback } from "react";
|
|
5
|
+
import { Logger } from "@/utils/Logger";
|
|
5
6
|
import { type CellId, HTMLCellId } from "../cells/ids";
|
|
7
|
+
import { CSSClasses } from "../constants";
|
|
6
8
|
import { toggleAppMode, viewStateAtom } from "../mode";
|
|
7
9
|
|
|
10
|
+
interface ScrollAnchor {
|
|
11
|
+
cellId: CellId;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function findScrollAnchor(): ScrollAnchor | null {
|
|
15
|
+
const outputAreas = document.getElementsByClassName(CSSClasses.outputArea);
|
|
16
|
+
|
|
17
|
+
for (const elem of Array.from(outputAreas)) {
|
|
18
|
+
const rect = elem.getBoundingClientRect();
|
|
19
|
+
|
|
20
|
+
// Find first visible output area
|
|
21
|
+
if (rect.bottom > 0 && rect.top < window.innerHeight) {
|
|
22
|
+
const cellEl = HTMLCellId.findElement(elem);
|
|
23
|
+
if (!cellEl) {
|
|
24
|
+
Logger.warn("Could not find HTMLCellId for visible output area", elem);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
cellId: HTMLCellId.parse(cellEl.id),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Logger.warn("No visible output area found for scroll anchor");
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function restoreScrollPosition(anchor: ScrollAnchor | null): void {
|
|
38
|
+
if (!anchor) {
|
|
39
|
+
Logger.warn("No scroll anchor provided to restore scroll position");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Find the cell element
|
|
44
|
+
const cellElement = document.getElementById(HTMLCellId.create(anchor.cellId));
|
|
45
|
+
if (!cellElement) {
|
|
46
|
+
Logger.warn(
|
|
47
|
+
"Could not find cell element to restore scroll position",
|
|
48
|
+
anchor.cellId,
|
|
49
|
+
);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Find its output area
|
|
54
|
+
const outputArea = cellElement.querySelector(`.${CSSClasses.outputArea}`);
|
|
55
|
+
if (!outputArea) {
|
|
56
|
+
Logger.warn(
|
|
57
|
+
"Could not find output area to restore scroll position",
|
|
58
|
+
anchor.cellId,
|
|
59
|
+
);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Adjust scroll to restore visual position
|
|
64
|
+
cellElement.scrollIntoView({ block: "start", behavior: "auto" });
|
|
65
|
+
}
|
|
66
|
+
|
|
8
67
|
/**
|
|
9
68
|
* Toggle the notebook's presentation state and scroll to current visible cell
|
|
10
69
|
*/
|
|
11
70
|
export function useTogglePresenting() {
|
|
12
71
|
const setViewState = useSetAtom(viewStateAtom);
|
|
13
72
|
|
|
14
|
-
// Toggle the array's presenting state
|
|
73
|
+
// Toggle the array's presenting state and preserve scroll position
|
|
15
74
|
const togglePresenting = useCallback(() => {
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
window.innerHeight || document.documentElement.clientHeight;
|
|
19
|
-
let cellAnchor: CellId | null = null;
|
|
20
|
-
|
|
21
|
-
// Find the first output area that is visible
|
|
22
|
-
// eslint-disable-next-line unicorn/prefer-spread
|
|
23
|
-
for (const elem of Array.from(outputAreas)) {
|
|
24
|
-
const rect = elem.getBoundingClientRect();
|
|
25
|
-
if (
|
|
26
|
-
(rect.top >= 0 && rect.top <= viewportEnd) ||
|
|
27
|
-
(rect.bottom >= 0 && rect.bottom <= viewportEnd)
|
|
28
|
-
) {
|
|
29
|
-
cellAnchor = HTMLCellId.parse(
|
|
30
|
-
(elem.parentNode as HTMLElement).id as HTMLCellId,
|
|
31
|
-
);
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
75
|
+
// Capture scroll anchor BEFORE toggle
|
|
76
|
+
const scrollAnchor = findScrollAnchor();
|
|
35
77
|
|
|
78
|
+
// Toggle the mode
|
|
36
79
|
setViewState((prev) => ({
|
|
37
80
|
mode: toggleAppMode(prev.mode),
|
|
38
|
-
cellAnchor:
|
|
81
|
+
cellAnchor: scrollAnchor?.cellId ?? null,
|
|
39
82
|
}));
|
|
40
83
|
|
|
84
|
+
// Restore scroll position AFTER DOM updates
|
|
85
|
+
// Double RAF ensures React commits changes and browser completes layout
|
|
41
86
|
requestAnimationFrame(() => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
document.getElementById(HTMLCellId.create(cellAnchor))?.scrollIntoView();
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
restoreScrollPosition(scrollAnchor);
|
|
89
|
+
});
|
|
46
90
|
});
|
|
47
91
|
}, [setViewState]);
|
|
48
92
|
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
type Hotkey,
|
|
5
|
+
type HotkeyAction,
|
|
6
|
+
HotkeyProvider,
|
|
7
|
+
} from "@/core/hotkeys/hotkeys";
|
|
8
|
+
import {
|
|
9
|
+
findDuplicateShortcuts,
|
|
10
|
+
normalizeShortcutKey,
|
|
11
|
+
} from "../useDuplicateShortcuts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Helper to create a minimal hotkey configuration for testing.
|
|
15
|
+
*/
|
|
16
|
+
function createHotkeys(
|
|
17
|
+
keys: Partial<Record<HotkeyAction, Hotkey>>,
|
|
18
|
+
): Record<HotkeyAction, Hotkey> {
|
|
19
|
+
return new Proxy(keys as Record<HotkeyAction, Hotkey>, {
|
|
20
|
+
// biome-ignore lint: ok to have three arguments here (It's a web API)
|
|
21
|
+
get(target, p, receiver) {
|
|
22
|
+
const key = Reflect.get(target, p, receiver);
|
|
23
|
+
if (key === "undefined") {
|
|
24
|
+
throw new Error("Missing required hotkey.");
|
|
25
|
+
}
|
|
26
|
+
return key;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("normalizeShortcutKey", () => {
|
|
32
|
+
it("should convert to lowercase", () => {
|
|
33
|
+
expect(normalizeShortcutKey("Ctrl-Shift-A")).toBe("ctrl-shift-a");
|
|
34
|
+
expect(normalizeShortcutKey("MOD-ENTER")).toBe("mod-enter");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should replace + with -", () => {
|
|
38
|
+
expect(normalizeShortcutKey("Ctrl+Shift+A")).toBe("ctrl-shift-a");
|
|
39
|
+
expect(normalizeShortcutKey("Cmd+Enter")).toBe("cmd-enter");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should trim whitespace", () => {
|
|
43
|
+
expect(normalizeShortcutKey(" Ctrl-A ")).toBe("ctrl-a");
|
|
44
|
+
expect(normalizeShortcutKey(" Mod-Enter ")).toBe("mod-enter");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should handle mixed separators", () => {
|
|
48
|
+
expect(normalizeShortcutKey("Ctrl+Shift-A")).toBe("ctrl-shift-a");
|
|
49
|
+
expect(normalizeShortcutKey("Mod-Alt+K")).toBe("mod-alt-k");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("findDuplicateShortcuts", () => {
|
|
54
|
+
it("should detect no duplicates when all shortcuts are unique", () => {
|
|
55
|
+
const hotkeys = createHotkeys({
|
|
56
|
+
"cell.run": {
|
|
57
|
+
name: "Run cell",
|
|
58
|
+
group: "Running Cells",
|
|
59
|
+
key: "Mod-Enter",
|
|
60
|
+
},
|
|
61
|
+
"cell.format": {
|
|
62
|
+
name: "Format cell",
|
|
63
|
+
group: "Editing",
|
|
64
|
+
key: "Mod-b",
|
|
65
|
+
},
|
|
66
|
+
"cell.delete": {
|
|
67
|
+
name: "Delete cell",
|
|
68
|
+
group: "Editing",
|
|
69
|
+
key: "Shift-Backspace",
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const provider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
74
|
+
const result = findDuplicateShortcuts(provider);
|
|
75
|
+
|
|
76
|
+
expect(result.duplicates).toHaveLength(0);
|
|
77
|
+
expect(result.hasDuplicate("cell.run")).toBe(false);
|
|
78
|
+
expect(result.hasDuplicate("cell.format")).toBe(false);
|
|
79
|
+
expect(result.hasDuplicate("cell.delete")).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should detect duplicates when two actions share the same key", () => {
|
|
83
|
+
const hotkeys = createHotkeys({
|
|
84
|
+
"cell.format": {
|
|
85
|
+
name: "Format cell",
|
|
86
|
+
group: "Editing",
|
|
87
|
+
key: "Mod-b",
|
|
88
|
+
},
|
|
89
|
+
"markdown.bold": {
|
|
90
|
+
name: "Bold",
|
|
91
|
+
group: "Markdown",
|
|
92
|
+
key: "Mod-b",
|
|
93
|
+
},
|
|
94
|
+
"cell.run": {
|
|
95
|
+
name: "Run cell",
|
|
96
|
+
group: "Running Cells",
|
|
97
|
+
key: "Mod-Enter",
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const provider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
102
|
+
const result = findDuplicateShortcuts(provider);
|
|
103
|
+
|
|
104
|
+
expect(result.duplicates).toHaveLength(1);
|
|
105
|
+
expect(result.duplicates[0].key).toBe("cmd-b");
|
|
106
|
+
expect(result.duplicates[0].actions).toHaveLength(2);
|
|
107
|
+
|
|
108
|
+
expect(result.hasDuplicate("cell.format")).toBe(true);
|
|
109
|
+
expect(result.hasDuplicate("markdown.bold")).toBe(true);
|
|
110
|
+
expect(result.hasDuplicate("cell.run")).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should detect multiple duplicate groups", () => {
|
|
114
|
+
const hotkeys = createHotkeys({
|
|
115
|
+
"cell.format": {
|
|
116
|
+
name: "Format cell",
|
|
117
|
+
group: "Editing",
|
|
118
|
+
key: "Mod-b",
|
|
119
|
+
},
|
|
120
|
+
"markdown.bold": {
|
|
121
|
+
name: "Bold",
|
|
122
|
+
group: "Markdown",
|
|
123
|
+
key: "Mod-b",
|
|
124
|
+
},
|
|
125
|
+
"cell.run": {
|
|
126
|
+
name: "Run cell",
|
|
127
|
+
group: "Running Cells",
|
|
128
|
+
key: "Mod-Enter",
|
|
129
|
+
},
|
|
130
|
+
"cell.complete": {
|
|
131
|
+
name: "Code completion",
|
|
132
|
+
group: "Editing",
|
|
133
|
+
key: "Ctrl-Space",
|
|
134
|
+
},
|
|
135
|
+
"cell.signatureHelp": {
|
|
136
|
+
name: "Signature help",
|
|
137
|
+
group: "Editing",
|
|
138
|
+
key: "Mod-Enter",
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const provider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
143
|
+
const result = findDuplicateShortcuts(provider);
|
|
144
|
+
|
|
145
|
+
expect(result.duplicates).toHaveLength(2);
|
|
146
|
+
|
|
147
|
+
// Check that both duplicate groups are detected
|
|
148
|
+
const duplicateKeys = result.duplicates.map((d) => d.key).sort();
|
|
149
|
+
expect(duplicateKeys).toEqual(["cmd-b", "cmd-enter"]);
|
|
150
|
+
|
|
151
|
+
expect(result.hasDuplicate("cell.format")).toBe(true);
|
|
152
|
+
expect(result.hasDuplicate("markdown.bold")).toBe(true);
|
|
153
|
+
expect(result.hasDuplicate("cell.run")).toBe(true);
|
|
154
|
+
expect(result.hasDuplicate("cell.signatureHelp")).toBe(true);
|
|
155
|
+
expect(result.hasDuplicate("cell.complete")).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should handle three or more actions with the same key", () => {
|
|
159
|
+
const hotkeys = createHotkeys({
|
|
160
|
+
"cell.format": {
|
|
161
|
+
name: "Format cell",
|
|
162
|
+
group: "Editing",
|
|
163
|
+
key: "Mod-k",
|
|
164
|
+
},
|
|
165
|
+
"markdown.link": {
|
|
166
|
+
name: "Convert to Link",
|
|
167
|
+
group: "Markdown",
|
|
168
|
+
key: "Mod-k",
|
|
169
|
+
},
|
|
170
|
+
"global.commandPalette": {
|
|
171
|
+
name: "Show command palette",
|
|
172
|
+
group: "Other",
|
|
173
|
+
key: "Mod-k",
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const provider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
178
|
+
const result = findDuplicateShortcuts(provider);
|
|
179
|
+
|
|
180
|
+
expect(result.duplicates).toHaveLength(1);
|
|
181
|
+
expect(result.duplicates[0].actions).toHaveLength(3);
|
|
182
|
+
expect(result.duplicates[0].key).toBe("cmd-k");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should ignore empty or unset shortcuts", () => {
|
|
186
|
+
const hotkeys = createHotkeys({
|
|
187
|
+
"cell.run": {
|
|
188
|
+
name: "Run cell",
|
|
189
|
+
group: "Running Cells",
|
|
190
|
+
key: "Mod-Enter",
|
|
191
|
+
},
|
|
192
|
+
"global.runAll": {
|
|
193
|
+
name: "Re-run all cells",
|
|
194
|
+
group: "Running Cells",
|
|
195
|
+
key: "",
|
|
196
|
+
},
|
|
197
|
+
"cell.format": {
|
|
198
|
+
name: "Format cell",
|
|
199
|
+
group: "Editing",
|
|
200
|
+
key: "Mod-b",
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const provider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
205
|
+
const result = findDuplicateShortcuts(provider);
|
|
206
|
+
|
|
207
|
+
expect(result.duplicates).toHaveLength(0);
|
|
208
|
+
expect(result.hasDuplicate("global.runAll")).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should normalize keys for comparison (case and separator insensitive)", () => {
|
|
212
|
+
const hotkeys = createHotkeys({
|
|
213
|
+
"cell.format": {
|
|
214
|
+
name: "Format cell",
|
|
215
|
+
group: "Editing",
|
|
216
|
+
key: "Cmd-B",
|
|
217
|
+
},
|
|
218
|
+
"markdown.bold": {
|
|
219
|
+
name: "Bold",
|
|
220
|
+
group: "Markdown",
|
|
221
|
+
key: "cmd+b",
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const provider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
226
|
+
const result = findDuplicateShortcuts(provider);
|
|
227
|
+
|
|
228
|
+
expect(result.duplicates).toHaveLength(1);
|
|
229
|
+
expect(result.duplicates[0].actions).toHaveLength(2);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("getDuplicatesFor should return other actions with the same key", () => {
|
|
233
|
+
const hotkeys = createHotkeys({
|
|
234
|
+
"cell.format": {
|
|
235
|
+
name: "Format cell",
|
|
236
|
+
group: "Editing",
|
|
237
|
+
key: "Mod-b",
|
|
238
|
+
},
|
|
239
|
+
"markdown.bold": {
|
|
240
|
+
name: "Bold",
|
|
241
|
+
group: "Markdown",
|
|
242
|
+
key: "Mod-b",
|
|
243
|
+
},
|
|
244
|
+
"markdown.italic": {
|
|
245
|
+
name: "Italic",
|
|
246
|
+
group: "Markdown",
|
|
247
|
+
key: "Mod-i",
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const provider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
252
|
+
const result = findDuplicateShortcuts(provider);
|
|
253
|
+
|
|
254
|
+
const duplicatesForFormat = result.getDuplicatesFor("cell.format");
|
|
255
|
+
expect(duplicatesForFormat).toEqual(["markdown.bold"]);
|
|
256
|
+
|
|
257
|
+
const duplicatesForBold = result.getDuplicatesFor("markdown.bold");
|
|
258
|
+
expect(duplicatesForBold).toEqual(["cell.format"]);
|
|
259
|
+
|
|
260
|
+
const duplicatesForItalic = result.getDuplicatesFor("markdown.italic");
|
|
261
|
+
expect(duplicatesForItalic).toEqual([]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("getDuplicatesFor should return all other actions when three or more share a key", () => {
|
|
265
|
+
const hotkeys = createHotkeys({
|
|
266
|
+
"cell.format": {
|
|
267
|
+
name: "Format cell",
|
|
268
|
+
group: "Editing",
|
|
269
|
+
key: "Mod-k",
|
|
270
|
+
},
|
|
271
|
+
"markdown.link": {
|
|
272
|
+
name: "Convert to Link",
|
|
273
|
+
group: "Markdown",
|
|
274
|
+
key: "Mod-k",
|
|
275
|
+
},
|
|
276
|
+
"global.commandPalette": {
|
|
277
|
+
name: "Show command palette",
|
|
278
|
+
group: "Other",
|
|
279
|
+
key: "Mod-k",
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const provider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
284
|
+
const result = findDuplicateShortcuts(provider);
|
|
285
|
+
|
|
286
|
+
const duplicatesForFormat = result.getDuplicatesFor("cell.format");
|
|
287
|
+
expect(duplicatesForFormat).toHaveLength(2);
|
|
288
|
+
expect(duplicatesForFormat).toContain("markdown.link");
|
|
289
|
+
expect(duplicatesForFormat).toContain("global.commandPalette");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("should respect platform-specific key overrides", () => {
|
|
293
|
+
const hotkeys = createHotkeys({
|
|
294
|
+
"cell.format": {
|
|
295
|
+
name: "Format cell",
|
|
296
|
+
group: "Editing",
|
|
297
|
+
key: {
|
|
298
|
+
main: "Mod-Shift-F",
|
|
299
|
+
mac: "Cmd-Option-F",
|
|
300
|
+
windows: "Ctrl-Alt-F",
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
"markdown.bold": {
|
|
304
|
+
name: "Bold",
|
|
305
|
+
group: "Markdown",
|
|
306
|
+
key: "Cmd-Option-F", // Duplicate on Mac only
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const macProvider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
311
|
+
const macResult = findDuplicateShortcuts(macProvider);
|
|
312
|
+
expect(macResult.duplicates).toHaveLength(1);
|
|
313
|
+
expect(macResult.hasDuplicate("cell.format")).toBe(true);
|
|
314
|
+
expect(macResult.hasDuplicate("markdown.bold")).toBe(true);
|
|
315
|
+
|
|
316
|
+
const windowsProvider = new HotkeyProvider(hotkeys, {
|
|
317
|
+
platform: "windows",
|
|
318
|
+
});
|
|
319
|
+
const windowsResult = findDuplicateShortcuts(windowsProvider);
|
|
320
|
+
expect(windowsResult.duplicates).toHaveLength(0);
|
|
321
|
+
expect(windowsResult.hasDuplicate("cell.format")).toBe(false);
|
|
322
|
+
expect(windowsResult.hasDuplicate("markdown.bold")).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("ignoreGroup parameter", () => {
|
|
326
|
+
it("should ignore duplicates from the specified group", () => {
|
|
327
|
+
const hotkeys = createHotkeys({
|
|
328
|
+
"cell.format": {
|
|
329
|
+
name: "Format cell",
|
|
330
|
+
group: "Editing",
|
|
331
|
+
key: "Mod-b",
|
|
332
|
+
},
|
|
333
|
+
"markdown.bold": {
|
|
334
|
+
name: "Bold",
|
|
335
|
+
group: "Markdown",
|
|
336
|
+
key: "Mod-b",
|
|
337
|
+
},
|
|
338
|
+
"cell.run": {
|
|
339
|
+
name: "Run cell",
|
|
340
|
+
group: "Running Cells",
|
|
341
|
+
key: "Mod-Enter",
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const provider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
346
|
+
const result = findDuplicateShortcuts(provider, "Markdown");
|
|
347
|
+
|
|
348
|
+
// markdown.bold should be ignored, so no duplicates should be found
|
|
349
|
+
expect(result.duplicates).toHaveLength(0);
|
|
350
|
+
expect(result.hasDuplicate("cell.format")).toBe(false);
|
|
351
|
+
expect(result.hasDuplicate("markdown.bold")).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should still detect duplicates outside the ignored group", () => {
|
|
355
|
+
const hotkeys = createHotkeys({
|
|
356
|
+
"cell.format": {
|
|
357
|
+
name: "Format cell",
|
|
358
|
+
group: "Editing",
|
|
359
|
+
key: "Mod-b",
|
|
360
|
+
},
|
|
361
|
+
"markdown.bold": {
|
|
362
|
+
name: "Bold",
|
|
363
|
+
group: "Markdown",
|
|
364
|
+
key: "Mod-b",
|
|
365
|
+
},
|
|
366
|
+
"cell.run": {
|
|
367
|
+
name: "Run cell",
|
|
368
|
+
group: "Running Cells",
|
|
369
|
+
key: "Mod-Enter",
|
|
370
|
+
},
|
|
371
|
+
"cell.complete": {
|
|
372
|
+
name: "Code completion",
|
|
373
|
+
group: "Editing",
|
|
374
|
+
key: "Mod-Enter",
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const provider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
379
|
+
const result = findDuplicateShortcuts(provider, "Markdown");
|
|
380
|
+
|
|
381
|
+
// markdown.bold is ignored, but cell.run and cell.complete should still be detected
|
|
382
|
+
expect(result.duplicates).toHaveLength(1);
|
|
383
|
+
expect(result.duplicates[0].key).toBe("cmd-enter");
|
|
384
|
+
expect(result.hasDuplicate("cell.run")).toBe(true);
|
|
385
|
+
expect(result.hasDuplicate("cell.complete")).toBe(true);
|
|
386
|
+
expect(result.hasDuplicate("markdown.bold")).toBe(false);
|
|
387
|
+
expect(result.hasDuplicate("cell.format")).toBe(false);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("should handle ignoring a group that doesn't exist", () => {
|
|
391
|
+
const hotkeys = createHotkeys({
|
|
392
|
+
"cell.format": {
|
|
393
|
+
name: "Format cell",
|
|
394
|
+
group: "Editing",
|
|
395
|
+
key: "Mod-b",
|
|
396
|
+
},
|
|
397
|
+
"markdown.bold": {
|
|
398
|
+
name: "Bold",
|
|
399
|
+
group: "Markdown",
|
|
400
|
+
key: "Mod-b",
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const provider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
405
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
406
|
+
const result = findDuplicateShortcuts(provider, "NonExistent" as any);
|
|
407
|
+
|
|
408
|
+
// Should still work normally and detect the duplicate
|
|
409
|
+
expect(result.duplicates).toHaveLength(1);
|
|
410
|
+
expect(result.hasDuplicate("cell.format")).toBe(true);
|
|
411
|
+
expect(result.hasDuplicate("markdown.bold")).toBe(true);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("should ignore multiple actions from the same group", () => {
|
|
415
|
+
const hotkeys = createHotkeys({
|
|
416
|
+
"cell.format": {
|
|
417
|
+
name: "Format cell",
|
|
418
|
+
group: "Editing",
|
|
419
|
+
key: "Mod-b",
|
|
420
|
+
},
|
|
421
|
+
"markdown.bold": {
|
|
422
|
+
name: "Bold",
|
|
423
|
+
group: "Markdown",
|
|
424
|
+
key: "Mod-b",
|
|
425
|
+
},
|
|
426
|
+
"markdown.italic": {
|
|
427
|
+
name: "Italic",
|
|
428
|
+
group: "Markdown",
|
|
429
|
+
key: "Mod-i",
|
|
430
|
+
},
|
|
431
|
+
"cell.hideCode": {
|
|
432
|
+
name: "Hide cell code",
|
|
433
|
+
group: "Editing",
|
|
434
|
+
key: "Mod-i",
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const provider = new HotkeyProvider(hotkeys, { platform: "mac" });
|
|
439
|
+
const result = findDuplicateShortcuts(provider, "Markdown");
|
|
440
|
+
|
|
441
|
+
// Both markdown actions should be ignored
|
|
442
|
+
expect(result.duplicates).toHaveLength(0);
|
|
443
|
+
expect(result.hasDuplicate("cell.format")).toBe(false);
|
|
444
|
+
expect(result.hasDuplicate("cell.hideCode")).toBe(false);
|
|
445
|
+
expect(result.hasDuplicate("markdown.bold")).toBe(false);
|
|
446
|
+
expect(result.hasDuplicate("markdown.italic")).toBe(false);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
});
|