Skip to content

Commit 13f0890

Browse files
authored
fix(editor): stabilize restored tab highlighting (#2193)
1 parent 230c4b9 commit 13f0890

2 files changed

Lines changed: 164 additions & 116 deletions

File tree

src/lib/editorFile.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,17 +1575,6 @@ export default class EditorFile {
15751575
this.markChanged = false;
15761576
this.#emit("loadstart", createFileEvent(this));
15771577

1578-
// Immediately apply the loading read-only state without inserting placeholder
1579-
// text into the real document or undo history.
1580-
try {
1581-
const { activeFile, emit } = editorManager;
1582-
if (activeFile?.id === this.id) {
1583-
emit("file-loaded", this);
1584-
}
1585-
} catch (error) {
1586-
console.warn("Failed to emit interim file-loaded event.", error);
1587-
}
1588-
15891578
try {
15901579
const cacheFs = fsOperation(this.cacheFile);
15911580
const cacheExists = await cacheFs.exists();
@@ -1620,6 +1609,8 @@ export default class EditorFile {
16201609
this.markChanged = false;
16211610
this.session = EditorState.create({ doc: value });
16221611
this.__cmSessionReady = false;
1612+
this.__cmLanguageReady = false;
1613+
this.__cmLanguageSignature = null;
16231614
this.markLoaded({ mtime: loadedMtime, isUnsaved, savedDoc });
16241615
this.markChanged = true;
16251616
this.loaded = true;

src/lib/editorManager.js

Lines changed: 162 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import sidebarApps from "sidebarApps";
2-
import { indentUnit } from "@codemirror/language";
2+
import { indentUnit, language as languageFacet } from "@codemirror/language";
33
import { search } from "@codemirror/search";
44
import { Compartment, EditorState, Prec, StateEffect } from "@codemirror/state";
55
import { oneDark } from "@codemirror/theme-one-dark";
@@ -12,6 +12,7 @@ import {
1212
highlightWhitespace,
1313
keymap,
1414
lineNumbers,
15+
placeholder,
1516
} from "@codemirror/view";
1617
import {
1718
abbreviationTracker,
@@ -1197,6 +1198,16 @@ async function EditorManager($header, $body) {
11971198
});
11981199
}
11991200

1201+
function getEditorOptionsSignature() {
1202+
const values = appSettings?.value || {};
1203+
const keys = new Set(["editorTheme"]);
1204+
for (const spec of cmOptionSpecs) {
1205+
spec.keys.forEach((key) => keys.add(key));
1206+
}
1207+
1208+
return JSON.stringify([...keys].sort().map((key) => [key, values[key]]));
1209+
}
1210+
12001211
function getRawEditorState(state) {
12011212
return state?.__rawState || state || null;
12021213
}
@@ -1213,6 +1224,88 @@ async function EditorManager($header, $body) {
12131224
);
12141225
}
12151226

1227+
function getFileLanguageSignature(file, extensionSignature) {
1228+
return JSON.stringify({
1229+
mode: file?.currentMode || "text",
1230+
extensions: extensionSignature,
1231+
});
1232+
}
1233+
1234+
function hasLanguageSupport(state) {
1235+
try {
1236+
return !!state?.facet?.(languageFacet);
1237+
} catch (_) {
1238+
return false;
1239+
}
1240+
}
1241+
1242+
function shouldApplyLanguage(file, state, languageSignature) {
1243+
const langExtFn = file?.currentLanguageExtension;
1244+
if (typeof langExtFn !== "function") return false;
1245+
const isPlainText =
1246+
String(file?.currentMode || "").toLowerCase() === "text";
1247+
return (
1248+
file.__cmLanguageSignature !== languageSignature ||
1249+
!file.__cmLanguageReady ||
1250+
(!isPlainText && !hasLanguageSupport(state))
1251+
);
1252+
}
1253+
1254+
function markLanguageReady(file, languageSignature, ready) {
1255+
file.__cmLanguageSignature = languageSignature;
1256+
file.__cmLanguageReady = ready;
1257+
}
1258+
1259+
function dispatchLanguageExtension(file, languageSignature, ext, warnKey) {
1260+
try {
1261+
editor.dispatch({
1262+
effects: languageCompartment.reconfigure(ext || []),
1263+
});
1264+
file.session = editor.state;
1265+
markLanguageReady(file, languageSignature, true);
1266+
} catch (error) {
1267+
warnRecoverable("Failed to apply language extensions.", error, warnKey);
1268+
}
1269+
}
1270+
1271+
function resolveLanguageExtension(file, languageSignature, warnKey) {
1272+
const langExtFn = file.currentLanguageExtension;
1273+
if (typeof langExtFn !== "function") {
1274+
markLanguageReady(file, languageSignature, true);
1275+
return [];
1276+
}
1277+
1278+
let result;
1279+
try {
1280+
result = langExtFn();
1281+
} catch (_) {
1282+
markLanguageReady(file, languageSignature, true);
1283+
return [];
1284+
}
1285+
1286+
if (result && typeof result.then === "function") {
1287+
const fileId = file.id;
1288+
markLanguageReady(file, languageSignature, false);
1289+
result
1290+
.then((ext) => {
1291+
if (
1292+
manager.activeFile?.id !== fileId ||
1293+
file.__cmLanguageSignature !== languageSignature
1294+
) {
1295+
return;
1296+
}
1297+
dispatchLanguageExtension(file, languageSignature, ext, warnKey);
1298+
})
1299+
.catch(() => {
1300+
markLanguageReady(file, languageSignature, true);
1301+
});
1302+
return [];
1303+
}
1304+
1305+
markLanguageReady(file, languageSignature, true);
1306+
return result || [];
1307+
}
1308+
12161309
function scheduleLspForFile(file) {
12171310
const fileId = file?.id;
12181311
window.setTimeout(() => {
@@ -1221,16 +1314,21 @@ async function EditorManager($header, $body) {
12211314
}, 80);
12221315
}
12231316

1224-
function applyCurrentEditorOptions(file) {
1317+
function applyCurrentEditorOptions(file, { forceOptions = false } = {}) {
12251318
touchSelectionController?.onSessionChanged();
1226-
const desiredTheme = appSettings?.value?.editorTheme;
1227-
if (desiredTheme) editor.setTheme(desiredTheme);
1228-
applyOptions();
1319+
const optionsSignature = getEditorOptionsSignature();
1320+
if (forceOptions || file.__cmOptionsSignature !== optionsSignature) {
1321+
const desiredTheme = appSettings?.value?.editorTheme;
1322+
if (desiredTheme) editor.setTheme(desiredTheme);
1323+
applyOptions();
1324+
file.__cmOptionsSignature = optionsSignature;
1325+
}
12291326
try {
12301327
const ro = !file.editable || !!file.loading;
12311328
editor.dispatch({
12321329
effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(ro)),
12331330
});
1331+
file.session = editor.state;
12341332
} catch (error) {
12351333
warnRecoverable(
12361334
"Failed to apply read-only compartment update.",
@@ -1240,65 +1338,55 @@ async function EditorManager($header, $body) {
12401338
}
12411339
}
12421340

1341+
function showLoadingEditor(file) {
1342+
const desiredTheme = appSettings?.value?.editorTheme;
1343+
const themeExt = desiredTheme
1344+
? getThemeExtensions(desiredTheme, [oneDark])
1345+
: oneDark;
1346+
const loadingState = EditorState.create({
1347+
doc: "",
1348+
extensions: [
1349+
themeCompartment.of(themeExt),
1350+
...getBaseExtensionsFromOptions(),
1351+
languageCompartment.of([]),
1352+
lspCompartment.of([]),
1353+
readOnlyCompartment.of(EditorState.readOnly.of(true)),
1354+
EditorView.editable.of(false),
1355+
placeholder(`Loading ${file.filename || "file"}...`),
1356+
],
1357+
});
1358+
editor.setState(loadingState);
1359+
touchSelectionController?.onSessionChanged();
1360+
}
1361+
12431362
// Helper: apply a file's content and language to the editor view
12441363
function applyFileToEditor(file, options = {}) {
12451364
if (!file || file.type !== "editor") return;
12461365
const { forceRecreate = false } = options;
12471366
const extensionSignature = getEditorExtensionSignature(file);
1367+
const languageSignature = getFileLanguageSignature(
1368+
file,
1369+
extensionSignature,
1370+
);
12481371

12491372
if (!forceRecreate && isReusableEditorState(file, extensionSignature)) {
1250-
editor.setState(getRawEditorState(file.session));
1373+
const reusedState = getRawEditorState(file.session);
1374+
editor.setState(reusedState);
12511375
applyCurrentEditorOptions(file);
12521376

1253-
// Ensure language extensions are properly applied even when reusing state
1254-
const langExtFn = file.currentLanguageExtension;
1255-
if (typeof langExtFn === "function") {
1256-
let result;
1257-
try {
1258-
result = langExtFn();
1259-
} catch (_) {
1260-
result = [];
1261-
}
1262-
// If the loader returns a Promise, reconfigure when it resolves
1263-
if (result && typeof result.then === "function") {
1264-
const fileId = file.id;
1265-
const expectedSignature = extensionSignature;
1266-
result
1267-
.then((ext) => {
1268-
if (
1269-
manager.activeFile?.id !== fileId ||
1270-
file.__cmExtensionSignature !== expectedSignature
1271-
) {
1272-
return;
1273-
}
1274-
try {
1275-
editor.dispatch({
1276-
effects: languageCompartment.reconfigure(ext || []),
1277-
});
1278-
} catch (error) {
1279-
warnRecoverable(
1280-
"Failed to apply language extensions for reused state.",
1281-
error,
1282-
"reused-language-reconfigure",
1283-
);
1284-
}
1285-
})
1286-
.catch(() => {
1287-
// ignore load errors; remain in plain text
1288-
});
1289-
} else if (result && result.length) {
1290-
// Synchronous language extensions available
1291-
try {
1292-
editor.dispatch({
1293-
effects: languageCompartment.reconfigure(result),
1294-
});
1295-
} catch (error) {
1296-
warnRecoverable(
1297-
"Failed to apply language extensions for reused state.",
1298-
error,
1299-
"reused-language-reconfigure",
1300-
);
1301-
}
1377+
if (shouldApplyLanguage(file, reusedState, languageSignature)) {
1378+
const ext = resolveLanguageExtension(
1379+
file,
1380+
languageSignature,
1381+
"reused-language-reconfigure",
1382+
);
1383+
if (file.__cmLanguageReady) {
1384+
dispatchLanguageExtension(
1385+
file,
1386+
languageSignature,
1387+
ext,
1388+
"reused-language-reconfigure",
1389+
);
13021390
}
13031391
}
13041392

@@ -1330,47 +1418,11 @@ async function EditorManager($header, $body) {
13301418
const exts = [...baseExtensions];
13311419
maybeAttachEmmetCompletions(exts, syntax);
13321420
try {
1333-
const langExtFn = file.currentLanguageExtension;
1334-
let initialLang = [];
1335-
if (typeof langExtFn === "function") {
1336-
let result;
1337-
try {
1338-
result = langExtFn();
1339-
} catch (_) {
1340-
result = [];
1341-
}
1342-
// If the loader returns a Promise, reconfigure when it resolves
1343-
if (result && typeof result.then === "function") {
1344-
initialLang = [];
1345-
const fileId = file.id;
1346-
const expectedSignature = extensionSignature;
1347-
result
1348-
.then((ext) => {
1349-
if (
1350-
manager.activeFile?.id !== fileId ||
1351-
file.__cmExtensionSignature !== expectedSignature
1352-
) {
1353-
return;
1354-
}
1355-
try {
1356-
editor.dispatch({
1357-
effects: languageCompartment.reconfigure(ext || []),
1358-
});
1359-
} catch (error) {
1360-
warnRecoverable(
1361-
"Failed to apply async language extensions.",
1362-
error,
1363-
"async-language-reconfigure",
1364-
);
1365-
}
1366-
})
1367-
.catch(() => {
1368-
// ignore load errors; remain in plain text
1369-
});
1370-
} else {
1371-
initialLang = result || [];
1372-
}
1373-
}
1421+
const initialLang = resolveLanguageExtension(
1422+
file,
1423+
languageSignature,
1424+
"async-language-reconfigure",
1425+
);
13741426
// Ensure language compartment is present (empty -> plain text)
13751427
exts.push(languageCompartment.of(initialLang));
13761428
} catch (e) {
@@ -1402,6 +1454,9 @@ async function EditorManager($header, $body) {
14021454
file.session = state;
14031455
file.__cmSessionReady = true;
14041456
file.__cmExtensionSignature = extensionSignature;
1457+
if (file.__cmLanguageReady) {
1458+
markLanguageReady(file, languageSignature, true);
1459+
}
14051460
editor.setState(state);
14061461
applyCurrentEditorOptions(file);
14071462

@@ -2502,15 +2557,23 @@ async function EditorManager($header, $body) {
25022557
`cache-flush-${prev.id}`,
25032558
);
25042559
});
2505-
}, 250);
2560+
}, 1000);
25062561
}
25072562

25082563
manager.activeFile = file;
2564+
file.tab.classList.add("active");
2565+
file.tab.scrollIntoView();
2566+
$header.text = file.filename;
2567+
$header.subText = file.headerSubtitle || "";
25092568

25102569
if (file.type === "editor") {
25112570
touchSelectionController?.setEnabled(true);
2512-
// Apply active file content and language to CodeMirror
2513-
applyFileToEditor(file);
2571+
if (!file.loaded && !file.loading) {
2572+
showLoadingEditor(file);
2573+
} else {
2574+
// Apply active file content and language to CodeMirror
2575+
applyFileToEditor(file);
2576+
}
25142577
$container.style.display = "block";
25152578

25162579
$hScrollbar.hideImmediately();
@@ -2530,12 +2593,6 @@ async function EditorManager($header, $body) {
25302593
}
25312594
}
25322595
}
2533-
2534-
file.tab.classList.add("active");
2535-
file.tab.scrollIntoView();
2536-
2537-
$header.text = file.filename;
2538-
$header.subText = file.headerSubtitle || "";
25392596
manager.onupdate("switch-file");
25402597
events.emit("switch-file", file);
25412598

0 commit comments

Comments
 (0)