Skip to content

Commit 73a2a7e

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/multi-pane-view
2 parents 1c478aa + 0a5237a commit 73a2a7e

29 files changed

Lines changed: 1543 additions & 147 deletions

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cm/lineNumberSelection.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ type LineNumberClickEvent = Pick<
1414
| "defaultPrevented"
1515
>;
1616

17+
interface LineNumberClickOptions {
18+
shiftClickSelection?: boolean;
19+
}
20+
1721
function toDocumentOffset(
1822
value: number | null | undefined,
1923
fallback = 0,
@@ -91,6 +95,7 @@ export function handleLineNumberClick(
9195
view: EditorView | null | undefined,
9296
line: LineInfo,
9397
event: LineNumberClickEvent | null | undefined,
98+
options: LineNumberClickOptions = {},
9499
): boolean {
95100
if (!view || !event || event.defaultPrevented) return false;
96101
if ((event.button ?? 0) !== 0) return false;
@@ -100,13 +105,15 @@ export function handleLineNumberClick(
100105

101106
const range = getLineSelectionRange(view.state, line);
102107
if (!range) return false;
108+
const extendSelection =
109+
event.shiftKey && options.shiftClickSelection !== false;
103110

104111
event.preventDefault();
105112
view.dispatch({
106-
selection: event.shiftKey
113+
selection: extendSelection
107114
? createExtendedLineSelection(view.state, range)
108115
: createLineSelection(range),
109-
userEvent: event.shiftKey ? "select.extend.pointer" : "select.pointer",
116+
userEvent: extendSelection ? "select.extend.pointer" : "select.pointer",
110117
});
111118
view.focus();
112119
return true;

src/cm/mainEditorExtensions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ interface MainEditorExtensionOptions {
88
themeExtension?: Extension;
99
pointerCursorVisibilityExtension?: Extension;
1010
shiftClickSelectionExtension?: Extension;
11+
multiCursorSelectionExtension?: Extension;
1112
touchSelectionUpdateExtension?: Extension;
13+
quickToolsModifierInputExtension?: Extension;
1214
searchExtension?: Extension;
1315
readOnlyExtension?: Extension;
1416
optionExtensions?: Extension[];
@@ -46,7 +48,9 @@ export function createMainEditorExtensions(
4648
extensions.push(fixedHeightTheme);
4749
pushExtension(extensions, options.pointerCursorVisibilityExtension);
4850
pushExtension(extensions, options.shiftClickSelectionExtension);
51+
pushExtension(extensions, options.multiCursorSelectionExtension);
4952
pushExtension(extensions, options.touchSelectionUpdateExtension);
53+
pushExtension(extensions, options.quickToolsModifierInputExtension);
5054
pushExtension(extensions, options.searchExtension);
5155
pushExtension(extensions, options.readOnlyExtension);
5256

src/cm/quickToolsModifierInput.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Extension } from "@codemirror/state";
2+
import { EditorView, type EditorView as CodeMirrorEditorView } from "@codemirror/view";
3+
4+
type QuickToolsModifierInputHandler = (
5+
view: CodeMirrorEditorView,
6+
text: string,
7+
) => boolean | void;
8+
9+
let handleTextInput: QuickToolsModifierInputHandler = () => false;
10+
11+
export function setQuickToolsModifierInputHandler(
12+
handler: QuickToolsModifierInputHandler,
13+
): void {
14+
handleTextInput = typeof handler === "function" ? handler : () => false;
15+
}
16+
17+
export default function quickToolsModifierInput(): Extension {
18+
return EditorView.inputHandler.of((view, _from, _to, text) => {
19+
return !!handleTextInput(view, text);
20+
});
21+
}

src/cm/quickToolsModifierKeys.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
interface QuickToolModifiers {
2+
ctrlKey?: boolean;
3+
altKey?: boolean;
4+
shiftKey?: boolean;
5+
metaKey?: boolean;
6+
}
7+
8+
interface QuickToolCommand {
9+
name: string;
10+
key?: string | null;
11+
}
12+
13+
type ModifierName = "Ctrl" | "Alt" | "Shift" | "Meta";
14+
15+
const modifierAliases: Record<string, ModifierName> = {
16+
ctrl: "Ctrl",
17+
control: "Ctrl",
18+
alt: "Alt",
19+
option: "Alt",
20+
shift: "Shift",
21+
meta: "Meta",
22+
cmd: "Meta",
23+
command: "Meta",
24+
mod: "Ctrl",
25+
};
26+
27+
const modifierOrder: ModifierName[] = ["Ctrl", "Alt", "Shift", "Meta"];
28+
29+
export function mapQuickToolShiftText(char: string | null | undefined): string {
30+
switch (char) {
31+
case "1":
32+
return "!";
33+
case "2":
34+
return "@";
35+
case "3":
36+
return "#";
37+
case "4":
38+
return "$";
39+
case "5":
40+
return "%";
41+
case "6":
42+
return "^";
43+
case "7":
44+
return "&";
45+
case "8":
46+
return "*";
47+
case "9":
48+
return "(";
49+
case "0":
50+
return ")";
51+
case "-":
52+
return "_";
53+
case "=":
54+
return "+";
55+
case "[":
56+
return "{";
57+
case "]":
58+
return "}";
59+
case "\\":
60+
return "|";
61+
case ";":
62+
return ":";
63+
case "'":
64+
return '"';
65+
case ",":
66+
return "<";
67+
case ".":
68+
return ">";
69+
case "/":
70+
return "?";
71+
default:
72+
return String(char ?? "").toUpperCase();
73+
}
74+
}
75+
76+
export function getQuickToolCombo(
77+
key: string,
78+
modifiers: QuickToolModifiers = {},
79+
): string | null {
80+
const normalizedKey = normalizeKey(key);
81+
if (!normalizedKey) return null;
82+
83+
const modifierParts = Object.entries(modifiers)
84+
.filter(([, enabled]) => enabled)
85+
.map(([modifier]) => modifier.replace(/Key$/, ""));
86+
return normalizeShortcutCombo([...modifierParts, normalizedKey].join("-"));
87+
}
88+
89+
export function findQuickToolCommand(
90+
commands: QuickToolCommand[],
91+
key: string,
92+
modifiers: QuickToolModifiers = {},
93+
): QuickToolCommand | null {
94+
const combo = getQuickToolCombo(key, modifiers);
95+
if (!combo) return null;
96+
97+
return (
98+
commands.find((command) =>
99+
getShortcutAlternatives(command.key).includes(combo),
100+
) || null
101+
);
102+
}
103+
104+
export function getShortcutAlternatives(
105+
keyString: string | null | undefined,
106+
): string[] {
107+
if (!keyString) return [];
108+
return String(keyString)
109+
.split("|")
110+
.map(normalizeShortcutCombo)
111+
.filter((combo): combo is string => Boolean(combo));
112+
}
113+
114+
export function normalizeShortcutCombo(combo: string): string | null {
115+
if (!combo) return null;
116+
const parts = splitShortcutCombo(combo);
117+
const modifiers: ModifierName[] = [];
118+
let key: string | null = null;
119+
120+
parts.forEach((part) => {
121+
const modifier = modifierAliases[part.toLowerCase()];
122+
if (modifier) {
123+
if (!modifiers.includes(modifier)) modifiers.push(modifier);
124+
return;
125+
}
126+
127+
key = normalizeKey(part);
128+
});
129+
130+
if (!key) return null;
131+
const orderedModifiers = modifierOrder.filter((modifier) =>
132+
modifiers.includes(modifier),
133+
);
134+
return [...orderedModifiers, key].join("-");
135+
}
136+
137+
function splitShortcutCombo(combo: string): string[] {
138+
if (combo.endsWith("-")) {
139+
return [...combo.slice(0, -1).split("-").filter(Boolean), "-"];
140+
}
141+
return combo
142+
.split("-")
143+
.map((part) => part.trim())
144+
.filter(Boolean);
145+
}
146+
147+
function normalizeKey(key: string | null | undefined): string | null {
148+
const text = String(key ?? "");
149+
if (!text) return null;
150+
if (text.length === 1 && /[a-z]/i.test(text)) return text.toUpperCase();
151+
return text;
152+
}

0 commit comments

Comments
 (0)