11import sidebarApps from "sidebarApps" ;
2- import { indentUnit } from "@codemirror/language" ;
2+ import { indentUnit , language as languageFacet } from "@codemirror/language" ;
33import { search } from "@codemirror/search" ;
44import { Compartment , EditorState , Prec , StateEffect } from "@codemirror/state" ;
55import { oneDark } from "@codemirror/theme-one-dark" ;
@@ -12,6 +12,7 @@ import {
1212 highlightWhitespace ,
1313 keymap ,
1414 lineNumbers ,
15+ placeholder ,
1516} from "@codemirror/view" ;
1617import {
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