@actualwave/react-native-codeditor 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.2
4
+
5
+ ### Fixed
6
+ - A `content` change that arrives while the WebView is still booting is now applied on initialization instead of being dropped.
7
+ - `resetValue()` and `historyClear()` now really empty the undo history (they rebuild the editor
8
+ state, keeping the live extensions). Before, they only dispatched an unrecorded transaction, so
9
+ earlier undo steps survived and were applied on top of the new document.
10
+
11
+ ### Added
12
+ - `saveState()` / `restoreState(snapshot)`: JSON snapshot of document, selection, undo/redo history
13
+ and scroll offset, so a document can leave and re-enter an editor without losing its history.
14
+ `restoreState` keeps the live extensions (language, theme, shortcuts, completions) and fires the
15
+ content / history-size callbacks. Exports the `EditorStateSnapshot` type.
16
+ If a snapshot's undo history does not match its document (undo/redo would throw "Applying change
17
+ set to a document with the wrong length"), `restoreState` logs a warning and restores the text,
18
+ selection and scroll only, with an empty history.
19
+
3
20
  ## 1.2.1
4
21
 
5
22
  ### Changed
package/README.md CHANGED
@@ -384,6 +384,8 @@ api.editor.historyClear(): Promise<void>
384
384
  // Clears the undo/redo history without changing the document.
385
385
 
386
386
  api.editor.historySize(): Promise<HistorySize>
387
+ api.editor.saveState(): Promise<EditorStateSnapshot> // doc + selection + undo/redo history + scroll (plain JSON)
388
+ api.editor.restoreState(snapshot): Promise<void> // keeps live language/theme/shortcuts; set language/theme first
387
389
  // Returns the current { undo, redo } depth.
388
390
  // Note: history size is also reported automatically via onHistorySizeUpdate.
389
391
  ```
package/SKILL.md CHANGED
@@ -266,6 +266,8 @@ api.editor.historyUndo(): Promise<boolean>
266
266
  api.editor.historyRedo(): Promise<boolean>
267
267
  api.editor.historyClear(): Promise<void>
268
268
  api.editor.historySize(): Promise<HistorySize> // { undo, redo }
269
+ api.editor.saveState(): Promise<EditorStateSnapshot> // doc + selection + undo/redo history + scroll (plain JSON)
270
+ api.editor.restoreState(snapshot): Promise<void> // keeps live language/theme/shortcuts; set language/theme first
269
271
  ```
270
272
 
271
273
  ### Editing commands
@@ -108,6 +108,13 @@ const CodeEditor = ({
108
108
  onInitialized: api => {
109
109
  initializedRef.current = true;
110
110
  setInitialized(true);
111
+ // `content` may have changed while the WebView was still booting (the content effect
112
+ // skips pushing then), so the editor would keep the stale initial document.
113
+ const latest = propsRef.current.content;
114
+ if (latest !== currentContentRef.current) {
115
+ currentContentRef.current = latest;
116
+ void api.editor.setValue(latest);
117
+ }
111
118
  callbacksRef.current.onInitialized(api);
112
119
  },
113
120
  onContentUpdate: value => {
@@ -44,6 +44,12 @@ export interface EmbedsConfig {
44
44
  taggedTemplates?: string[];
45
45
  supportExtensions?: Array<string | [string, Record<string, unknown>]>;
46
46
  }
47
+ /** Opaque, JSON-serializable snapshot produced by `saveState()`. */
48
+ export interface EditorStateSnapshot {
49
+ state: unknown;
50
+ scrollTop: number;
51
+ scrollLeft: number;
52
+ }
47
53
  export interface EditorAPI {
48
54
  getValue(): Promise<string>;
49
55
  setValue(value: string): Promise<void>;
@@ -72,6 +78,17 @@ export interface EditorAPI {
72
78
  historyRedo(): Promise<boolean>;
73
79
  historyClear(): Promise<void>;
74
80
  historySize(): Promise<HistorySize>;
81
+ /**
82
+ * Snapshot of document, selection, undo/redo history and scroll offset. Plain JSON, so it
83
+ * can be kept in memory or persisted. Restore with `restoreState()`.
84
+ */
85
+ saveState(): Promise<EditorStateSnapshot>;
86
+ /**
87
+ * Replaces the editor state with a `saveState()` snapshot, keeping the live extensions
88
+ * (language, theme, shortcuts, completions). Set language/theme first. The content and
89
+ * history-size callbacks fire afterwards.
90
+ */
91
+ restoreState(snapshot: EditorStateSnapshot): Promise<void>;
75
92
  scrollToCursor(margin?: number): Promise<void>;
76
93
  startCompletion(): Promise<void>;
77
94
  setCompletions(items: CompletionItem[]): Promise<void>;
@@ -3,5 +3,5 @@ export { default as CodeEditor } from './CodeEditor';
3
3
  export type { CodeEditorProps } from './CodeEditor';
4
4
  export { default as WebViewAPI } from './WebViewAPI';
5
5
  export type { WebViewAPIHandlers, WebViewRef, InitialConfig } from './WebViewAPI';
6
- export type { EditorAPI, ExtensionSpec, ViewportSettings, CursorPosition, HistorySize, CompletionItem, EmbedsConfig, } from './EditorAPI';
6
+ export type { EditorAPI, ExtensionSpec, ViewportSettings, CursorPosition, HistorySize, EditorStateSnapshot, CompletionItem, EmbedsConfig, } from './EditorAPI';
7
7
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actualwave/react-native-codeditor",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "CodeMirror 6 code editor for React Native, embedded in a WebView with a full bidirectional RPC bridge. Offline, no CDN required.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -228,6 +228,40 @@
228
228
  // setExtensions, loadExtension, destroy — then add methods not in EditorController.
229
229
  const { getValue, setValue, setLanguage, setExtensions, loadExtension, destroy } = editor;
230
230
 
231
+ // Rebuilds the editor state around `doc` with an empty undo history (an unrecorded transaction
232
+ // only hides a change from the history — earlier entries stay and get mapped over it, so Undo
233
+ // would still walk back into the previous document). The live extension set is kept: the new
234
+ // state is built from the current config and the compartments' contents are re-applied.
235
+ const _replaceStateFresh = (doc, selection) => {
236
+ const view = editor.view;
237
+ const old = view.state;
238
+ view.setState(EditorState.create({ doc, selection, extensions: old.config.base }));
239
+ const effects = [];
240
+ old.config.compartments.forEach((ext, comp) => effects.push(comp.reconfigure(ext)));
241
+ if (effects.length) view.dispatch({ effects });
242
+ const state = view.state;
243
+ _rnPost('__contentChange__', {
244
+ value: state.doc.toString(),
245
+ undo: undoDepth(state),
246
+ redo: redoDepth(state),
247
+ });
248
+ };
249
+
250
+ // Dry-runs every undo and redo step on a throw-away copy of `state`. A history that does not
251
+ // match its document makes CodeMirror throw "Applying change set to a document with the wrong
252
+ // length" on the user's first Undo; catching that here keeps it from ever reaching the user.
253
+ const _historyConsistent = (state) => {
254
+ try {
255
+ let s = state;
256
+ const step = (tr) => { s = tr.state; };
257
+ for (let i = 0; i < 1000 && undo({ state: s, dispatch: step }); i++);
258
+ for (let i = 0; i < 1000 && redo({ state: s, dispatch: step }); i++);
259
+ return s.doc.eq(state.doc);
260
+ } catch (e) {
261
+ return false;
262
+ }
263
+ };
264
+
231
265
  Object.assign(editorTarget, {
232
266
  getValue,
233
267
  setValue: (value) => {
@@ -245,6 +279,51 @@
245
279
  loadExtension,
246
280
  destroy,
247
281
 
282
+ // Serializable snapshot of the whole editor state: document, selection, undo/redo
283
+ // history and scroll offset. Pair with restoreState() to move a document in and out
284
+ // of this editor without losing its history.
285
+ saveState: async () => {
286
+ const { historyField } = await requireAsyncModule('@codemirror/commands');
287
+ return {
288
+ state: editor.view.state.toJSON({ history: historyField }),
289
+ scrollTop: editor.view.scrollDOM.scrollTop,
290
+ scrollLeft: editor.view.scrollDOM.scrollLeft,
291
+ };
292
+ },
293
+
294
+ // Replaces the editor's state with a snapshot from saveState(). The live extension set
295
+ // (language, theme, shortcuts, completions...) is kept: the new state is built from the
296
+ // current config and the compartments' current contents are re-applied. Set the desired
297
+ // language/theme BEFORE calling this. Notifies the host like a regular content change.
298
+ restoreState: async (snapshot) => {
299
+ const { historyField } = await requireAsyncModule('@codemirror/commands');
300
+ const old = editor.view.state;
301
+ const next = EditorState.fromJSON(
302
+ snapshot.state,
303
+ { extensions: old.config.base },
304
+ { history: historyField }
305
+ );
306
+ if (!_historyConsistent(next)) {
307
+ console.warn('restoreState: snapshot history does not match its document, restoring text only');
308
+ _replaceStateFresh(next.doc, next.selection);
309
+ editor.view.scrollDOM.scrollTop = snapshot.scrollTop ?? 0;
310
+ editor.view.scrollDOM.scrollLeft = snapshot.scrollLeft ?? 0;
311
+ return;
312
+ }
313
+ editor.view.setState(next);
314
+ const effects = [];
315
+ old.config.compartments.forEach((ext, comp) => effects.push(comp.reconfigure(ext)));
316
+ if (effects.length) editor.view.dispatch({ effects });
317
+ editor.view.scrollDOM.scrollTop = snapshot.scrollTop ?? 0;
318
+ editor.view.scrollDOM.scrollLeft = snapshot.scrollLeft ?? 0;
319
+ const state = editor.view.state;
320
+ _rnPost('__contentChange__', {
321
+ value: state.doc.toString(),
322
+ undo: undoDepth(state),
323
+ redo: redoDepth(state),
324
+ });
325
+ },
326
+
248
327
  // Maps a language name to a package that doesn't follow the
249
328
  // @codemirror/lang-{name} convention. Takes effect on the next setLanguage().
250
329
  registerLanguage: (name, packageName) => registerLanguage(name, packageName),
@@ -258,13 +337,8 @@
258
337
  registerSupportExtension: (packageName, supportConfig) =>
259
338
  registerSupportExtension(packageName, supportConfig),
260
339
 
261
- resetValue: (value) => {
262
- const view = editor.view;
263
- view.dispatch({
264
- changes: { from: 0, to: view.state.doc.length, insert: value ?? '' },
265
- annotations: Transaction.addToHistory.of(false),
266
- });
267
- },
340
+ // Replaces the document AND empties the undo history.
341
+ resetValue: (value) => _replaceStateFresh(value ?? ''),
268
342
 
269
343
  setTheme: (themeName) => _applyTheme(themeName),
270
344
 
@@ -358,11 +432,10 @@
358
432
 
359
433
  historyRedo: () => redo(editor.view),
360
434
 
435
+ // Empties the undo/redo history, keeping the document and selection.
361
436
  historyClear: () => {
362
- // clearHistory StateEffect was removed from @codemirror/commands 6.7+.
363
- // Dispatch a no-doc-change transaction marked as not-in-history so
364
- // callers don't crash, though the undo stack is not actually emptied.
365
- editor.view.dispatch({ annotations: Transaction.addToHistory.of(false) });
437
+ const { doc, selection } = editor.view.state;
438
+ _replaceStateFresh(doc, selection);
366
439
  },
367
440
 
368
441
  startCompletion: () => startCompletion(editor.view),