Skip to content

Commit 9e9d617

Browse files
committed
feat: add Ace compatibility shims for plugins
Add editor.getSelectionRange(), editor.scrollToRow(), and ace.require('ace/ext/modelist') compatibility mapping.
1 parent ab2335d commit 9e9d617

File tree

3 files changed

+151
-1
lines changed

3 files changed

+151
-1
lines changed

src/lib/editorManager.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,62 @@ async function EditorManager($header, $body) {
921921
}
922922
};
923923

924+
/**
925+
* Ace-compatible selection range getter with 0-based rows.
926+
* @returns {{start: {row: number, column: number}, end: {row: number, column: number}}}
927+
*/
928+
editor.getSelectionRange = function () {
929+
try {
930+
const { from, to } = editor.state.selection.main;
931+
const fromLine = editor.state.doc.lineAt(from);
932+
const toLine = editor.state.doc.lineAt(to);
933+
return {
934+
start: {
935+
row: Math.max(0, fromLine.number - 1),
936+
column: from - fromLine.from,
937+
},
938+
end: {
939+
row: Math.max(0, toLine.number - 1),
940+
column: to - toLine.from,
941+
},
942+
};
943+
} catch (_) {
944+
return { start: { row: 0, column: 0 }, end: { row: 0, column: 0 } };
945+
}
946+
};
947+
948+
/**
949+
* Ace-compatible row scrolling helper.
950+
* @param {number} row - 0-based row index, supports Infinity to jump to end.
951+
* @returns {boolean}
952+
*/
953+
editor.scrollToRow = function (row) {
954+
try {
955+
const scroller = editor.scrollDOM;
956+
if (!scroller) return false;
957+
958+
if (row === Number.POSITIVE_INFINITY) {
959+
scroller.scrollTop = Math.max(
960+
scroller.scrollHeight - scroller.clientHeight,
961+
0,
962+
);
963+
return true;
964+
}
965+
966+
const parsedRow = Number(row);
967+
if (!Number.isFinite(parsedRow)) return false;
968+
const aceRow = Math.max(0, Math.floor(parsedRow));
969+
const lineNum = Math.min(editor.state.doc.lines, aceRow + 1);
970+
const line = editor.state.doc.line(lineNum);
971+
editor.dispatch({
972+
effects: EditorView.scrollIntoView(line.from, { y: "start" }),
973+
});
974+
return true;
975+
} catch (_) {
976+
return false;
977+
}
978+
};
979+
924980
/**
925981
* Move cursor to specific position
926982
* @param {{row: number, column: number}} pos - Position to move to

src/main.js

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ import fsOperation from "fileSystem";
1515
import sidebarApps from "sidebarApps";
1616
import ajax from "@deadlyjack/ajax";
1717
import { setKeyBindings } from "cm/commandRegistry";
18-
import { initModes } from "cm/modelist";
18+
import {
19+
getModeForPath,
20+
getModes,
21+
getModesByName,
22+
initModes,
23+
} from "cm/modelist";
1924
import Contextmenu from "components/contextmenu";
2025
import { hasConnectedServers } from "components/lspInfoDialog";
2126
import Sidebar from "components/sidebar";
@@ -64,6 +69,59 @@ const previousVersionCode = Number.parseInt(localStorage.versionCode, 10);
6469
window.onload = Main;
6570
const logger = new Logger();
6671

72+
function createAceModelistCompatModule() {
73+
const toAceMode = (mode) => {
74+
const resolved = mode || getModeForPath("");
75+
if (!resolved) return null;
76+
const name = resolved.name || "text";
77+
const rawMode = String(resolved.mode || name);
78+
const modePath = rawMode.startsWith("ace/mode/")
79+
? rawMode
80+
: `ace/mode/${rawMode}`;
81+
return {
82+
...resolved,
83+
name,
84+
caption: resolved.caption || name,
85+
mode: modePath,
86+
};
87+
};
88+
89+
return {
90+
get modes() {
91+
return getModes()
92+
.map((mode) => toAceMode(mode))
93+
.filter(Boolean);
94+
},
95+
get modesByName() {
96+
const source = getModesByName();
97+
const result = {};
98+
Object.keys(source).forEach((name) => {
99+
result[name] = toAceMode(source[name]);
100+
});
101+
return result;
102+
},
103+
getModeForPath(path) {
104+
return toAceMode(getModeForPath(String(path || "")));
105+
},
106+
};
107+
}
108+
109+
function ensureAceCompatApi() {
110+
const ace = window.ace || {};
111+
const modelistModule = createAceModelistCompatModule();
112+
const originalRequire =
113+
typeof ace.require === "function" ? ace.require.bind(ace) : null;
114+
115+
ace.require = (moduleId) => {
116+
if (moduleId === "ace/ext/modelist" || moduleId === "ace/ext/modelist.js") {
117+
return modelistModule;
118+
}
119+
return originalRequire?.(moduleId);
120+
};
121+
122+
window.ace = ace;
123+
}
124+
67125
async function Main() {
68126
const oldPreventDefault = TouchEvent.prototype.preventDefault;
69127

@@ -179,6 +237,7 @@ async function onDeviceReady() {
179237
return true;
180238
})();
181239
window.acode = new Acode();
240+
ensureAceCompatApi();
182241

183242
system.requestPermission("android.permission.READ_EXTERNAL_STORAGE");
184243
system.requestPermission("android.permission.WRITE_EXTERNAL_STORAGE");

src/test/ace.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,27 @@ export async function runAceCompatibilityTests(writeOutput) {
9999
test.assert(range.end != null, "range should have end");
100100
});
101101

102+
runner.test("editor.getSelectionRange()", (test) => {
103+
const editor = getEditor();
104+
test.assert(
105+
typeof editor.getSelectionRange === "function",
106+
"getSelectionRange should be a function",
107+
);
108+
const range = editor.getSelectionRange();
109+
test.assert(range.start != null, "range should have start");
110+
test.assert(range.end != null, "range should have end");
111+
});
112+
113+
runner.test("editor.scrollToRow()", (test) => {
114+
const editor = getEditor();
115+
test.assert(
116+
typeof editor.scrollToRow === "function",
117+
"scrollToRow should be a function",
118+
);
119+
const ok = editor.scrollToRow(0);
120+
test.assert(ok === true || ok === undefined, "scrollToRow should not fail");
121+
});
122+
102123
runner.test("editor.selection.getCursor()", (test) => {
103124
const editor = getEditor();
104125
test.assert(
@@ -199,6 +220,20 @@ export async function runAceCompatibilityTests(writeOutput) {
199220
test.assert(editor.contentDOM != null, "contentDOM should exist");
200221
});
201222

223+
runner.test("ace.require('ace/ext/modelist')", (test) => {
224+
test.assert(window.ace != null, "window.ace should exist");
225+
test.assert(
226+
typeof window.ace.require === "function",
227+
"ace.require should be a function",
228+
);
229+
const modelist = window.ace.require("ace/ext/modelist");
230+
test.assert(modelist != null, "modelist should be available");
231+
test.assert(
232+
typeof modelist.getModeForPath === "function",
233+
"modelist.getModeForPath should be a function",
234+
);
235+
});
236+
202237
// Session API tests
203238

204239
runner.test("session.getValue()", async (test) => {

0 commit comments

Comments
 (0)