svedit 0.12.0 → 0.13.0

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.
Files changed (86) hide show
  1. package/README.md +33 -21
  2. package/dist/Command.svelte.d.ts +38 -30
  3. package/dist/Command.svelte.d.ts.map +1 -1
  4. package/dist/Command.svelte.js +271 -326
  5. package/dist/CustomProperty.svelte +3 -5
  6. package/dist/CustomProperty.svelte.d.ts +3 -6
  7. package/dist/CustomProperty.svelte.d.ts.map +1 -1
  8. package/dist/KeyMapper.svelte.d.ts +19 -6
  9. package/dist/KeyMapper.svelte.d.ts.map +1 -1
  10. package/dist/KeyMapper.svelte.js +95 -112
  11. package/dist/Node.svelte +16 -13
  12. package/dist/Node.svelte.d.ts +3 -5
  13. package/dist/Node.svelte.d.ts.map +1 -1
  14. package/dist/NodeArrayProperty.svelte +39 -22
  15. package/dist/NodeArrayProperty.svelte.d.ts +3 -5
  16. package/dist/NodeArrayProperty.svelte.d.ts.map +1 -1
  17. package/dist/NodeCaret.svelte +1 -1
  18. package/dist/NodeCaret.svelte.d.ts +6 -14
  19. package/dist/NodeCaret.svelte.d.ts.map +1 -1
  20. package/dist/NodeGap.svelte +12 -6
  21. package/dist/NodeGap.svelte.d.ts +7 -14
  22. package/dist/NodeGap.svelte.d.ts.map +1 -1
  23. package/dist/NodeGapMarkers.svelte +19 -10
  24. package/dist/NodeGapMarkers.svelte.d.ts +6 -11
  25. package/dist/NodeGapMarkers.svelte.d.ts.map +1 -1
  26. package/dist/NodeSelectionMarkers.svelte +20 -6
  27. package/dist/NodeSelectionMarkers.svelte.d.ts +2 -5
  28. package/dist/NodeSelectionMarkers.svelte.d.ts.map +1 -1
  29. package/dist/Session.svelte.d.ts +81 -129
  30. package/dist/Session.svelte.d.ts.map +1 -1
  31. package/dist/Session.svelte.js +542 -648
  32. package/dist/Svedit.svelte +144 -138
  33. package/dist/Svedit.svelte.d.ts +30 -10
  34. package/dist/Svedit.svelte.d.ts.map +1 -1
  35. package/dist/TextProperty.svelte +32 -18
  36. package/dist/TextProperty.svelte.d.ts +3 -5
  37. package/dist/TextProperty.svelte.d.ts.map +1 -1
  38. package/dist/Transaction.svelte.d.ts +64 -127
  39. package/dist/Transaction.svelte.d.ts.map +1 -1
  40. package/dist/Transaction.svelte.js +839 -1001
  41. package/dist/UnknownNode.svelte +4 -3
  42. package/dist/UnknownNode.svelte.d.ts +5 -9
  43. package/dist/UnknownNode.svelte.d.ts.map +1 -1
  44. package/dist/doc_utils.d.ts +67 -155
  45. package/dist/doc_utils.d.ts.map +1 -1
  46. package/dist/doc_utils.js +619 -863
  47. package/dist/index.d.ts +20 -17
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -17
  50. package/dist/node_visibility.svelte.d.ts +110 -9
  51. package/dist/node_visibility.svelte.d.ts.map +1 -1
  52. package/dist/node_visibility.svelte.js +473 -544
  53. package/dist/paste_utils.d.ts +11 -44
  54. package/dist/paste_utils.d.ts.map +1 -1
  55. package/dist/paste_utils.js +111 -152
  56. package/dist/transforms.svelte.d.ts +9 -7
  57. package/dist/transforms.svelte.d.ts.map +1 -1
  58. package/dist/transforms.svelte.js +125 -150
  59. package/dist/types.d.ts +277 -309
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +1 -0
  62. package/dist/utils.d.ts +40 -119
  63. package/dist/utils.d.ts.map +1 -1
  64. package/dist/utils.js +346 -473
  65. package/package.json +69 -70
  66. package/src/lib/{Command.svelte.js → Command.svelte.ts} +29 -17
  67. package/src/lib/CustomProperty.svelte +3 -5
  68. package/src/lib/{KeyMapper.svelte.js → KeyMapper.svelte.ts} +28 -11
  69. package/src/lib/Node.svelte +16 -13
  70. package/src/lib/NodeArrayProperty.svelte +39 -22
  71. package/src/lib/NodeCaret.svelte +1 -1
  72. package/src/lib/NodeGap.svelte +12 -6
  73. package/src/lib/NodeGapMarkers.svelte +19 -10
  74. package/src/lib/NodeSelectionMarkers.svelte +20 -6
  75. package/src/lib/{Session.svelte.js → Session.svelte.ts} +130 -127
  76. package/src/lib/Svedit.svelte +144 -138
  77. package/src/lib/TextProperty.svelte +32 -18
  78. package/src/lib/{Transaction.svelte.js → Transaction.svelte.ts} +139 -143
  79. package/src/lib/UnknownNode.svelte +4 -3
  80. package/src/lib/{doc_utils.js → doc_utils.ts} +247 -206
  81. package/src/lib/{index.js → index.ts} +47 -0
  82. package/src/lib/{node_visibility.svelte.js → node_visibility.svelte.ts} +68 -92
  83. package/src/lib/{paste_utils.js → paste_utils.ts} +47 -58
  84. package/src/lib/{transforms.svelte.js → transforms.svelte.ts} +39 -21
  85. package/src/lib/{types.d.ts → types.ts} +143 -66
  86. package/src/lib/{utils.js → utils.ts} +110 -129
@@ -1,7 +1,6 @@
1
1
  import { insert_default_node, break_text_node } from './transforms.svelte.js';
2
2
  import { can_switch_mark_type, get_selected_range_types } from './doc_utils.js';
3
3
  import { is_selection_collapsed, get_char_length, char_slice } from './utils.js';
4
-
5
4
  /**
6
5
  * Base class for commands that can be executed in response to user actions
7
6
  * like keyboard shortcuts, menu items, or toolbar buttons.
@@ -25,87 +24,72 @@ import { is_selection_collapsed, get_char_length, char_slice } from './utils.js'
25
24
  * ```
26
25
  */
27
26
  export default class Command {
28
- /**
29
- * Derived state that indicates if the command is disabled.
30
- * Automatically computed from is_enabled().
31
- */
32
- disabled = $derived(!this.is_enabled());
33
-
34
- /**
35
- * Creates a new Command instance.
36
- *
37
- * @param {any} context - The context object providing access to application state
38
- */
39
- constructor(context) {
40
- this.context = context;
41
- }
42
-
43
- /**
44
- * Determines if the command can currently be executed.
45
- * Override this method to implement command-specific logic.
46
- *
47
- * @returns {boolean} true if the command can be executed, false otherwise
48
- */
49
- is_enabled() {
50
- return true;
51
- }
52
-
53
- /**
54
- * Executes the command.
55
- * Override this method to implement the command's behavior.
56
- * Can be async for commands that need to perform asynchronous operations.
57
- *
58
- * @returns {void | Promise<void>}
59
- */
60
- execute() {
61
- throw new Error('Not implemented');
62
- }
27
+ context;
28
+ /**
29
+ * Derived state that indicates if the command is disabled.
30
+ * Automatically computed from is_enabled().
31
+ */
32
+ disabled = $derived(!this.is_enabled());
33
+ /**
34
+ * Creates a new Command instance.
35
+ *
36
+ * @param context - The context object providing access to application state
37
+ */
38
+ constructor(context) {
39
+ this.context = context;
40
+ }
41
+ /**
42
+ * Determines if the command can currently be executed.
43
+ * Override this method to implement command-specific logic.
44
+ */
45
+ is_enabled() {
46
+ return true;
47
+ }
48
+ /**
49
+ * Executes the command.
50
+ * Override this method to implement the command's behavior.
51
+ * Can be async for commands that need to perform asynchronous operations.
52
+ */
53
+ execute() {
54
+ throw new Error('Not implemented');
55
+ }
63
56
  }
64
-
65
57
  /**
66
58
  * Command that undoes the last change to the document.
67
59
  */
68
60
  export class UndoCommand extends Command {
69
- is_enabled() {
70
- return this.context.editable && this.context.session.can_undo;
71
- }
72
-
73
- execute() {
74
- this.context.session.undo();
75
- }
61
+ is_enabled() {
62
+ return this.context.editable && this.context.session.can_undo;
63
+ }
64
+ execute() {
65
+ this.context.session.undo();
66
+ }
76
67
  }
77
-
78
68
  /**
79
69
  * Command that redoes the last undone change to the document.
80
70
  */
81
71
  export class RedoCommand extends Command {
82
- is_enabled() {
83
- return this.context.editable && this.context.session.can_redo;
84
- }
85
-
86
- execute() {
87
- this.context.session.redo();
88
- }
72
+ is_enabled() {
73
+ return this.context.editable && this.context.session.can_redo;
74
+ }
75
+ execute() {
76
+ this.context.session.redo();
77
+ }
89
78
  }
90
-
91
79
  /**
92
80
  * Command that selects the parent of the current selection.
93
81
  * Useful for navigating up the document hierarchy.
94
82
  */
95
83
  export class SelectParentCommand extends Command {
96
- is_enabled() {
97
- return (
98
- this.context.editable &&
99
- this.context.session.selection &&
100
- this.context.session.selection.path.length > 3
101
- );
102
- }
103
-
104
- execute() {
105
- this.context.session.select_parent();
106
- }
84
+ is_enabled() {
85
+ return Boolean(this.context.editable &&
86
+ this.context.session.selection &&
87
+ this.context.session.selection.path.length > 3);
88
+ }
89
+ execute() {
90
+ this.context.session.select_parent();
91
+ }
107
92
  }
108
-
109
93
  /**
110
94
  * Generic command that toggles a mark on the current text or node selection.
111
95
  *
@@ -114,51 +98,43 @@ export class SelectParentCommand extends Command {
114
98
  * touched types disable the command. Annotations never affect mark toggling.
115
99
  */
116
100
  export class ToggleMarkCommand extends Command {
117
- constructor(node_type, context) {
118
- super(context);
119
- this.node_type = node_type;
120
- }
121
-
122
- active = $derived(this.is_active());
123
-
124
- is_active() {
125
- const selected_marks = this.context.session.selected_marks;
126
- return (
127
- selected_marks.length > 0 &&
128
- selected_marks.every(({ node }) => node?.type === this.node_type)
129
- );
130
- }
131
-
132
- is_enabled() {
133
- const { session, editable } = this.context;
134
- const selection = session.selection;
135
- const is_valid_selection =
136
- selection?.type === 'text' ||
137
- (selection?.type === 'node' && !is_selection_collapsed(selection));
138
- const mark_type_is_allowed = session.available_mark_types.includes(this.node_type);
139
- const selected_marks = session.selected_marks;
140
- const selected_mark_types = get_selected_range_types(selected_marks);
141
-
142
- if (!editable || !is_valid_selection || !mark_type_is_allowed) return false;
143
- if (selected_mark_types.size > 1) return false;
144
-
145
- if (selected_marks.length === 0) {
146
- return Boolean(selection && !is_selection_collapsed(selection));
147
- }
148
-
149
- const first_selected_mark = selected_marks[0];
150
- const selected_mark_type = first_selected_mark.node.type;
151
- if (selected_mark_type === this.node_type) return true;
152
- if (selected_marks.length !== 1) return false;
153
-
154
- return can_switch_mark_type(session.schema, selected_mark_type, this.node_type);
155
- }
156
-
157
- execute() {
158
- this.context.session.apply(this.context.session.tr.toggle_mark(this.node_type));
159
- }
101
+ node_type;
102
+ constructor(node_type, context) {
103
+ super(context);
104
+ this.node_type = node_type;
105
+ }
106
+ active = $derived(this.is_active());
107
+ is_active() {
108
+ const selected_marks = this.context.session.selected_marks;
109
+ return (selected_marks.length > 0 && selected_marks.every(({ node }) => node?.type === this.node_type));
110
+ }
111
+ is_enabled() {
112
+ const { session, editable } = this.context;
113
+ const selection = session.selection;
114
+ const is_valid_selection = selection?.type === 'text' ||
115
+ (selection?.type === 'node' && !is_selection_collapsed(selection));
116
+ const mark_type_is_allowed = session.available_mark_types.includes(this.node_type);
117
+ const selected_marks = session.selected_marks;
118
+ const selected_mark_types = get_selected_range_types(selected_marks);
119
+ if (!editable || !is_valid_selection || !mark_type_is_allowed)
120
+ return false;
121
+ if (selected_mark_types.size > 1)
122
+ return false;
123
+ if (selected_marks.length === 0) {
124
+ return Boolean(selection && !is_selection_collapsed(selection));
125
+ }
126
+ const first_selected_mark = selected_marks[0];
127
+ const selected_mark_type = first_selected_mark.node.type;
128
+ if (selected_mark_type === this.node_type)
129
+ return true;
130
+ if (selected_marks.length !== 1)
131
+ return false;
132
+ return can_switch_mark_type(session.schema, selected_mark_type, this.node_type);
133
+ }
134
+ execute() {
135
+ this.context.session.apply(this.context.session.tr.toggle_mark(this.node_type));
136
+ }
160
137
  }
161
-
162
138
  /**
163
139
  * Generic command that toggles an annotation on the current text or node selection.
164
140
  *
@@ -167,246 +143,215 @@ export class ToggleMarkCommand extends Command {
167
143
  * Marks and other annotation types never block the toggle.
168
144
  */
169
145
  export class ToggleAnnotationCommand extends Command {
170
- constructor(node_type, context) {
171
- super(context);
172
- this.node_type = node_type;
173
- }
174
-
175
- active = $derived(this.is_active());
176
-
177
- // Mirrors the filter in tr.toggle_annotation, so active/enabled match what
178
- // execute() does.
179
- relevant_annotations() {
180
- return this.context.session.selected_annotations.filter(
181
- ({ node }) => node?.type === this.node_type
182
- );
183
- }
184
-
185
- is_active() {
186
- return this.relevant_annotations().length > 0;
187
- }
188
-
189
- is_enabled() {
190
- const { session, editable } = this.context;
191
- const selection = session.selection;
192
- const is_valid_selection =
193
- selection?.type === 'text' ||
194
- (selection?.type === 'node' && !is_selection_collapsed(selection));
195
- const annotation_type_is_allowed = session.available_annotation_types.includes(this.node_type);
196
-
197
- if (!editable || !is_valid_selection || !annotation_type_is_allowed) return false;
198
-
199
- // Removing touched same-type annotations also works from a collapsed
200
- // caret inside the annotation; creating a new one requires an expanded
201
- // selection.
202
- if (this.relevant_annotations().length > 0) return true;
203
- return Boolean(selection && !is_selection_collapsed(selection));
204
- }
205
-
206
- execute() {
207
- this.context.session.apply(this.context.session.tr.toggle_annotation(this.node_type));
208
- }
146
+ node_type;
147
+ constructor(node_type, context) {
148
+ super(context);
149
+ this.node_type = node_type;
150
+ }
151
+ active = $derived(this.is_active());
152
+ // Mirrors the filter in tr.toggle_annotation, so active/enabled match what
153
+ // execute() does.
154
+ relevant_annotations() {
155
+ return this.context.session.selected_annotations.filter(({ node }) => node?.type === this.node_type);
156
+ }
157
+ is_active() {
158
+ return this.relevant_annotations().length > 0;
159
+ }
160
+ is_enabled() {
161
+ const { session, editable } = this.context;
162
+ const selection = session.selection;
163
+ const is_valid_selection = selection?.type === 'text' ||
164
+ (selection?.type === 'node' && !is_selection_collapsed(selection));
165
+ const annotation_type_is_allowed = session.available_annotation_types.includes(this.node_type);
166
+ if (!editable || !is_valid_selection || !annotation_type_is_allowed)
167
+ return false;
168
+ // Removing touched same-type annotations also works from a collapsed
169
+ // caret inside the annotation; creating a new one requires an expanded
170
+ // selection.
171
+ if (this.relevant_annotations().length > 0)
172
+ return true;
173
+ return Boolean(selection && !is_selection_collapsed(selection));
174
+ }
175
+ execute() {
176
+ this.context.session.apply(this.context.session.tr.toggle_annotation(this.node_type));
177
+ }
209
178
  }
210
-
211
179
  /**
212
180
  * Command that adds a new line character at the current caret position.
213
181
  * Only works in text selections where newlines are allowed.
214
182
  */
215
183
  export class AddNewLineCommand extends Command {
216
- is_enabled() {
217
- const session = this.context.session;
218
- const selection = session.selection;
219
-
220
- return (
221
- this.context.editable &&
222
- selection?.type === 'text' &&
223
- session.inspect(selection.path).allow_newlines
224
- );
225
- }
226
-
227
- execute() {
228
- const session = this.context.session;
229
- const selection = session.selection;
230
- if (!selection || selection.type !== 'text') return;
231
- if (!session.inspect(selection.path).allow_newlines) return;
232
-
233
- const tr = session.tr;
234
- if (selection.anchor_offset !== selection.focus_offset) {
235
- tr.delete_selection();
236
- }
237
-
238
- const collapsed_offset = tr.selection.anchor_offset;
239
- const content = tr.get(tr.selection.path);
240
- const text_before_caret = char_slice(content.content, 0, collapsed_offset);
241
- const line_start_index = text_before_caret.lastIndexOf('\n') + 1;
242
- const current_line_prefix = text_before_caret.slice(line_start_index);
243
- const indentation_match = current_line_prefix.match(/^[\t ]*/);
244
- const indentation = indentation_match ? indentation_match[0] : '';
245
-
246
- tr.insert_text(`\n${indentation}`);
247
- session.apply(tr);
248
- }
184
+ is_enabled() {
185
+ const session = this.context.session;
186
+ const selection = session.selection;
187
+ return (this.context.editable &&
188
+ selection?.type === 'text' &&
189
+ session.inspect(selection.path).allow_newlines);
190
+ }
191
+ execute() {
192
+ const session = this.context.session;
193
+ const selection = session.selection;
194
+ if (!selection || selection.type !== 'text')
195
+ return;
196
+ if (!session.inspect(selection.path).allow_newlines)
197
+ return;
198
+ const tr = session.tr;
199
+ if (selection.anchor_offset !== selection.focus_offset) {
200
+ tr.delete_selection();
201
+ }
202
+ if (tr.selection.type !== 'text')
203
+ return;
204
+ const collapsed_offset = tr.selection.anchor_offset;
205
+ const content = tr.get(tr.selection.path);
206
+ const text_before_caret = char_slice(content.content, 0, collapsed_offset);
207
+ const line_start_index = text_before_caret.lastIndexOf('\n') + 1;
208
+ const current_line_prefix = text_before_caret.slice(line_start_index);
209
+ const indentation_match = current_line_prefix.match(/^[\t ]*/);
210
+ const indentation = indentation_match ? indentation_match[0] : '';
211
+ tr.insert_text(`\n${indentation}`);
212
+ session.apply(tr);
213
+ }
249
214
  }
250
-
251
215
  /**
252
216
  * Command that breaks a text node at the caret position.
253
217
  * Creates a new node and splits the content between the current and new node.
254
218
  * Only works in text selections.
255
219
  */
256
220
  export class BreakTextNodeCommand extends Command {
257
- is_enabled() {
258
- const session = this.context.session;
259
- const selection = session.selection;
260
- if (!this.context.editable || selection?.type !== 'text') return false;
261
-
262
- const owner_node = session.get(selection.path.slice(0, -1));
263
- const owner_node_schema = owner_node ? session.schema[owner_node.type] : null;
264
- if (!owner_node_schema || owner_node_schema.kind !== 'text') return false;
265
-
266
- return session.inspect(selection.path.slice(0, -2))?.type === 'node_array';
267
- }
268
-
269
- execute() {
270
- const tr = this.context.session.tr;
271
- if (break_text_node(tr)) {
272
- this.context.session.apply(tr);
273
- }
274
- }
221
+ is_enabled() {
222
+ const session = this.context.session;
223
+ const selection = session.selection;
224
+ if (!this.context.editable || selection?.type !== 'text')
225
+ return false;
226
+ const owner_node = session.get(selection.path.slice(0, -1));
227
+ const owner_node_schema = owner_node ? session.schema[owner_node.type] : null;
228
+ if (!owner_node_schema || owner_node_schema.kind !== 'text')
229
+ return false;
230
+ return session.inspect(selection.path.slice(0, -2))?.type === 'node_array';
231
+ }
232
+ execute() {
233
+ const tr = this.context.session.tr;
234
+ if (break_text_node(tr)) {
235
+ this.context.session.apply(tr);
236
+ }
237
+ }
275
238
  }
276
-
277
239
  /**
278
240
  * Command that selects all content in the current context.
279
241
  * Progressively expands selection from text → node → parent node array.
280
242
  */
281
243
  export class SelectAllCommand extends Command {
282
- is_enabled() {
283
- return this.context.editable && this.context.session.selection;
284
- }
285
-
286
- execute() {
287
- const session = this.context.session;
288
- const selection = session.selection;
289
-
290
- if (!selection) {
291
- return;
292
- }
293
-
294
- if (selection.type === 'text') {
295
- const text_content = session.get(selection.path);
296
- const text_length = get_char_length(text_content.content);
297
-
298
- // Check if all text is already selected
299
- const is_all_text_selected =
300
- Math.min(selection.anchor_offset, selection.focus_offset) === 0 &&
301
- Math.max(selection.anchor_offset, selection.focus_offset) === text_length;
302
-
303
- if (!is_all_text_selected) {
304
- // Select all text in the current text node
305
- session.selection = {
306
- type: 'text',
307
- path: selection.path,
308
- anchor_offset: 0,
309
- focus_offset: text_length
310
- };
311
- } else {
312
- // All text is selected, move up to select the containing node
313
- const node_path = selection.path.slice(0, -1); // Remove the property name (e.g., 'content')
314
-
315
- // Check if we have enough path segments and if we're inside a node_array
316
- if (node_path.length >= 2) {
317
- const is_inside_node_array =
318
- session.inspect(node_path.slice(0, -1))?.type === 'node_array';
319
-
320
- if (is_inside_node_array) {
321
- const node_index = parseInt(node_path.at(-1));
322
- session.selection = {
323
- type: 'node',
324
- path: node_path.slice(0, -1),
325
- anchor_offset: node_index,
326
- focus_offset: node_index + 1
327
- };
328
- }
329
- }
330
- // Stop expanding - text is not in a selectable node_array
331
- }
332
- } else if (selection.type === 'node') {
333
- const node_array_path = selection.path;
334
- const node_array = session.get(node_array_path);
335
-
336
- // Check if the entire node_array is already selected
337
- const is_entire_node_array_selected =
338
- Math.min(selection.anchor_offset, selection.focus_offset) === 0 &&
339
- Math.max(selection.anchor_offset, selection.focus_offset) === node_array.nodes.length;
340
-
341
- if (!is_entire_node_array_selected) {
342
- // Select the entire node_array
343
- session.selection = {
344
- type: 'node',
345
- path: node_array_path,
346
- anchor_offset: 0,
347
- focus_offset: node_array.nodes.length
348
- };
349
- } else {
350
- // Entire node_array is selected, try to move up to parent node_array
351
- const parent_path = node_array_path.slice(0, -1);
352
-
353
- // Check if we have enough path segments and if parent is a valid node_array
354
- if (parent_path.length >= 2) {
355
- const is_parent_node_array =
356
- session.inspect(parent_path.slice(0, -1))?.type === 'node_array';
357
-
358
- if (is_parent_node_array) {
359
- const parent_node_index = parseInt(parent_path.at(-1));
360
- session.selection = {
361
- type: 'node',
362
- path: parent_path.slice(0, -1),
363
- anchor_offset: parent_node_index,
364
- focus_offset: parent_node_index + 1
365
- };
366
- }
367
- }
368
- // Stop expanding - we've reached the top level
369
- }
370
- } else if (selection.type === 'property') {
371
- // For property selections, select the containing node
372
- const node_path = selection.path.slice(0, -1);
373
-
374
- // Check if we have enough path segments and if we're inside a node_array
375
- if (node_path.length >= 2) {
376
- const is_inside_node_array = session.inspect(node_path.slice(0, -1))?.type === 'node_array';
377
-
378
- if (is_inside_node_array) {
379
- const node_index = parseInt(node_path.at(-1));
380
- session.selection = {
381
- type: 'node',
382
- path: node_path.slice(0, -1),
383
- anchor_offset: node_index,
384
- focus_offset: node_index + 1
385
- };
386
- }
387
- }
388
- // Stop expanding - property is not in a selectable node_array
389
- }
390
- }
244
+ is_enabled() {
245
+ return Boolean(this.context.editable && this.context.session.selection);
246
+ }
247
+ execute() {
248
+ const session = this.context.session;
249
+ const selection = session.selection;
250
+ if (!selection) {
251
+ return;
252
+ }
253
+ if (selection.type === 'text') {
254
+ const text_content = session.get(selection.path);
255
+ const text_length = get_char_length(text_content.content);
256
+ // Check if all text is already selected
257
+ const is_all_text_selected = Math.min(selection.anchor_offset, selection.focus_offset) === 0 &&
258
+ Math.max(selection.anchor_offset, selection.focus_offset) === text_length;
259
+ if (!is_all_text_selected) {
260
+ // Select all text in the current text node
261
+ session.selection = {
262
+ type: 'text',
263
+ path: selection.path,
264
+ anchor_offset: 0,
265
+ focus_offset: text_length
266
+ };
267
+ }
268
+ else {
269
+ // All text is selected, move up to select the containing node
270
+ const node_path = selection.path.slice(0, -1); // Remove the property name (e.g., 'content')
271
+ // Check if we have enough path segments and if we're inside a node_array
272
+ if (node_path.length >= 2) {
273
+ const is_inside_node_array = session.inspect(node_path.slice(0, -1))?.type === 'node_array';
274
+ if (is_inside_node_array) {
275
+ const node_index = node_path.at(-1);
276
+ session.selection = {
277
+ type: 'node',
278
+ path: node_path.slice(0, -1),
279
+ anchor_offset: node_index,
280
+ focus_offset: node_index + 1
281
+ };
282
+ }
283
+ }
284
+ // Stop expanding - text is not in a selectable node_array
285
+ }
286
+ }
287
+ else if (selection.type === 'node') {
288
+ const node_array_path = selection.path;
289
+ const node_array = session.get(node_array_path);
290
+ // Check if the entire node_array is already selected
291
+ const is_entire_node_array_selected = Math.min(selection.anchor_offset, selection.focus_offset) === 0 &&
292
+ Math.max(selection.anchor_offset, selection.focus_offset) === node_array.nodes.length;
293
+ if (!is_entire_node_array_selected) {
294
+ // Select the entire node_array
295
+ session.selection = {
296
+ type: 'node',
297
+ path: node_array_path,
298
+ anchor_offset: 0,
299
+ focus_offset: node_array.nodes.length
300
+ };
301
+ }
302
+ else {
303
+ // Entire node_array is selected, try to move up to parent node_array
304
+ const parent_path = node_array_path.slice(0, -1);
305
+ // Check if we have enough path segments and if parent is a valid node_array
306
+ if (parent_path.length >= 2) {
307
+ const is_parent_node_array = session.inspect(parent_path.slice(0, -1))?.type === 'node_array';
308
+ if (is_parent_node_array) {
309
+ const parent_node_index = parent_path.at(-1);
310
+ session.selection = {
311
+ type: 'node',
312
+ path: parent_path.slice(0, -1),
313
+ anchor_offset: parent_node_index,
314
+ focus_offset: parent_node_index + 1
315
+ };
316
+ }
317
+ }
318
+ // Stop expanding - we've reached the top level
319
+ }
320
+ }
321
+ else if (selection.type === 'property') {
322
+ // For property selections, select the containing node
323
+ const node_path = selection.path.slice(0, -1);
324
+ // Check if we have enough path segments and if we're inside a node_array
325
+ if (node_path.length >= 2) {
326
+ const is_inside_node_array = session.inspect(node_path.slice(0, -1))?.type === 'node_array';
327
+ if (is_inside_node_array) {
328
+ const node_index = node_path.at(-1);
329
+ session.selection = {
330
+ type: 'node',
331
+ path: node_path.slice(0, -1),
332
+ anchor_offset: node_index,
333
+ focus_offset: node_index + 1
334
+ };
335
+ }
336
+ }
337
+ // Stop expanding - property is not in a selectable node_array
338
+ }
339
+ }
391
340
  }
392
-
393
341
  /**
394
342
  * Command that inserts a default node at the current caret position.
395
343
  * Only works when a collapsed node selection is active.
396
344
  */
397
345
  export class InsertDefaultNodeCommand extends Command {
398
- is_enabled() {
399
- const selection = this.context.session.selection;
400
- return (
401
- this.context.editable &&
402
- selection?.type === 'node' &&
403
- selection.anchor_offset === selection.focus_offset
404
- );
405
- }
406
-
407
- execute() {
408
- const tr = this.context.session.tr;
409
- insert_default_node(tr);
410
- this.context.session.apply(tr);
411
- }
346
+ is_enabled() {
347
+ const selection = this.context.session.selection;
348
+ return (this.context.editable &&
349
+ selection?.type === 'node' &&
350
+ selection.anchor_offset === selection.focus_offset);
351
+ }
352
+ execute() {
353
+ const tr = this.context.session.tr;
354
+ insert_default_node(tr);
355
+ this.context.session.apply(tr);
356
+ }
412
357
  }