Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/cm/lineNumberSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ type LineNumberClickEvent = Pick<
| "defaultPrevented"
>;

interface LineNumberClickOptions {
shiftClickSelection?: boolean;
}

function toDocumentOffset(
value: number | null | undefined,
fallback = 0,
Expand Down Expand Up @@ -91,6 +95,7 @@ export function handleLineNumberClick(
view: EditorView | null | undefined,
line: LineInfo,
event: LineNumberClickEvent | null | undefined,
options: LineNumberClickOptions = {},
): boolean {
if (!view || !event || event.defaultPrevented) return false;
if ((event.button ?? 0) !== 0) return false;
Expand All @@ -100,13 +105,15 @@ export function handleLineNumberClick(

const range = getLineSelectionRange(view.state, line);
if (!range) return false;
const extendSelection =
event.shiftKey && options.shiftClickSelection !== false;

event.preventDefault();
view.dispatch({
selection: event.shiftKey
selection: extendSelection
? createExtendedLineSelection(view.state, range)
: createLineSelection(range),
userEvent: event.shiftKey ? "select.extend.pointer" : "select.pointer",
userEvent: extendSelection ? "select.extend.pointer" : "select.pointer",
});
view.focus();
return true;
Expand Down
4 changes: 4 additions & 0 deletions src/cm/mainEditorExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ interface MainEditorExtensionOptions {
themeExtension?: Extension;
pointerCursorVisibilityExtension?: Extension;
shiftClickSelectionExtension?: Extension;
multiCursorSelectionExtension?: Extension;
touchSelectionUpdateExtension?: Extension;
quickToolsModifierInputExtension?: Extension;
searchExtension?: Extension;
readOnlyExtension?: Extension;
optionExtensions?: Extension[];
Expand Down Expand Up @@ -46,7 +48,9 @@ export function createMainEditorExtensions(
extensions.push(fixedHeightTheme);
pushExtension(extensions, options.pointerCursorVisibilityExtension);
pushExtension(extensions, options.shiftClickSelectionExtension);
pushExtension(extensions, options.multiCursorSelectionExtension);
pushExtension(extensions, options.touchSelectionUpdateExtension);
pushExtension(extensions, options.quickToolsModifierInputExtension);
pushExtension(extensions, options.searchExtension);
pushExtension(extensions, options.readOnlyExtension);

Expand Down
21 changes: 21 additions & 0 deletions src/cm/quickToolsModifierInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Extension } from "@codemirror/state";
import { EditorView, type EditorView as CodeMirrorEditorView } from "@codemirror/view";

type QuickToolsModifierInputHandler = (
view: CodeMirrorEditorView,
text: string,
) => boolean | void;

let handleTextInput: QuickToolsModifierInputHandler = () => false;

export function setQuickToolsModifierInputHandler(
handler: QuickToolsModifierInputHandler,
): void {
handleTextInput = typeof handler === "function" ? handler : () => false;
}

export default function quickToolsModifierInput(): Extension {
return EditorView.inputHandler.of((view, _from, _to, text) => {
return !!handleTextInput(view, text);
});
}
152 changes: 152 additions & 0 deletions src/cm/quickToolsModifierKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
interface QuickToolModifiers {
ctrlKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
metaKey?: boolean;
}

interface QuickToolCommand {
name: string;
key?: string | null;
}

type ModifierName = "Ctrl" | "Alt" | "Shift" | "Meta";

const modifierAliases: Record<string, ModifierName> = {
ctrl: "Ctrl",
control: "Ctrl",
alt: "Alt",
option: "Alt",
shift: "Shift",
meta: "Meta",
cmd: "Meta",
command: "Meta",
mod: "Ctrl",
};

const modifierOrder: ModifierName[] = ["Ctrl", "Alt", "Shift", "Meta"];

export function mapQuickToolShiftText(char: string | null | undefined): string {
switch (char) {
case "1":
return "!";
case "2":
return "@";
case "3":
return "#";
case "4":
return "$";
case "5":
return "%";
case "6":
return "^";
case "7":
return "&";
case "8":
return "*";
case "9":
return "(";
case "0":
return ")";
case "-":
return "_";
case "=":
return "+";
case "[":
return "{";
case "]":
return "}";
case "\\":
return "|";
case ";":
return ":";
case "'":
return '"';
case ",":
return "<";
case ".":
return ">";
case "/":
return "?";
default:
return String(char ?? "").toUpperCase();
}
}

export function getQuickToolCombo(
key: string,
modifiers: QuickToolModifiers = {},
): string | null {
const normalizedKey = normalizeKey(key);
if (!normalizedKey) return null;

const modifierParts = Object.entries(modifiers)
.filter(([, enabled]) => enabled)
.map(([modifier]) => modifier.replace(/Key$/, ""));
return normalizeShortcutCombo([...modifierParts, normalizedKey].join("-"));
}

export function findQuickToolCommand(
commands: QuickToolCommand[],
key: string,
modifiers: QuickToolModifiers = {},
): QuickToolCommand | null {
const combo = getQuickToolCombo(key, modifiers);
if (!combo) return null;

return (
commands.find((command) =>
getShortcutAlternatives(command.key).includes(combo),
) || null
);
}

export function getShortcutAlternatives(
keyString: string | null | undefined,
): string[] {
if (!keyString) return [];
return String(keyString)
.split("|")
.map(normalizeShortcutCombo)
.filter((combo): combo is string => Boolean(combo));
}

export function normalizeShortcutCombo(combo: string): string | null {
if (!combo) return null;
const parts = splitShortcutCombo(combo);
const modifiers: ModifierName[] = [];
let key: string | null = null;

parts.forEach((part) => {
const modifier = modifierAliases[part.toLowerCase()];
if (modifier) {
if (!modifiers.includes(modifier)) modifiers.push(modifier);
return;
}

key = normalizeKey(part);
});

if (!key) return null;
const orderedModifiers = modifierOrder.filter((modifier) =>
modifiers.includes(modifier),
);
return [...orderedModifiers, key].join("-");
}

function splitShortcutCombo(combo: string): string[] {
if (combo.endsWith("-")) {
return [...combo.slice(0, -1).split("-").filter(Boolean), "-"];
}
return combo
.split("-")
.map((part) => part.trim())
.filter(Boolean);
}

function normalizeKey(key: string | null | undefined): string | null {
const text = String(key ?? "");
if (!text) return null;
if (text.length === 1 && /[a-z]/i.test(text)) return text.toUpperCase();
return text;
}
Loading