Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
72 changes: 70 additions & 2 deletions src/cm/touchSelectionMenu.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EditorSelection } from "@codemirror/state";
import selectionMenu from "lib/selectionMenu";

const TAP_MAX_DELAY = 500;
Expand Down Expand Up @@ -126,9 +127,11 @@ class TouchSelectionMenuController {
#view;
#container;
#getActiveFile;
#isShiftSelectionActive;
#stateSyncRaf = 0;
#isScrolling = false;
#isPointerInteracting = false;
#shiftSelectionSession = null;
#menuActive = false;
#menuRequested = false;
#enabled = true;
Expand All @@ -140,6 +143,8 @@ class TouchSelectionMenuController {
this.#container =
options.container || view.dom.closest(".editor-container") || view.dom;
this.#getActiveFile = options.getActiveFile || (() => null);
this.#isShiftSelectionActive =
options.isShiftSelectionActive || (() => false);
this.$menu = document.createElement("menu");
this.$menu.className = "cursor-menu";
this.#bindEvents();
Expand Down Expand Up @@ -170,12 +175,14 @@ class TouchSelectionMenuController {
this.#clearMenuShowTimer();
cancelAnimationFrame(this.#stateSyncRaf);
this.#stateSyncRaf = 0;
this.#shiftSelectionSession = null;
this.#hideMenu(true);
}

setEnabled(enabled) {
this.#enabled = !!enabled;
if (this.#enabled) return;
this.#shiftSelectionSession = null;
this.#menuRequested = false;
this.#isPointerInteracting = false;
this.#isScrolling = false;
Expand Down Expand Up @@ -246,6 +253,7 @@ class TouchSelectionMenuController {

onSessionChanged() {
if (!this.#enabled) return;
this.#shiftSelectionSession = null;
this.#menuRequested = false;
this.#isPointerInteracting = false;
this.#isScrolling = false;
Expand All @@ -265,19 +273,29 @@ class TouchSelectionMenuController {
#onGlobalPointerDown = (event) => {
const target = event.target;
if (this.$menu.contains(target)) return;
if (this.#isIgnoredPointerTarget(target)) return;
if (this.#isIgnoredPointerTarget(target)) {
this.#shiftSelectionSession = null;
return;
}
if (target instanceof Node && this.#view.dom.contains(target)) {
this.#captureShiftSelection(event);
this.#isPointerInteracting = true;
this.#clearMenuShowTimer();
this.#hideMenu();
return;
}
this.#shiftSelectionSession = null;
this.#isPointerInteracting = false;
this.#menuRequested = false;
this.#hideMenu();
};

#onGlobalPointerUp = () => {
#onGlobalPointerUp = (event) => {
if (event.type === "pointerup") {
this.#commitShiftSelection(event);
} else {
this.#shiftSelectionSession = null;
}
if (!this.#isPointerInteracting) return;
this.#isPointerInteracting = false;
if (!this.#enabled) return;
Expand All @@ -291,6 +309,56 @@ class TouchSelectionMenuController {
this.#hideMenu();
};

#captureShiftSelection(event) {
if (!this.#canExtendSelection(event)) {
this.#shiftSelectionSession = null;
return;
}

this.#shiftSelectionSession = {
pointerId: event.pointerId,
anchor: this.#view.state.selection.main.anchor,
x: event.clientX,
y: event.clientY,
};
}

#commitShiftSelection(event) {
const session = this.#shiftSelectionSession;
this.#shiftSelectionSession = null;
if (!session) return;
if (!this.#canExtendSelection(event)) return;
if (event.pointerId !== session.pointerId) return;
if (
Math.hypot(event.clientX - session.x, event.clientY - session.y) >
TAP_MAX_DISTANCE
) {
return;
}
const target = event.target;
if (!(target instanceof Node) || !this.#view.dom.contains(target)) return;
if (this.#isIgnoredPointerTarget(target)) return;

// Rely on pointer coordinates instead of click events so touch selection
// keeps working when the browser/native path owns the actual tap.
const head = this.#view.posAtCoords(
{ x: event.clientX, y: event.clientY },
false,
);
this.#view.dispatch({
selection: EditorSelection.range(session.anchor, head),
userEvent: "select.extend",
});
event.preventDefault();
Comment thread
bajrangCoder marked this conversation as resolved.
}

#canExtendSelection(event) {
if (!this.#enabled) return false;
if (!(event.isTrusted && event.isPrimary)) return false;
if (typeof event.button === "number" && event.button !== 0) return false;
return !!this.#isShiftSelectionActive(event);
}

#shouldShowMenu() {
if (this.#isScrolling || this.#isPointerInteracting || !this.#view.hasFocus)
return false;
Expand Down
46 changes: 6 additions & 40 deletions src/lib/editorManager.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import sidebarApps from "sidebarApps";
import { indentUnit } from "@codemirror/language";
import { search } from "@codemirror/search";
import {
Compartment,
EditorSelection,
EditorState,
Prec,
StateEffect,
} from "@codemirror/state";
import { Compartment, EditorState, Prec, StateEffect } from "@codemirror/state";
import { oneDark } from "@codemirror/theme-one-dark";
import {
closeHoverTooltips,
Expand Down Expand Up @@ -200,37 +194,10 @@ async function EditorManager($header, $body) {
},
);

let shiftClickSelectionExtension;
{
const pointerIdMap = new Map();
shiftClickSelectionExtension = EditorView.domEventHandlers({
pointerup(event, view) {
if (!appSettings.value.shiftClickSelection) return;
if (!(event.isTrusted && event.isPrimary)) return;
if (!event.shiftKey && quickTools.$footer.dataset.shift == null) return;
const { pointerId } = event;
const tid = setTimeout(() => pointerIdMap.delete(pointerId), 1001);
pointerIdMap.set(pointerId, [view.state.selection.main.anchor, tid]);
},
click(event, view) {
const { pointerId } = event;
if (!pointerIdMap.has(pointerId)) return false;
const [anchor, tid] = pointerIdMap.get(pointerId);
clearTimeout(tid);
pointerIdMap.delete(pointerId);
view.dispatch({
selection: EditorSelection.range(
anchor,
view.state.selection.main.anchor,
),
userEvent: "select.extend",
});
event.preventDefault();
return true;
},
});
}

const isShiftSelectionActive = (event) => {
if (!appSettings.value.shiftClickSelection) return false;
return !!event?.shiftKey || quickTools?.$footer?.dataset?.shift != null;
};
const touchSelectionUpdateExtension = EditorView.updateListener.of(
(update) => {
if (!touchSelectionController) return;
Expand Down Expand Up @@ -788,7 +755,6 @@ async function EditorManager($header, $body) {
commandKeymapExtension: getCommandKeymapExtension(),
themeExtension: themeCompartment.of(oneDark),
pointerCursorVisibilityExtension,
shiftClickSelectionExtension,
touchSelectionUpdateExtension,
searchExtension: search(),
// Ensure read-only can be toggled later via compartment
Expand Down Expand Up @@ -844,6 +810,7 @@ async function EditorManager($header, $body) {
touchSelectionController = createTouchSelectionMenu(editor, {
container: $container,
getActiveFile: () => manager?.activeFile || null,
isShiftSelectionActive,
});

// Provide minimal Ace-like API compatibility used by plugins
Expand Down Expand Up @@ -1150,7 +1117,6 @@ async function EditorManager($header, $body) {
// keep compartment in the state to allow dynamic theme changes later
themeExtension: themeCompartment.of(oneDark),
pointerCursorVisibilityExtension,
shiftClickSelectionExtension,
touchSelectionUpdateExtension,
searchExtension: search(),
// Keep dynamic compartments across state swaps
Expand Down