@@ -25,6 +25,189 @@ import run from "./run";
2525import saveFile from "./saveFile" ;
2626import appSettings from "./settings" ;
2727
28+ /**
29+ * Creates a Proxy around an EditorState that provides Ace-compatible methods.
30+ * @param {EditorState } state - The raw CodeMirror EditorState
31+ * @param {EditorFile } file - The parent EditorFile instance
32+ * @returns {Proxy } Proxied state with Ace-compatible methods
33+ */
34+ function createSessionProxy ( state , file ) {
35+ if ( ! state ) return null ;
36+
37+ /**
38+ * Convert Ace position {row, column} to CodeMirror offset
39+ */
40+ function positionToOffset ( pos , doc ) {
41+ if ( ! pos || ! doc ) return 0 ;
42+ try {
43+ const lineNum = Math . max ( 1 , Math . min ( ( pos . row ?? 0 ) + 1 , doc . lines ) ) ;
44+ const line = doc . line ( lineNum ) ;
45+ const col = Math . max ( 0 , Math . min ( pos . column ?? 0 , line . length ) ) ;
46+ return line . from + col ;
47+ } catch ( _ ) {
48+ return 0 ;
49+ }
50+ }
51+
52+ /**
53+ * Convert CodeMirror offset to Ace position {row, column}
54+ */
55+ function offsetToPosition ( offset , doc ) {
56+ if ( ! doc ) return { row : 0 , column : 0 } ;
57+ try {
58+ const line = doc . lineAt ( offset ) ;
59+ return { row : line . number - 1 , column : offset - line . from } ;
60+ } catch ( _ ) {
61+ return { row : 0 , column : 0 } ;
62+ }
63+ }
64+
65+ return new Proxy ( state , {
66+ get ( target , prop ) {
67+ // Ace-compatible method: getValue()
68+ if ( prop === "getValue" ) {
69+ return ( ) => target . doc . toString ( ) ;
70+ }
71+
72+ // Ace-compatible method: setValue(text)
73+ if ( prop === "setValue" ) {
74+ return ( text ) => {
75+ const newText = String ( text ?? "" ) ;
76+ const { activeFile, editor } = editorManager ;
77+ if ( activeFile ?. id === file . id && editor ) {
78+ // Active file: dispatch to live EditorView
79+ editor . dispatch ( {
80+ changes : {
81+ from : 0 ,
82+ to : editor . state . doc . length ,
83+ insert : newText ,
84+ } ,
85+ } ) ;
86+ } else {
87+ // Inactive file: update stored state
88+ file . _setRawSession (
89+ target . update ( {
90+ changes : { from : 0 , to : target . doc . length , insert : newText } ,
91+ } ) . state ,
92+ ) ;
93+ }
94+ } ;
95+ }
96+
97+ // Ace-compatible method: getLine(row)
98+ if ( prop === "getLine" ) {
99+ return ( row ) => {
100+ try {
101+ return target . doc . line ( row + 1 ) . text ;
102+ } catch ( _ ) {
103+ return "" ;
104+ }
105+ } ;
106+ }
107+
108+ // Ace-compatible method: getLength()
109+ if ( prop === "getLength" ) {
110+ return ( ) => target . doc . lines ;
111+ }
112+
113+ // Ace-compatible method: getTextRange(range)
114+ if ( prop === "getTextRange" ) {
115+ return ( range ) => {
116+ if ( ! range ) return "" ;
117+ try {
118+ const from = positionToOffset ( range . start , target . doc ) ;
119+ const to = positionToOffset ( range . end , target . doc ) ;
120+ return target . doc . sliceString ( from , to ) ;
121+ } catch ( _ ) {
122+ return "" ;
123+ }
124+ } ;
125+ }
126+
127+ // Ace-compatible method: insert(position, text)
128+ if ( prop === "insert" ) {
129+ return ( position , text ) => {
130+ const { activeFile, editor } = editorManager ;
131+ const offset = positionToOffset ( position , target . doc ) ;
132+ if ( activeFile ?. id === file . id && editor ) {
133+ editor . dispatch ( {
134+ changes : { from : offset , insert : String ( text ?? "" ) } ,
135+ } ) ;
136+ } else {
137+ file . _setRawSession (
138+ target . update ( {
139+ changes : { from : offset , insert : String ( text ?? "" ) } ,
140+ } ) . state ,
141+ ) ;
142+ }
143+ } ;
144+ }
145+
146+ // Ace-compatible method: remove(range)
147+ if ( prop === "remove" ) {
148+ return ( range ) => {
149+ if ( ! range ) return "" ;
150+ const from = positionToOffset ( range . start , target . doc ) ;
151+ const to = positionToOffset ( range . end , target . doc ) ;
152+ const removed = target . doc . sliceString ( from , to ) ;
153+ const { activeFile, editor } = editorManager ;
154+ if ( activeFile ?. id === file . id && editor ) {
155+ editor . dispatch ( { changes : { from, to, insert : "" } } ) ;
156+ } else {
157+ file . _setRawSession (
158+ target . update ( { changes : { from, to, insert : "" } } ) . state ,
159+ ) ;
160+ }
161+ return removed ;
162+ } ;
163+ }
164+
165+ // Ace-compatible method: replace(range, text)
166+ if ( prop === "replace" ) {
167+ return ( range , text ) => {
168+ if ( ! range ) return ;
169+ const from = positionToOffset ( range . start , target . doc ) ;
170+ const to = positionToOffset ( range . end , target . doc ) ;
171+ const { activeFile, editor } = editorManager ;
172+ if ( activeFile ?. id === file . id && editor ) {
173+ editor . dispatch ( {
174+ changes : { from, to, insert : String ( text ?? "" ) } ,
175+ } ) ;
176+ } else {
177+ file . _setRawSession (
178+ target . update ( {
179+ changes : { from, to, insert : String ( text ?? "" ) } ,
180+ } ) . state ,
181+ ) ;
182+ }
183+ } ;
184+ }
185+
186+ // Ace-compatible method: getWordRange(row, column)
187+ if ( prop === "getWordRange" ) {
188+ return ( row , column ) => {
189+ const offset = positionToOffset ( { row, column } , target . doc ) ;
190+ const word = target . wordAt ( offset ) ;
191+ if ( word ) {
192+ return {
193+ start : offsetToPosition ( word . from , target . doc ) ,
194+ end : offsetToPosition ( word . to , target . doc ) ,
195+ } ;
196+ }
197+ return { start : { row, column } , end : { row, column } } ;
198+ } ;
199+ }
200+
201+ // Pass through all other properties to the real EditorState
202+ const value = target [ prop ] ;
203+ if ( typeof value === "function" ) {
204+ return value . bind ( target ) ;
205+ }
206+ return value ;
207+ } ,
208+ } ) ;
209+ }
210+
28211/**
29212 * @typedef {'run'|'save'|'change'|'focus'|'blur'|'close'|'rename'|'load'|'loadError'|'loadStart'|'loadEnd'|'changeMode'|'changeEncoding'|'changeReadOnly' } FileEvents
30213 */
@@ -99,10 +282,10 @@ export default class EditorFile {
99282 */
100283 deletedFile = false ;
101284 /**
102- * CodeMirror document state for the file
285+ * Raw CodeMirror EditorState. Use session getter to access with Ace-compatible methods.
103286 * @type {EditorState }
104287 */
105- session = null ;
288+ #rawSession = null ;
106289 /**
107290 * Encoding of the text e.g. 'gbk'
108291 * @type {string }
@@ -346,7 +529,7 @@ export default class EditorFile {
346529 editorManager . emit ( "new-file" , this ) ;
347530
348531 if ( this . #type === "editor" ) {
349- this . session = EditorState . create ( {
532+ this . #rawSession = EditorState . create ( {
350533 doc : options ?. text || "" ,
351534 } ) ;
352535 this . setMode ( ) ;
@@ -368,6 +551,32 @@ export default class EditorFile {
368551 return this . #content;
369552 }
370553
554+ /**
555+ * Session with Ace-compatible methods
556+ * Returns a Proxy over the raw EditorState.
557+ * @returns {Proxy<EditorState> }
558+ */
559+ get session ( ) {
560+ return createSessionProxy ( this . #rawSession, this ) ;
561+ }
562+
563+ /**
564+ * Set the session
565+ * @param {EditorState } value
566+ */
567+ set session ( value ) {
568+ this . #rawSession = value ;
569+ }
570+
571+ /**
572+ * Internal method to update the raw session state.
573+ * Used by the Proxy for inactive file updates.
574+ * @param {EditorState } state
575+ */
576+ _setRawSession ( state ) {
577+ this . #rawSession = state ;
578+ }
579+
371580 /**
372581 * File unique id.
373582 */
@@ -517,9 +726,7 @@ export default class EditorFile {
517726 }
518727
519728 // Update the document in the session
520- this . session = this . session . update ( {
521- changes : { from : 0 , to : this . session . doc . length , insert : text } ,
522- } ) . state ;
729+ this . session . setValue ( text ) ;
523730 }
524731
525732 /**
@@ -1074,13 +1281,7 @@ export default class EditorFile {
10741281 this . loading = true ;
10751282 this . markChanged = false ;
10761283 this . #emit( "loadstart" , createFileEvent ( this ) ) ;
1077- this . session = this . session . update ( {
1078- changes : {
1079- from : 0 ,
1080- to : this . session . doc . length ,
1081- insert : strings [ "loading..." ] ,
1082- } ,
1083- } ) . state ;
1284+ this . session . setValue ( strings [ "loading..." ] ) ;
10841285
10851286 // Immediately reflect "loading..." in the visible editor if this tab is active
10861287 try {
@@ -1113,9 +1314,7 @@ export default class EditorFile {
11131314 }
11141315
11151316 this . markChanged = false ;
1116- this . session = this . session . update ( {
1117- changes : { from : 0 , to : this . session . doc . length , insert : value } ,
1118- } ) . state ;
1317+ this . session . setValue ( value ) ;
11191318 this . loaded = true ;
11201319 this . loading = false ;
11211320
0 commit comments