Skip to content

Commit d053597

Browse files
committed
fix: readOnly editor thing and folds, cursor , scroll thing
- dynamic readonly through compartment - cursor and selection - folds saving and restore - scroll related stuff(for editor state)
1 parent 9c1b0e9 commit d053597

File tree

4 files changed

+206
-56
lines changed

4 files changed

+206
-56
lines changed

src/codemirror/editorUtils.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { foldEffect, foldedRanges } from "@codemirror/language";
2+
import { EditorSelection, EditorState } from "@codemirror/state";
3+
import { EditorView } from "@codemirror/view";
4+
/**
5+
* Interface for fold span objects
6+
* @typedef {Object} FoldSpan
7+
* @property {number} fromLine - Starting line number (1-based)
8+
* @property {number} fromCol - Starting column (0-based)
9+
* @property {number} toLine - Ending line number (1-based)
10+
* @property {number} toCol - Ending column (0-based)
11+
*/
12+
13+
/**
14+
* Get all folded ranges from CodeMirror editor state
15+
* @param {EditorState} state - CodeMirror editor state
16+
* @returns {FoldSpan[]} Array of fold span objects
17+
*/
18+
export function getAllFolds(state) {
19+
const doc = state.doc;
20+
const folds = [];
21+
22+
foldedRanges(state).between(0, doc.length, (from, to) => {
23+
const fromPos = doc.lineAt(from);
24+
const toPos = doc.lineAt(to);
25+
folds.push({
26+
fromLine: fromPos.number,
27+
fromCol: from - fromPos.from,
28+
toLine: toPos.number,
29+
toCol: to - toPos.from,
30+
});
31+
});
32+
33+
return folds;
34+
}
35+
36+
/**
37+
* @param {EditorView} view - CodeMirror editor state
38+
*/
39+
export function getSelection(view) {
40+
const sel = view.state.selection;
41+
return {
42+
ranges: sel.ranges.map((r) => ({ from: r.from, to: r.to })),
43+
mainIndex: sel.mainIndex,
44+
};
45+
}
46+
47+
/**
48+
* Get scroll
49+
* @param {EditorView} view - CodeMirror editor view
50+
*/
51+
export function getScrollPosition(view) {
52+
const { scrollTop, scrollLeft } = view.scrollDOM;
53+
return { scrollTop, scrollLeft };
54+
}
55+
56+
/**
57+
* Set scroll position in CodeMirror editor view
58+
* @param {EditorView} view - CodeMirror editor view
59+
* @param {number} scrollTop - Vertical scroll position
60+
* @param {number} scrollLeft - Horizontal scroll position
61+
*/
62+
export function setScrollPosition(view, scrollTop, scrollLeft) {
63+
const scroller = view.scrollDOM;
64+
65+
if (typeof scrollTop === "number") {
66+
scroller.scrollTop = scrollTop;
67+
}
68+
69+
if (typeof scrollLeft === "number") {
70+
scroller.scrollLeft = scrollLeft;
71+
}
72+
}
73+
74+
export function restoreSelection(view, sel) {
75+
if (!sel || !sel.ranges || !sel.ranges.length) return;
76+
const len = view.state.doc.length;
77+
78+
const ranges = sel.ranges
79+
.map((r) => {
80+
const from = Math.max(0, Math.min(len, r.from | 0));
81+
const to = Math.max(0, Math.min(len, r.to | 0));
82+
return EditorSelection.range(from, to);
83+
})
84+
.filter(Boolean);
85+
86+
if (!ranges.length) return;
87+
88+
const mainIndex =
89+
sel.mainIndex >= 0 && sel.mainIndex < ranges.length ? sel.mainIndex : 0;
90+
91+
view.dispatch({
92+
selection: EditorSelection.create(ranges, mainIndex),
93+
scrollIntoView: true,
94+
});
95+
}
96+
97+
/**
98+
* Restore folds to CodeMirror editor
99+
* @param {EditorView} view - CodeMirror editor view
100+
* @param {FoldSpan[]} folds - Array of fold spans to restore
101+
*/
102+
export function restoreFolds(view, folds) {
103+
if (!Array.isArray(folds) || folds.length === 0) return;
104+
105+
function lineColToOffset(doc, line, col) {
106+
const ln = doc.line(line);
107+
return Math.min(ln.from + col, ln.to);
108+
}
109+
110+
function loadFolds(state, saved) {
111+
const doc = state.doc;
112+
const effects = [];
113+
114+
for (const f of saved) {
115+
// Validate line numbers
116+
if (f.fromLine < 1 || f.fromLine > doc.lines) continue;
117+
if (f.toLine < 1 || f.toLine > doc.lines) continue;
118+
119+
const from = lineColToOffset(doc, f.fromLine, f.fromCol);
120+
const to = lineColToOffset(doc, f.toLine, f.toCol);
121+
if (to > from) {
122+
effects.push(foldEffect.of({ from, to }));
123+
}
124+
}
125+
return effects;
126+
}
127+
128+
const restoreEffects = loadFolds(view.state, folds);
129+
if (restoreEffects.length) {
130+
view.dispatch({ effects: restoreEffects });
131+
}
132+
}
133+
134+
export default {
135+
getAllFolds,
136+
getSelection,
137+
getScrollPosition,
138+
setScrollPosition,
139+
restoreSelection,
140+
restoreFolds,
141+
};

src/lib/editorFile.js

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ import mimeTypes from "mime-types";
1111
import helpers from "utils/helpers";
1212
import Path from "utils/Path";
1313
import Url from "utils/Url";
14+
import {
15+
restoreFolds,
16+
restoreSelection,
17+
setScrollPosition,
18+
} from "../codemirror/editorUtils";
1419
import { getModeForPath } from "../codemirror/modelist";
1520
import constants from "./constants";
1621
import openFolder from "./openFolder";
@@ -528,7 +533,7 @@ export default class EditorFile {
528533
*/
529534
set editable(value) {
530535
if (this.#editable === value) return;
531-
editorManager.editor.setReadOnly(!value);
536+
this.setReadOnly(!value);
532537
editorManager.onupdate("read-only");
533538
editorManager.emit("update", "read-only");
534539
this.#editable = value;
@@ -743,6 +748,19 @@ export default class EditorFile {
743748
return this.#save(true);
744749
}
745750

751+
setReadOnly(value) {
752+
try {
753+
const { editor, readOnlyCompartment } = editorManager;
754+
if (!editor) return;
755+
if (!readOnlyCompartment) return;
756+
editor.dispatch({
757+
effects: readOnlyCompartment.reconfigure(
758+
EditorState.readOnly.of(!!value),
759+
),
760+
});
761+
} catch (_) {}
762+
}
763+
746764
/**
747765
* Sets syntax highlighting of the file.
748766
* @param {string} [mode]
@@ -1022,14 +1040,17 @@ export default class EditorFile {
10221040

10231041
this.#loadOptions = null;
10241042

1025-
editor.state.readOnly = true;
1043+
this.setReadOnly(true);
10261044
this.loading = true;
10271045
this.markChanged = false;
10281046
this.#emit("loadstart", createFileEvent(this));
1029-
this.session = EditorState.create({
1030-
doc: strings["loading..."],
1031-
extensions: this.session.extensions,
1032-
});
1047+
this.session = this.session.update({
1048+
changes: {
1049+
from: 0,
1050+
to: this.session.doc.length,
1051+
insert: strings["loading..."],
1052+
},
1053+
}).state;
10331054

10341055
try {
10351056
const cacheFs = fsOperation(this.cacheFile);
@@ -1054,33 +1075,28 @@ export default class EditorFile {
10541075
}
10551076

10561077
this.markChanged = false;
1057-
this.session = EditorState.create({
1058-
doc: value,
1059-
extensions: this.session.extensions,
1060-
});
1078+
this.session = this.session.update({
1079+
changes: { from: 0, to: this.session.doc.length, insert: value },
1080+
}).state;
10611081
this.loaded = true;
10621082
this.loading = false;
10631083

10641084
const { activeFile, emit } = editorManager;
10651085
if (activeFile.id === this.id) {
1066-
editor.state.readOnly = false;
1086+
this.setReadOnly(false);
10671087
}
10681088

10691089
setTimeout(() => {
10701090
this.#emit("load", createFileEvent(this));
10711091
emit("file-loaded", this);
1072-
// TODO: Implement cursor positioning and scrolling for CodeMirror
1073-
// if (cursorPos)
1074-
// this.session.selection.moveCursorTo(cursorPos.row, cursorPos.column);
1075-
// if (scrollTop) this.session.setScrollTop(scrollTop);
1076-
// if (scrollLeft) this.session.setScrollLeft(scrollLeft);
1092+
if (cursorPos) {
1093+
restoreSelection(editor, cursorPos);
1094+
}
1095+
if (scrollTop || scrollLeft) {
1096+
setScrollPosition(editor, scrollTop, scrollLeft);
1097+
}
10771098
if (editable !== undefined) this.editable = editable;
1078-
1079-
// TODO: Implement folding for CodeMirror
1080-
// if (Array.isArray(folds)) {
1081-
// const parsedFolds = EditorFile.#parseFolds(folds);
1082-
// this.session?.addFolds(parsedFolds);
1083-
// }
1099+
restoreFolds(editor, folds);
10841100
}, 0);
10851101
} catch (error) {
10861102
this.#emit("loaderror", createFileEvent(this));
@@ -1106,16 +1122,6 @@ export default class EditorFile {
11061122
// editorManager.editor._emit("scrollleft", e);
11071123
// }
11081124

1109-
/**
1110-
* Parse folds - TODO: Update for CodeMirror folding system
1111-
* @param {Array} folds
1112-
*/
1113-
static #parseFolds(folds) {
1114-
if (!Array.isArray(folds)) return [];
1115-
// TODO: Implement CodeMirror fold parsing
1116-
return [];
1117-
}
1118-
11191125
#save(as) {
11201126
const event = createFileEvent(this);
11211127
this.#emit("save", event);

src/lib/editorManager.js

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ async function EditorManager($header, $body) {
134134
const completionCompartment = new Compartment();
135135
// Compartment for rainbow bracket colorizer
136136
const rainbowCompartment = new Compartment();
137+
// Compartment for read-only toggling
138+
const readOnlyCompartment = new Compartment();
137139

138140
function getEditorFontFamily() {
139141
const font = appSettings?.value?.editorFont || "Roboto Mono";
@@ -340,6 +342,8 @@ async function EditorManager($header, $body) {
340342
// Default theme
341343
themeCompartment.of(oneDark),
342344
fixedHeightTheme,
345+
// Ensure read-only can be toggled later via compartment
346+
readOnlyCompartment.of(EditorState.readOnly.of(false)),
343347
// Editor options driven by settings via compartments
344348
...getBaseExtensionsFromOptions(),
345349
// Emmet abbreviation tracker and common keybindings
@@ -705,9 +709,10 @@ async function EditorManager($header, $body) {
705709
exts.push(colorView(true));
706710
}
707711

708-
// Apply read-only state based on file.editable/loading
712+
// Apply read-only state based on file.editable/loading using Compartment
709713
try {
710-
exts.push(EditorState.readOnly.of(!file.editable || !!file.loading));
714+
const ro = !file.editable || !!file.loading;
715+
exts.push(readOnlyCompartment.of(EditorState.readOnly.of(ro)));
711716
} catch (e) {
712717
// safe to ignore; editor will remain editable by default
713718
}
@@ -765,6 +770,7 @@ async function EditorManager($header, $body) {
765770
activeFile: null,
766771
addFile,
767772
editor,
773+
readOnlyCompartment,
768774
getFile,
769775
switchFile,
770776
hasUnsavedFiles,
@@ -1421,7 +1427,16 @@ async function EditorManager($header, $body) {
14211427
// Re-apply state when read-only toggles on the active file
14221428
manager.on(["update:read-only"], () => {
14231429
const file = manager.activeFile;
1424-
if (file?.type === "editor") applyFileToEditor(file);
1430+
if (file?.type !== "editor") return;
1431+
try {
1432+
const ro = !file.editable || !!file.loading;
1433+
editor.dispatch({
1434+
effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(ro)),
1435+
});
1436+
} catch (_) {
1437+
// Fallback: re-apply full state if something goes wrong
1438+
applyFileToEditor(file);
1439+
}
14251440
});
14261441

14271442
/**

src/lib/saveState.js

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
getAllFolds,
3+
getScrollPosition,
4+
getSelection,
5+
} from "../codemirror/editorUtils";
16
import constants from "./constants";
27
import { addedFolder } from "./openFolder";
38
import appSettings from "./settings";
@@ -24,13 +29,13 @@ export default () => {
2429
readOnly: file.readOnly,
2530
SAFMode: file.SAFMode,
2631
deletedFile: file.deletedFile,
27-
cursorPos: editor.getCursorPosition(),
28-
scrollTop: editor.session.getScrollTop(),
29-
scrollLeft: editor.session.getScrollLeft(),
32+
cursorPos: getSelection(editor),
33+
scrollTop: getScrollPosition(editor).scrollTop,
34+
scrollLeft: getScrollPosition(editor).scrollLeft,
3035
editable: file.editable,
3136
encoding: file.encoding,
3237
render: activeFile.id === file.id,
33-
folds: parseFolds(file.session.getAllFolds()),
38+
folds: getAllFolds(file.session),
3439
};
3540

3641
if (settings.rememberFiles || fileJson.isUnsaved)
@@ -55,20 +60,3 @@ export default () => {
5560
localStorage.files = JSON.stringify(filesToSave);
5661
localStorage.folders = JSON.stringify(folders);
5762
};
58-
59-
function parseFolds(folds) {
60-
if (!Array.isArray(folds)) return [];
61-
62-
return folds
63-
.map((fold) => {
64-
if (!fold || !fold.range) return null;
65-
66-
const { range, ranges, placeholder } = fold;
67-
return {
68-
range,
69-
ranges: parseFolds(ranges || []),
70-
placeholder,
71-
};
72-
})
73-
.filter(Boolean);
74-
}

0 commit comments

Comments
 (0)