Skip to content

Commit 50b3703

Browse files
deadlyjackAjit Kumar
andauthored
fix: quicktools ctrl+c not working properly (Acode-Foundation#2737)
Co-authored-by: Ajit Kumar <dellevenjack@gmail>
1 parent e5fa7d5 commit 50b3703

7 files changed

Lines changed: 656 additions & 40 deletions

File tree

src/cm/quickToolsModifierInput.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ import {
55
} from "@codemirror/view";
66
import { blurEditorIfReadOnly, focusEditorIfEditable } from "cm/editorReadOnly";
77

8+
export interface QuickToolsModifierInputContext {
9+
from: number;
10+
to: number;
11+
text: string;
12+
}
13+
814
type QuickToolsModifierInputHandler = (
915
view: CodeMirrorEditorView,
10-
text: string,
16+
input: QuickToolsModifierInputContext,
1117
) => boolean | void;
1218

1319
let handleTextInput: QuickToolsModifierInputHandler = () => false;
@@ -18,6 +24,21 @@ export function setQuickToolsModifierInputHandler(
1824
handleTextInput = typeof handler === "function" ? handler : () => false;
1925
}
2026

27+
/**
28+
* Android may report typing over a selection as an empty deletion followed by
29+
* a separate insertion. Quick-tools modifiers must hold that deletion until
30+
* the character arrives, otherwise shortcuts such as Ctrl+C copy only after
31+
* the selected text has already been removed.
32+
*/
33+
export function isSelectedRangeDeletion(
34+
view: CodeMirrorEditorView,
35+
input: QuickToolsModifierInputContext,
36+
): boolean {
37+
const selection = view?.state?.selection?.main;
38+
if (!selection || selection.empty || input.text !== "") return false;
39+
return input.from <= selection.from && input.to >= selection.to;
40+
}
41+
2142
export function canQuickToolsEdit(view: CodeMirrorEditorView): boolean {
2243
return !view.state.readOnly;
2344
}
@@ -48,8 +69,12 @@ export function finishQuickToolsModifierInput(
4869
}
4970

5071
export default function quickToolsModifierInput(): Extension {
51-
return EditorView.inputHandler.of((view, _from, _to, text) => {
52-
const handled = !!handleTextInput(view, text);
72+
return EditorView.inputHandler.of((view, from, to, text) => {
73+
// When a DOM-derived input is handled without changing state, CodeMirror's
74+
// DOM observer performs its own view.update([]) reconciliation. Dispatching
75+
// here would make that observer think state changed and can leave Android's
76+
// native replacement in the content DOM.
77+
const handled = !!handleTextInput(view, { from, to, text });
5378
return view.state.readOnly || handled;
5479
});
5580
}

src/handlers/quickTools.js

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import {
77
SearchQuery,
88
setSearchQuery,
99
} from "@codemirror/search";
10+
import { EditorView } from "@codemirror/view";
1011
import { executeCommand, getRegisteredCommands } from "cm/commandRegistry";
1112
import { focusEditorIfEditable } from "cm/editorReadOnly";
1213
import {
1314
canQuickToolsEdit,
1415
finishQuickToolsModifierInput,
1516
focusQuickToolsModifierInput,
17+
isSelectedRangeDeletion,
1618
setQuickToolsModifierInputHandler,
1719
} from "cm/quickToolsModifierInput";
1820
import {
@@ -30,6 +32,7 @@ import {
3032
clearModifierState,
3133
clearQuickToolsButtonFeedback,
3234
removeActionStackEntries,
35+
shouldCaptureModifierInput,
3336
} from "./quickToolsState";
3437

3538
export let quickToolUsed = false;
@@ -69,13 +72,12 @@ quickTools.$input.addEventListener("input", (e) => {
6972
if (!key || key.length > 1) return;
7073
const keyCombination = getKeys({ key });
7174
const target = getInput();
75+
const codeMirrorView = getCodeMirrorInputView(target);
7276

73-
if (isCodeMirrorEditorInput(target)) {
74-
handleCodeMirrorQuickToolsTextInput(
75-
editorManager.editor,
76-
key,
77-
keyCombination,
78-
);
77+
if (
78+
codeMirrorView &&
79+
runCodeMirrorQuickToolsTextKey(codeMirrorView, key, keyCombination)
80+
) {
7981
return;
8082
}
8183

@@ -108,20 +110,27 @@ quickTools.$input.addEventListener("keydown", (e) => {
108110
e.preventDefault();
109111

110112
let target = getInput();
111-
if (isCodeMirrorEditorInput(target)) {
113+
const codeMirrorView = getCodeMirrorInputView(target);
114+
if (codeMirrorView) {
112115
try {
113-
runCodeMirrorQuickToolKey(keyCode, keyCombination);
116+
const handled = runQuickToolKey(codeMirrorView, keyCode, keyCombination);
117+
if (!handled && !codeMirrorView.state.readOnly) {
118+
target.dispatchEvent(KeyboardEvent("keydown", keyCombination));
119+
}
114120
} finally {
115-
resetKeys();
116-
dismissReadOnlyQuickToolsInput(editorManager.editor);
121+
if (codeMirrorView.state.readOnly) {
122+
resetKeys();
123+
dismissReadOnlyQuickToolsInput(codeMirrorView);
124+
}
125+
setQuicktoolsUsed();
117126
}
118127
return;
119128
}
120129
if (target === quickTools.$input) {
121-
target = editorManager.editor.contentDOM;
130+
target = editorManager.editor?.contentDOM;
122131
}
123132

124-
target.dispatchEvent(KeyboardEvent("keydown", keyCombination));
133+
target?.dispatchEvent(KeyboardEvent("keydown", keyCombination));
125134
setQuicktoolsUsed();
126135
});
127136

@@ -244,7 +253,9 @@ export function cancelQuickToolsModifierInput() {
244253
const changed = clearQuickToolsModifierState();
245254
quickTools.$input.value = "";
246255
quickTools.$input.blur();
247-
dismissReadOnlyQuickToolsInput(editorManager.editor);
256+
dismissReadOnlyQuickToolsInput(
257+
getCodeMirrorInputView(input) || editorManager.editor,
258+
);
248259
return changed;
249260
}
250261

@@ -266,10 +277,19 @@ export default function actions(action, value) {
266277
state[action] = value;
267278
events[action].forEach((cb) => cb(value));
268279
if (Object.values(state).includes(true)) {
269-
if (isCodeMirrorEditorInput(input)) {
270-
focusQuickToolsModifierInput(editor, $input);
280+
const codeMirrorView = getCodeMirrorInputView(input);
281+
const shouldCapture =
282+
codeMirrorView?.state.readOnly ||
283+
shouldCaptureModifierInput(state, Boolean(codeMirrorView));
284+
if (shouldCapture) {
285+
$input.value = "";
286+
if (codeMirrorView?.state.readOnly) {
287+
focusQuickToolsModifierInput(codeMirrorView, $input);
288+
} else {
289+
$input.focus();
290+
}
271291
} else {
272-
$input.focus();
292+
if (codeMirrorView) focusEditorIfEditable(codeMirrorView);
273293
}
274294
} else {
275295
restoreQuickToolsTargetFocus();
@@ -411,27 +431,36 @@ function setInput() {
411431
input = activeElement;
412432
}
413433

414-
function isCodeMirrorEditorInput(target) {
415-
const { editor, activeFile } = editorManager;
416-
if (!editor || activeFile?.type !== "editor") return false;
417-
const contentDOM = editor.contentDOM;
418-
return target === contentDOM || (contentDOM?.contains?.(target) ?? false);
434+
function getCodeMirrorInputView(target) {
435+
if (!(target instanceof HTMLElement)) return null;
436+
const view = EditorView.findFromDOM(target);
437+
if (!view?.contentDOM) return null;
438+
return target === view.contentDOM || view.contentDOM.contains(target)
439+
? view
440+
: null;
419441
}
420442

421443
function runCodeMirrorQuickToolKey(keyCode, keyCombination) {
422-
if (!isCodeMirrorEditorInput(input)) return false;
423-
return runQuickToolKey(editorManager.editor, keyCode, keyCombination);
444+
const view = getCodeMirrorInputView(input);
445+
return view ? runQuickToolKey(view, keyCode, keyCombination) : false;
424446
}
425447

426-
export function handleCodeMirrorQuickToolsTextInput(
427-
view,
428-
text,
429-
modifiers = null,
430-
) {
448+
export function handleCodeMirrorQuickToolsTextInput(view, input) {
449+
if (!Object.values(state).includes(true)) return false;
431450
if (!view?.state || !view.contentDOM) return false;
451+
if (isSelectedRangeDeletion(view, input)) {
452+
setQuicktoolsUsed();
453+
return true;
454+
}
455+
456+
const { text } = input;
432457
if (!text || text.length !== 1) return false;
433458

434-
const keyCombination = modifiers || getKeys({ key: text });
459+
const keyCombination = getKeys({ key: text });
460+
return runCodeMirrorQuickToolsTextKey(view, text, keyCombination);
461+
}
462+
463+
function runCodeMirrorQuickToolsTextKey(view, text, keyCombination) {
435464
if (!hasQuickToolsModifier(keyCombination)) return false;
436465
const canEdit = canQuickToolsEdit(view);
437466
resetKeys();
@@ -921,8 +950,7 @@ function blockReadOnlyQuickToolsEdit(view) {
921950
}
922951

923952
function isReadOnlyCodeMirrorInput(target) {
924-
const { editor } = editorManager;
925-
return isCodeMirrorEditorInput(target) && !!editor?.state?.readOnly;
953+
return !!getCodeMirrorInputView(target)?.state.readOnly;
926954
}
927955

928956
function dismissReadOnlyQuickToolsInput(view) {
@@ -931,10 +959,10 @@ function dismissReadOnlyQuickToolsInput(view) {
931959
}
932960

933961
function restoreQuickToolsTargetFocus() {
934-
const { editor } = editorManager;
935-
if (isCodeMirrorEditorInput(input) && editor) {
936-
if (dismissReadOnlyQuickToolsInput(editor)) return;
937-
focusEditorIfEditable(editor);
962+
const codeMirrorView = getCodeMirrorInputView(input);
963+
if (codeMirrorView) {
964+
if (dismissReadOnlyQuickToolsInput(codeMirrorView)) return;
965+
focusEditorIfEditable(codeMirrorView);
938966
return;
939967
}
940968
if (input) {

src/handlers/quickToolsState.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
export const modifierKeys = ["shift", "alt", "ctrl", "meta"];
22

3+
/**
4+
* CodeMirror can safely receive Shift-only text because replacing the current
5+
* selection is intentional. Command modifiers must capture text outside the
6+
* contenteditable so Android keyboards cannot mutate the document first.
7+
*/
8+
export function shouldCaptureModifierInput(state, isCodeMirrorTarget) {
9+
const hasActiveModifier = modifierKeys.some((key) => Boolean(state[key]));
10+
if (!hasActiveModifier) return false;
11+
if (!isCodeMirrorTarget) return true;
12+
return Boolean(state.ctrl || state.alt || state.meta);
13+
}
14+
315
export function clearModifierState(state, events = {}) {
416
let changed = false;
517

0 commit comments

Comments
 (0)