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