Skip to content

Commit efe2566

Browse files
committed
Implement cursor visibility and auto-scroll in editor
1 parent c697b7b commit efe2566

1 file changed

Lines changed: 73 additions & 32 deletions

File tree

src/lib/editorManager.js

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import {
1414

1515
import { indentUnit } from "@codemirror/language";
1616
import { search } from "@codemirror/search";
17-
import { Compartment, EditorState, Prec, StateEffect } from "@codemirror/state";
17+
import {
18+
Compartment,
19+
EditorState,
20+
Prec,
21+
StateEffect,
22+
} from "@codemirror/state";
1823
import { oneDark } from "@codemirror/theme-one-dark";
1924
import {
2025
EditorView,
@@ -139,6 +144,23 @@ async function EditorManager($header, $body) {
139144
".cm-scroller": { height: "100%", overflow: "auto" },
140145
});
141146

147+
const pointerCursorVisibilityExtension = EditorView.updateListener.of(
148+
(update) => {
149+
if (!update.selectionSet) return;
150+
const pointerTriggered = update.transactions.some((tr) =>
151+
tr.isUserEvent("pointer") ||
152+
tr.isUserEvent("select.pointer") ||
153+
tr.isUserEvent("touch") ||
154+
tr.isUserEvent("select.touch"),
155+
);
156+
if (!pointerTriggered) return;
157+
if (isCursorVisible()) return;
158+
requestAnimationFrame(() => {
159+
if (!isCursorVisible()) scrollCursorIntoView({ behavior: "instant" });
160+
});
161+
},
162+
);
163+
142164
// Compartment to swap editor theme dynamically
143165
const themeCompartment = new Compartment();
144166
// Compartments to control indentation, tab width, and font styling dynamically
@@ -605,6 +627,7 @@ async function EditorManager($header, $body) {
605627
// Default theme
606628
themeCompartment.of(oneDark),
607629
fixedHeightTheme,
630+
pointerCursorVisibilityExtension,
608631
search(),
609632
// Ensure read-only can be toggled later via compartment
610633
readOnlyCompartment.of(EditorState.readOnly.of(false)),
@@ -889,6 +912,7 @@ async function EditorManager($header, $body) {
889912
// keep compartment in the state to allow dynamic theme changes later
890913
themeCompartment.of(oneDark),
891914
fixedHeightTheme,
915+
pointerCursorVisibilityExtension,
892916
search(),
893917
// Keep dynamic compartments across state swaps
894918
...getBaseExtensionsFromOptions(),
@@ -1433,15 +1457,12 @@ async function EditorManager($header, $body) {
14331457
scroller?.addEventListener("scroll", handleEditorScroll, { passive: true });
14341458
handleEditorScroll();
14351459

1436-
// TODO: Implement focus event for CodeMirror
1437-
// editor.on("focus", async () => {
1438-
// const { activeFile } = manager;
1439-
// activeFile.focused = true;
1440-
// keyboardHandler.on("keyboardShow", scrollCursorIntoView);
1441-
// if (isScrolling) return;
1442-
// $hScrollbar.hide();
1443-
// $vScrollbar.hide();
1444-
// });
1460+
keyboardHandler.on("keyboardShowStart", () => {
1461+
requestAnimationFrame(() => {
1462+
scrollCursorIntoView({ behavior: "instant" });
1463+
});
1464+
});
1465+
keyboardHandler.on("keyboardShow", scrollCursorIntoView);
14451466

14461467
// TODO: Implement blur event for CodeMirror
14471468
// editor.on("blur", async () => {
@@ -1547,34 +1568,54 @@ async function EditorManager($header, $body) {
15471568
/**
15481569
* Scrolls the cursor into view if it is not currently visible.
15491570
*/
1550-
// TODO: Implement cursor scrolling for CodeMirror
1551-
function scrollCursorIntoView() {
1552-
// keyboardHandler.off("keyboardShow", scrollCursorIntoView);
1553-
// if (isCursorVisible()) return;
1554-
// const { teardropSize } = appSettings.value;
1555-
// editor.renderer.scrollCursorIntoView();
1556-
// editor.renderer.scrollBy(0, teardropSize + 10);
1557-
// editor._emit("scroll-intoview");
1571+
function scrollCursorIntoView(options = {}) {
1572+
const view = editor;
1573+
const scroller = view?.scrollDOM;
1574+
if (!view || !scroller) return;
1575+
1576+
const { behavior = "smooth" } = options;
1577+
const { head } = view.state.selection.main;
1578+
const caret = view.coordsAtPos(head);
1579+
if (!caret) return;
1580+
1581+
const scrollerRect = scroller.getBoundingClientRect();
1582+
const relativeTop = caret.top - scrollerRect.top + scroller.scrollTop;
1583+
const relativeBottom =
1584+
caret.bottom - scrollerRect.top + scroller.scrollTop;
1585+
const topMargin = 16;
1586+
const bottomMargin =
1587+
(appSettings.value?.teardropSize || 24) + 12;
1588+
1589+
const scrollTop = scroller.scrollTop;
1590+
const visibleTop = scrollTop + topMargin;
1591+
const visibleBottom = scrollTop + scroller.clientHeight - bottomMargin;
1592+
1593+
if (relativeTop < visibleTop) {
1594+
const nextTop = Math.max(relativeTop - topMargin, 0);
1595+
scroller.scrollTo({ top: nextTop, behavior });
1596+
} else if (relativeBottom > visibleBottom) {
1597+
const delta = relativeBottom - visibleBottom;
1598+
scroller.scrollTo({ top: scrollTop + delta, behavior });
1599+
}
15581600
}
15591601

15601602
/**
1561-
* Checks if the cursor is visible within the Ace editor.
1603+
* Checks if the cursor is visible within the CodeMirror viewport.
15621604
* @returns {boolean} - True if the cursor is visible, false otherwise.
15631605
*/
1564-
// TODO: Implement cursor visibility check for CodeMirror
15651606
function isCursorVisible() {
1566-
// const { editor, container } = manager;
1567-
// const { teardropSize } = appSettings.value;
1568-
// const cursorPos = editor.getCursorPosition();
1569-
// const contentTop = container.getBoundingClientRect().top;
1570-
// const contentBottom = contentTop + container.clientHeight;
1571-
// const cursorTop = editor.renderer.textToScreenCoordinates(
1572-
// cursorPos.row,
1573-
// cursorPos.column,
1574-
// ).pageY;
1575-
// const cursorBottom = cursorTop + teardropSize + 10;
1576-
// return cursorTop >= contentTop && cursorBottom <= contentBottom;
1577-
return true; // Placeholder
1607+
const view = editor;
1608+
const scroller = view?.scrollDOM;
1609+
if (!view || !scroller) return true;
1610+
1611+
const { head } = view.state.selection.main;
1612+
const caret = view.coordsAtPos(head);
1613+
if (!caret) return true;
1614+
1615+
const scrollerRect = scroller.getBoundingClientRect();
1616+
return (
1617+
caret.top >= scrollerRect.top && caret.bottom <= scrollerRect.bottom
1618+
);
15781619
}
15791620

15801621
/**

0 commit comments

Comments
 (0)