@orkestrel/workspace 0.0.12 → 0.0.14

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.
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _orkestrel_contract = require("@orkestrel/contract");
3
3
  let _orkestrel_database = require("@orkestrel/database");
4
4
  let _orkestrel_emitter = require("@orkestrel/emitter");
5
- //#region src/core/constants.ts
5
+ // src/core/constants.ts
6
6
  /**
7
7
  * Maps file extensions to language tags for text content.
8
8
  *
@@ -51,8 +51,7 @@ var EXTENSION_LANGUAGES = Object.freeze({
51
51
  svg: "xml",
52
52
  txt: "text"
53
53
  });
54
- //#endregion
55
- //#region src/core/errors.ts
54
+ // src/core/errors.ts
56
55
  /**
57
56
  * Reports an invalid workspace edit or search operation, carrying a {@link WorkspaceErrorCode}
58
57
  * and, when the operation had one, the context it ran under.
@@ -69,6 +68,7 @@ var WorkspaceError = class extends Error {
69
68
  */
70
69
  constructor(code, message, context) {
71
70
  super(message);
71
+ _orkestrel_contract.INTRINSICS.define(this, Symbol.for("@orkestrel/workspace.WorkspaceError"), { value: this });
72
72
  this.name = "WorkspaceError";
73
73
  this.code = code;
74
74
  if (context !== void 0) this.context = context;
@@ -78,8 +78,8 @@ var WorkspaceError = class extends Error {
78
78
  * Narrows a caught value to a {@link WorkspaceError}.
79
79
  *
80
80
  * @remarks
81
- * The check is one `instanceof` test, so it stays total: every input answers `true` or `false`
82
- * and none throws.
81
+ * Checks the identity brand and declared members across package copies, accepting subclasses
82
+ * and refusing transparent proxies. Inspection failures return false.
83
83
  *
84
84
  * @param value - The caught value
85
85
  * @returns True if the value is a workspace error; false otherwise
@@ -90,10 +90,16 @@ var WorkspaceError = class extends Error {
90
90
  * ```
91
91
  */
92
92
  function isWorkspaceError(value) {
93
- return (0, _orkestrel_contract.isInstance)(value, WorkspaceError);
93
+ return (0, _orkestrel_contract.holds)(() => {
94
+ if (!(value instanceof Error) || _orkestrel_contract.INTRINSICS.prototype(value) === Error.prototype) return false;
95
+ if (_orkestrel_contract.INTRINSICS.describe(value, Symbol.for("@orkestrel/workspace.WorkspaceError"))?.value !== value) return false;
96
+ const code = _orkestrel_contract.INTRINSICS.reflect.read(value, "code");
97
+ if (code !== "MISSING" && code !== "MODALITY" && code !== "PATTERN" && code !== "RANGE") return false;
98
+ const context = _orkestrel_contract.INTRINSICS.reflect.read(value, "context");
99
+ return context === void 0 || (0, _orkestrel_contract.isObject)(context) || (0, _orkestrel_contract.isFunction)(context);
100
+ });
94
101
  }
95
- //#endregion
96
- //#region src/core/helpers.ts
102
+ // src/core/helpers.ts
97
103
  /**
98
104
  * Infers a language tag from the final file extension.
99
105
  *
@@ -118,10 +124,10 @@ function inferLanguage(path) {
118
124
  *
119
125
  * @example
120
126
  * ```ts
121
- * isText({ text: 'hello', language: 'text' }) // true
127
+ * isTextContent({ text: 'hello', language: 'text' }) // true
122
128
  * ```
123
129
  */
124
- function isText(content) {
130
+ function isTextContent(content) {
125
131
  return "text" in content;
126
132
  }
127
133
  /**
@@ -150,7 +156,7 @@ function isBinary(content) {
150
156
  * ```
151
157
  */
152
158
  function computeSize(content) {
153
- if (isText(content)) return new TextEncoder().encode(content.text).length;
159
+ if (isTextContent(content)) return new TextEncoder().encode(content.text).length;
154
160
  return computeDecodedSize(content.base64);
155
161
  }
156
162
  /**
@@ -165,11 +171,9 @@ function computeSize(content) {
165
171
  * ```
166
172
  */
167
173
  function countLines(content) {
168
- if (!isText(content)) return 0;
174
+ if (!isTextContent(content)) return 0;
169
175
  if (content.text.length === 0) return 0;
170
- let count = 1;
171
- for (const character of content.text) if (character === "\n") count += 1;
172
- return count;
176
+ return content.text.split(/\r\n|\n/).length;
173
177
  }
174
178
  /**
175
179
  * Computes the decoded byte length of a base64 string, arithmetically rather than by decoding it.
@@ -183,16 +187,14 @@ function countLines(content) {
183
187
  * ```
184
188
  */
185
189
  function computeDecodedSize(base64) {
186
- let padding = 0;
187
- if (base64.endsWith("==")) padding = 2;
188
- else if (base64.endsWith("=")) padding = 1;
190
+ const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
189
191
  return Math.floor(base64.length * 3 / 4) - padding;
190
192
  }
191
193
  /**
192
194
  * Determines whether a 1-based range is structurally valid.
193
195
  *
194
196
  * @param range - The range to inspect
195
- * @returns True if both positions are positive and ordered; false otherwise
197
+ * @returns True if both carets are positive and ordered; false otherwise
196
198
  *
197
199
  * @example
198
200
  * ```ts
@@ -206,28 +208,28 @@ function isValidRange(range) {
206
208
  return !(range.start.line === range.end.line && range.start.column > range.end.column);
207
209
  }
208
210
  /**
209
- * Clamps a position to text bounds.
211
+ * Clamps a caret to text bounds.
210
212
  *
211
213
  * @param text - The addressed text
212
- * @param position - The requested position
213
- * @returns The clamped position
214
+ * @param caret - The requested caret
215
+ * @returns The clamped caret
214
216
  *
215
217
  * @example
216
218
  * ```ts
217
- * clampPosition('ab', { line: 9, column: 9 }) // { line: 1, column: 3 }
219
+ * clampCaret('ab', { line: 9, column: 9 }) // { line: 1, column: 3 }
218
220
  * ```
219
221
  */
220
- function clampPosition(text, position) {
222
+ function clampCaret(text, caret) {
221
223
  const lines = text.split("\n");
222
- const line = Math.max(1, Math.min(position.line, lines.length));
224
+ const line = Math.max(1, Math.min(caret.line, lines.length));
223
225
  const lineText = lines[line - 1] ?? "";
224
226
  return {
225
227
  line,
226
- column: Math.max(1, Math.min(position.column, lineText.length + 1))
228
+ column: Math.max(1, Math.min(caret.column, lineText.length + 1))
227
229
  };
228
230
  }
229
231
  /**
230
- * Clamps both positions in a range to text bounds.
232
+ * Clamps both carets in a range to text bounds.
231
233
  *
232
234
  * @param text - The addressed text
233
235
  * @param range - The requested range
@@ -240,15 +242,15 @@ function clampPosition(text, position) {
240
242
  */
241
243
  function clampRange(text, range) {
242
244
  return {
243
- start: clampPosition(text, range.start),
244
- end: clampPosition(text, range.end)
245
+ start: clampCaret(text, range.start),
246
+ end: clampCaret(text, range.end)
245
247
  };
246
248
  }
247
249
  /**
248
- * Converts a 1-based position to a zero-based string offset.
250
+ * Converts a 1-based caret to a zero-based string offset.
249
251
  *
250
252
  * @param text - The addressed text
251
- * @param position - The requested position
253
+ * @param caret - The requested caret
252
254
  * @returns The bounded string offset
253
255
  *
254
256
  * @example
@@ -256,12 +258,9 @@ function clampRange(text, range) {
256
258
  * offsetAt('ab\ncd', { line: 2, column: 1 }) // 3
257
259
  * ```
258
260
  */
259
- function offsetAt(text, position) {
260
- const lines = text.split("\n");
261
- let offset = 0;
262
- for (let index = 0; index < position.line - 1 && index < lines.length; index += 1) offset += (lines[index]?.length ?? 0) + 1;
263
- offset += position.column - 1;
264
- return Math.min(offset, text.length);
261
+ function offsetAt(text, caret) {
262
+ const offset = text.split("\n").filter((_, index) => index < caret.line - 1).reduce((total, line) => total + line.length + 1, 0);
263
+ return Math.min(offset + (caret.column - 1), text.length);
265
264
  }
266
265
  /**
267
266
  * Slices a clamped half-open text range.
@@ -324,22 +323,7 @@ function rangeOf(fromLine, fromColumn, toLine, toColumn) {
324
323
  }
325
324
  };
326
325
  }
327
- /**
328
- * Escapes regular-expression metacharacters for literal matching.
329
- *
330
- * @param value - The literal text
331
- * @returns The escaped regular-expression source
332
- *
333
- * @example
334
- * ```ts
335
- * escapeRegExp('a.b') // 'a\\.b'
336
- * ```
337
- */
338
- function escapeRegExp(value) {
339
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
340
- }
341
- //#endregion
342
- //#region src/core/validators.ts
326
+ // src/core/validators.ts
343
327
  /**
344
328
  * Narrows an unknown value to an immutable file record.
345
329
  *
@@ -376,8 +360,7 @@ function isFile(value) {
376
360
  function isWorkspaceSnapshot(value) {
377
361
  return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isString)(value.id) && (0, _orkestrel_contract.arrayOf)(isFile)(value.files));
378
362
  }
379
- //#endregion
380
- //#region src/core/workspaces/Workspace.ts
363
+ // src/core/workspaces/Workspace.ts
381
364
  /**
382
365
  * Implements `WorkspaceInterface` over one insertion-ordered path map the instance owns,
383
366
  * projecting fresh arrays on every read.
@@ -433,14 +416,14 @@ var Workspace = class {
433
416
  const result = {};
434
417
  for (const one of path) {
435
418
  const file = this.#files.get(one);
436
- if (file && isText(file.content)) result[one] = file.content.text;
419
+ if (file && isTextContent(file.content)) result[one] = file.content.text;
437
420
  }
438
421
  return result;
439
422
  }
440
423
  const file = this.#files.get(path);
441
424
  if (!file) return void 0;
442
- if (!range) return isText(file.content) ? file.content.text : void 0;
443
- if (!isText(file.content)) throw new WorkspaceError("MODALITY", `Cannot read a range of a binary file: ${path}`, { path });
425
+ if (!range) return isTextContent(file.content) ? file.content.text : void 0;
426
+ if (!isTextContent(file.content)) throw new WorkspaceError("MODALITY", `Cannot read a range of a binary file: ${path}`, { path });
444
427
  return {
445
428
  content: sliceRange(file.content.text, range),
446
429
  range: clampRange(file.content.text, range)
@@ -456,14 +439,12 @@ var Workspace = class {
456
439
  const matches = [];
457
440
  for (const file of this.#files.values()) {
458
441
  if (limit !== void 0 && matches.length >= limit) break;
459
- if (!isText(file.content)) continue;
442
+ if (!isTextContent(file.content)) continue;
460
443
  const lines = file.content.text.split(/\r\n|\n/);
461
- for (let index = 0; index < lines.length; index += 1) {
444
+ for (const [index, lineText] of lines.entries()) {
462
445
  if (limit !== void 0 && matches.length >= limit) break;
463
- const lineText = lines[index] ?? "";
464
446
  pattern.lastIndex = 0;
465
- let hit = pattern.exec(lineText);
466
- while (hit !== null) {
447
+ for (const hit of lineText.matchAll(pattern)) {
467
448
  if (limit !== void 0 && matches.length >= limit) break;
468
449
  matches.push({
469
450
  path: file.path,
@@ -472,8 +453,6 @@ var Workspace = class {
472
453
  length: hit[0].length,
473
454
  content: lineText
474
455
  });
475
- if (hit[0].length === 0) pattern.lastIndex += 1;
476
- hit = pattern.exec(lineText);
477
456
  }
478
457
  }
479
458
  }
@@ -482,28 +461,26 @@ var Workspace = class {
482
461
  replace(query, replacement, options) {
483
462
  const pattern = this.#pattern(query, options);
484
463
  const limit = options?.limit;
485
- let occurrences = 0;
486
- let files = 0;
464
+ const changes = [];
487
465
  for (const [path, file] of this.#files) {
466
+ const occurrences = changes.reduce((total, count) => total + count, 0);
488
467
  if (limit !== void 0 && occurrences >= limit) break;
489
- if (!isText(file.content)) continue;
468
+ if (!isTextContent(file.content)) continue;
490
469
  const remaining = limit === void 0 ? void 0 : limit - occurrences;
491
- let count = 0;
492
470
  pattern.lastIndex = 0;
493
- const next = file.content.text.replace(pattern, (match) => {
494
- if (remaining !== void 0 && count >= remaining) return match;
495
- count += 1;
496
- return replacement;
497
- });
471
+ const text = file.content.text;
472
+ const matches = Array.from(text.matchAll(pattern));
473
+ const count = matches.filter((_, index) => remaining === void 0 || !(index >= remaining)).length;
474
+ const positions = matches.keys();
475
+ const next = text.replace(pattern, (match) => (positions.next().value ?? Number.POSITIVE_INFINITY) < count ? replacement : match);
498
476
  if (count > 0) {
499
- occurrences += count;
500
- files += 1;
477
+ changes.push(count);
501
478
  this.write(path, next);
502
479
  }
503
480
  }
504
481
  return {
505
- occurrences,
506
- files
482
+ occurrences: changes.reduce((total, count) => total + count, 0),
483
+ files: changes.length
507
484
  };
508
485
  }
509
486
  write(path, content, range) {
@@ -535,20 +512,12 @@ var Workspace = class {
535
512
  this.#append(path, content);
536
513
  }
537
514
  move(from, to) {
538
- if ((0, _orkestrel_contract.isRecord)(from)) {
539
- let moved = true;
540
- for (const [one, target] of Object.entries(from)) if (!this.#move(one, target)) moved = false;
541
- return moved;
542
- }
515
+ if ((0, _orkestrel_contract.isRecord)(from)) return Object.entries(from).reduce((moved, [one, target]) => this.#move(one, target) && moved, true);
543
516
  if (to === void 0) return false;
544
517
  return this.#move(from, to);
545
518
  }
546
519
  remove(path) {
547
- if ((0, _orkestrel_contract.isArray)(path)) {
548
- let removed = true;
549
- for (const one of path) if (!this.#remove(one)) removed = false;
550
- return removed;
551
- }
520
+ if ((0, _orkestrel_contract.isArray)(path)) return Iterator.from(path).reduce((removed, one) => this.#remove(one) && removed, true);
552
521
  return this.#remove(path);
553
522
  }
554
523
  clear() {
@@ -570,7 +539,7 @@ var Workspace = class {
570
539
  path,
571
540
  content: {
572
541
  text: content,
573
- language: existing && isText(existing.content) ? existing.content.language : inferLanguage(path)
542
+ language: existing && isTextContent(existing.content) ? existing.content.language : inferLanguage(path)
574
543
  },
575
544
  state: existing ? "modified" : "created"
576
545
  });
@@ -580,7 +549,7 @@ var Workspace = class {
580
549
  #splice(path, content, range) {
581
550
  const existing = this.#files.get(path);
582
551
  if (!existing) throw new WorkspaceError("MISSING", `Cannot splice a range of a missing file: ${path}`, { path });
583
- if (!isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot splice a range of a binary file: ${path}`, { path });
552
+ if (!isTextContent(existing.content)) throw new WorkspaceError("MODALITY", `Cannot splice a range of a binary file: ${path}`, { path });
584
553
  if (!isValidRange(range)) throw new WorkspaceError("RANGE", `Invalid range for file: ${path}`, {
585
554
  path,
586
555
  range
@@ -598,21 +567,21 @@ var Workspace = class {
598
567
  }
599
568
  #prepend(path, content) {
600
569
  const existing = this.#files.get(path);
601
- let text = "";
602
- if (existing) {
603
- if (!isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot prepend text to a binary file: ${path}`, { path });
604
- text = existing.content.text;
570
+ if (existing === void 0) {
571
+ this.#write(path, content);
572
+ return;
605
573
  }
606
- this.#write(path, content + text);
574
+ if (!isTextContent(existing.content)) throw new WorkspaceError("MODALITY", `Cannot prepend text to a binary file: ${path}`, { path });
575
+ this.#write(path, content + existing.content.text);
607
576
  }
608
577
  #append(path, content) {
609
578
  const existing = this.#files.get(path);
610
- let text = "";
611
- if (existing) {
612
- if (!isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot append text to a binary file: ${path}`, { path });
613
- text = existing.content.text;
579
+ if (existing === void 0) {
580
+ this.#write(path, content);
581
+ return;
614
582
  }
615
- this.#write(path, text + content);
583
+ if (!isTextContent(existing.content)) throw new WorkspaceError("MODALITY", `Cannot append text to a binary file: ${path}`, { path });
584
+ this.#write(path, existing.content.text + content);
616
585
  }
617
586
  #move(from, to) {
618
587
  if (from === to) return false;
@@ -636,7 +605,7 @@ var Workspace = class {
636
605
  return removed;
637
606
  }
638
607
  #pattern(query, options) {
639
- const source = options?.regex === true ? query : escapeRegExp(query);
608
+ const source = options?.regex === true ? query : (0, _orkestrel_contract.escapeRegExp)(query);
640
609
  const flags = options?.sensitive === false ? "gi" : "g";
641
610
  try {
642
611
  return new RegExp(source, flags);
@@ -645,8 +614,7 @@ var Workspace = class {
645
614
  }
646
615
  }
647
616
  };
648
- //#endregion
649
- //#region src/core/workspaces/WorkspaceManager.ts
617
+ // src/core/workspaces/WorkspaceManager.ts
650
618
  /**
651
619
  * Implements `WorkspaceManagerInterface` over an insertion-ordered id map and one active id
652
620
  * the instance owns, resolving `active` through that map on every read and holding no emitter.
@@ -733,11 +701,7 @@ var WorkspaceManager = class {
733
701
  return true;
734
702
  }
735
703
  remove(ids) {
736
- if ((0, _orkestrel_contract.isArray)(ids)) {
737
- let removed = true;
738
- for (const id of ids) if (!this.#drop(id)) removed = false;
739
- return removed;
740
- }
704
+ if ((0, _orkestrel_contract.isArray)(ids)) return Iterator.from(ids).reduce((removed, id) => this.#drop(id) && removed, true);
741
705
  return this.#drop(ids);
742
706
  }
743
707
  clear() {
@@ -754,8 +718,7 @@ var WorkspaceManager = class {
754
718
  return true;
755
719
  }
756
720
  };
757
- //#endregion
758
- //#region src/core/workspaces/stores/DatabaseWorkspaceStore.ts
721
+ // src/core/workspaces/stores/DatabaseWorkspaceStore.ts
759
722
  /**
760
723
  * Persists workspace snapshots in a database table.
761
724
  *
@@ -799,8 +762,7 @@ var DatabaseWorkspaceStore = class {
799
762
  await this.#table.remove(id);
800
763
  }
801
764
  };
802
- //#endregion
803
- //#region src/core/workspaces/stores/MemoryWorkspaceStore.ts
765
+ // src/core/workspaces/stores/MemoryWorkspaceStore.ts
804
766
  /**
805
767
  * Holds workspace snapshots in the current process.
806
768
  *
@@ -825,8 +787,7 @@ var MemoryWorkspaceStore = class {
825
787
  return Promise.resolve();
826
788
  }
827
789
  };
828
- //#endregion
829
- //#region src/core/factories.ts
790
+ // src/core/factories.ts
830
791
  /**
831
792
  * Creates an immutable file with derived size and line counts.
832
793
  *
@@ -843,7 +804,7 @@ var MemoryWorkspaceStore = class {
843
804
  * createTextContent,
844
805
  * inferLanguage,
845
806
  * isBinary,
846
- * isText,
807
+ * isTextContent,
847
808
  * } from '@orkestrel/workspace'
848
809
  *
849
810
  * const note = createFile({
@@ -854,7 +815,7 @@ var MemoryWorkspaceStore = class {
854
815
  * note.size // 12 — UTF-8 bytes, through computeSize
855
816
  * note.lines // 2 — through countLines
856
817
  * note.state // 'created'
857
- * isText(note.content) // true
818
+ * isTextContent(note.content) // true
858
819
  *
859
820
  * const icon = createFile({ path: 'icon.png', content: createBinaryContent('AAAA', 'image/png') })
860
821
  * isBinary(icon.content) // true
@@ -985,14 +946,13 @@ function createDatabaseWorkspaceStore(driver = (0, _orkestrel_database.createMem
985
946
  function createWorkspaceManager(options) {
986
947
  return new WorkspaceManager(options);
987
948
  }
988
- //#endregion
989
949
  exports.DatabaseWorkspaceStore = DatabaseWorkspaceStore;
990
950
  exports.EXTENSION_LANGUAGES = EXTENSION_LANGUAGES;
991
951
  exports.MemoryWorkspaceStore = MemoryWorkspaceStore;
992
952
  exports.Workspace = Workspace;
993
953
  exports.WorkspaceError = WorkspaceError;
994
954
  exports.WorkspaceManager = WorkspaceManager;
995
- exports.clampPosition = clampPosition;
955
+ exports.clampCaret = clampCaret;
996
956
  exports.clampRange = clampRange;
997
957
  exports.computeDecodedSize = computeDecodedSize;
998
958
  exports.computeSize = computeSize;
@@ -1004,11 +964,10 @@ exports.createMemoryWorkspaceStore = createMemoryWorkspaceStore;
1004
964
  exports.createTextContent = createTextContent;
1005
965
  exports.createWorkspace = createWorkspace;
1006
966
  exports.createWorkspaceManager = createWorkspaceManager;
1007
- exports.escapeRegExp = escapeRegExp;
1008
967
  exports.inferLanguage = inferLanguage;
1009
968
  exports.isBinary = isBinary;
1010
969
  exports.isFile = isFile;
1011
- exports.isText = isText;
970
+ exports.isTextContent = isTextContent;
1012
971
  exports.isValidRange = isValidRange;
1013
972
  exports.isWorkspaceError = isWorkspaceError;
1014
973
  exports.isWorkspaceSnapshot = isWorkspaceSnapshot;