blots-editor 0.0.14 → 0.0.16

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 (43) hide show
  1. package/asset/language/en.json +4 -0
  2. package/asset/language/zh.json +4 -0
  3. package/dist/core/document/document.d.ts +41 -2
  4. package/dist/core/document/document.js +113 -36
  5. package/dist/core/{file-tree-model.d.ts → io/file-tree-model.d.ts} +12 -3
  6. package/dist/core/{file-tree-model.js → io/file-tree-model.js} +23 -12
  7. package/dist/core/{session.d.ts → io/session.d.ts} +1 -1
  8. package/dist/core/{session.js → io/session.js} +1 -1
  9. package/dist/index.js +4 -4
  10. package/dist/{view → utils/view}/click-mapping.d.ts +1 -1
  11. package/dist/{view → utils/view}/frame-clamp.d.ts +4 -0
  12. package/dist/{view → utils/view}/frame-clamp.js +8 -0
  13. package/dist/{i18n-resources.js → utils/view/i18n-resources.js} +1 -1
  14. package/dist/view/{command-bar.d.ts → editor/command-bar.d.ts} +1 -1
  15. package/dist/view/{editor-setting.js → editor/editor-setting.js} +2 -2
  16. package/dist/view/{file-tree.d.ts → editor/file-tree.d.ts} +5 -1
  17. package/dist/view/{file-tree.js → editor/file-tree.js} +46 -28
  18. package/dist/view/{information-bar.d.ts → editor/information-bar.d.ts} +1 -1
  19. package/dist/view/editor/tool-bar.d.ts +36 -0
  20. package/dist/view/editor/tool-bar.js +134 -0
  21. package/dist/view/{toolbar-store.d.ts → event/subscription/toolbar-store.d.ts} +5 -6
  22. package/dist/view/event/subscription/tree-store.d.ts +10 -0
  23. package/dist/view/{tree-width-store.js → event/subscription/tree-store.js} +9 -12
  24. package/dist/view/{editor.js → page/editor.js} +12 -12
  25. package/dist/view/{main-menu.js → page/main-menu.js} +1 -1
  26. package/dist/view/{settings.js → page/settings.js} +3 -3
  27. package/dist/view/{modal-frame.js → utils/modal-frame.js} +1 -1
  28. package/package.json +1 -1
  29. package/dist/view/tool-bar.d.ts +0 -62
  30. package/dist/view/tool-bar.js +0 -148
  31. package/dist/view/tree-width-store.d.ts +0 -6
  32. /package/dist/{view → utils/view}/click-mapping.js +0 -0
  33. /package/dist/{i18n-resources.d.ts → utils/view/i18n-resources.d.ts} +0 -0
  34. /package/dist/view/{command-bar.js → editor/command-bar.js} +0 -0
  35. /package/dist/view/{editor-setting.d.ts → editor/editor-setting.d.ts} +0 -0
  36. /package/dist/view/{information-bar.js → editor/information-bar.js} +0 -0
  37. /package/dist/view/{toolbar-store.js → event/subscription/toolbar-store.js} +0 -0
  38. /package/dist/view/{editor.d.ts → page/editor.d.ts} +0 -0
  39. /package/dist/view/{main-menu.d.ts → page/main-menu.d.ts} +0 -0
  40. /package/dist/view/{settings.d.ts → page/settings.d.ts} +0 -0
  41. /package/dist/view/{modal-frame.d.ts → utils/modal-frame.d.ts} +0 -0
  42. /package/dist/view/{sensitivity-bar.d.ts → utils/sensitivity-bar.d.ts} +0 -0
  43. /package/dist/view/{sensitivity-bar.js → utils/sensitivity-bar.js} +0 -0
@@ -19,6 +19,10 @@
19
19
  "toolbar.fileTree": "File Tree",
20
20
  "toolbar.save": "Save",
21
21
  "toolbar.settings": "Settings",
22
+ "fileTree.scanning": "Scanning...",
23
+ "fileTree.noDir": "No directory",
24
+ "fileTree.fail": "Scan failed",
25
+ "fileTree.unsaved": "Unsaved changes",
22
26
  "editor.setting.title": "Menu",
23
27
  "editor.setting.openSetting": "Settings",
24
28
  "editor.setting.exit": "Exit",
@@ -19,6 +19,10 @@
19
19
  "toolbar.fileTree": "文件树",
20
20
  "toolbar.save": "保存",
21
21
  "toolbar.settings": "设置",
22
+ "fileTree.scanning": "正在扫描...",
23
+ "fileTree.noDir": "没有目录",
24
+ "fileTree.fail": "扫描失败",
25
+ "fileTree.unsaved": "未保存的更改",
22
26
  "editor.setting.title": "菜单",
23
27
  "editor.setting.openSetting": "设置",
24
28
  "editor.setting.exit": "退出",
@@ -20,6 +20,23 @@ export declare class Document {
20
20
  private _wrapWidth;
21
21
  private _viewScrollTop;
22
22
  private _scrollRemainder;
23
+ /**
24
+ * Per-line soft-wrap segment cache. Segments are derived purely from
25
+ * `_lines` + `_wrapWidth`, but recomputing them is expensive (binary
26
+ * searches + per-character tab expansion), and the render path queries
27
+ * them O(viewport) times per frame — so identical inputs must not be
28
+ * recomputed. null while invalidated; rebuilt lazily on first use.
29
+ */
30
+ private _segmentCache;
31
+ /**
32
+ * Prefix sums of segment counts: `prefix[i]` = visual lines occupied by
33
+ * the first `i` logical lines (so `prefix[line]` is the global visual
34
+ * line where logical line `line` starts). Lets `visualLineAt` /
35
+ * `cursorVisualLine` / `visualLineCount` answer in O(log n)/O(1)
36
+ * instead of scanning every line. Invalidated together with
37
+ * `_segmentCache`.
38
+ */
39
+ private _segPrefix;
23
40
  readonly indentWidth: number;
24
41
  constructor(text: string, options?: DocumentOptions);
25
42
  get lineCount(): number;
@@ -45,14 +62,36 @@ export declare class Document {
45
62
  get wrapWidth(): number;
46
63
  /** Set the soft-wrap width (clamped to at least 1 cell). */
47
64
  setWrapWidth(width: number): void;
48
- /** The soft-wrap segments of one logical line (an empty line is one empty segment). */
65
+ /**
66
+ * The soft-wrap segments of one logical line (an empty line is one empty
67
+ * segment). Cached: the render path queries these O(viewport) times per
68
+ * frame, and every hit would otherwise re-run `segmentFrom` (binary
69
+ * search + per-character tab expansion) on unchanged input.
70
+ */
49
71
  private lineSegments;
72
+ /**
73
+ * Prefix sums of segment counts, built lazily from the segment cache.
74
+ * Both caches are invalidated together (see {@link invalidateIndex}), so
75
+ * they can never disagree once built.
76
+ */
77
+ private buildSegmentPrefix;
78
+ /**
79
+ * Drop the segment cache and the visual-line index. Every mutation entry
80
+ * point (`setText`, `setLine`, `insertLineAt`, `removeLineAt`, and a
81
+ * *changed* `setWrapWidth`) must call this, or queries would keep serving
82
+ * stale segments. Rebuilding is lazy — the next query pays O(total
83
+ * segments) once.
84
+ */
85
+ private invalidateIndex;
50
86
  /** Total number of visual (soft-wrapped) lines across the document. */
51
87
  get visualLineCount(): number;
52
88
  /**
53
89
  * Map a global visual line to its logical line and the segment's logical
54
90
  * span. `first` marks the segment that starts at the logical line start
55
- * (the one that carries the line number).
91
+ * (the one that carries the line number). Binary search over the segment
92
+ * prefix sums keeps this O(log n) — the render loop calls it once per
93
+ * visible line, so a linear scan here would be O(viewport × lines) every
94
+ * frame.
56
95
  */
57
96
  visualLineAt(vline: number): {
58
97
  line: number;
@@ -20,6 +20,23 @@ export class Document {
20
20
  _viewScrollTop = null;
21
21
  // Fractional view-scroll remainder, so e.g. 3.5 cells/notch scrolls 3,4,3,4…
22
22
  _scrollRemainder = 0;
23
+ /**
24
+ * Per-line soft-wrap segment cache. Segments are derived purely from
25
+ * `_lines` + `_wrapWidth`, but recomputing them is expensive (binary
26
+ * searches + per-character tab expansion), and the render path queries
27
+ * them O(viewport) times per frame — so identical inputs must not be
28
+ * recomputed. null while invalidated; rebuilt lazily on first use.
29
+ */
30
+ _segmentCache = null;
31
+ /**
32
+ * Prefix sums of segment counts: `prefix[i]` = visual lines occupied by
33
+ * the first `i` logical lines (so `prefix[line]` is the global visual
34
+ * line where logical line `line` starts). Lets `visualLineAt` /
35
+ * `cursorVisualLine` / `visualLineCount` answer in O(log n)/O(1)
36
+ * instead of scanning every line. Invalidated together with
37
+ * `_segmentCache`.
38
+ */
39
+ _segPrefix = null;
23
40
  indentWidth;
24
41
  constructor(text, options = {}) {
25
42
  this._lines = text.split(/\r?\n/).map((t) => new TextLine(t));
@@ -56,6 +73,7 @@ export class Document {
56
73
  this._scrollTop = 0;
57
74
  this._viewScrollTop = null;
58
75
  this._scrollRemainder = 0;
76
+ this.invalidateIndex();
59
77
  }
60
78
  get cursor() {
61
79
  const line = this._lines[this._cursor.line];
@@ -74,68 +92,123 @@ export class Document {
74
92
  }
75
93
  /** Set the soft-wrap width (clamped to at least 1 cell). */
76
94
  setWrapWidth(width) {
77
- this._wrapWidth = Math.max(1, width);
95
+ const w = Math.max(1, width);
96
+ // The view calls this on every render with the measured width; only a
97
+ // real change invalidates the cache, or it would be rebuilt (and
98
+ // discarded) every frame.
99
+ if (w !== this._wrapWidth) {
100
+ this._wrapWidth = w;
101
+ this.invalidateIndex();
102
+ }
78
103
  }
79
- /** The soft-wrap segments of one logical line (an empty line is one empty segment). */
104
+ /**
105
+ * The soft-wrap segments of one logical line (an empty line is one empty
106
+ * segment). Cached: the render path queries these O(viewport) times per
107
+ * frame, and every hit would otherwise re-run `segmentFrom` (binary
108
+ * search + per-character tab expansion) on unchanged input.
109
+ */
80
110
  lineSegments(line) {
111
+ const cached = this._segmentCache?.[line];
112
+ if (cached) {
113
+ return cached;
114
+ }
81
115
  const tl = this._lines[line];
116
+ const segs = [];
82
117
  if (tl.text.length === 0) {
83
- return [{ start: 0, end: 0, text: "" }];
118
+ segs.push({ start: 0, end: 0, text: "" });
84
119
  }
85
- const segs = [];
86
- let segVisual = 0;
87
- let segStart = 0;
88
- while (segStart < tl.text.length) {
89
- const seg = tl.segmentFrom(segVisual, this._wrapWidth);
90
- segs.push({ start: segStart, end: seg.endLogical, text: seg.text });
91
- if (seg.text === "") {
92
- break;
120
+ else {
121
+ let segVisual = 0;
122
+ let segStart = 0;
123
+ while (segStart < tl.text.length) {
124
+ const seg = tl.segmentFrom(segVisual, this._wrapWidth);
125
+ segs.push({ start: segStart, end: seg.endLogical, text: seg.text });
126
+ if (seg.text === "") {
127
+ break;
128
+ }
129
+ segStart = seg.endLogical;
130
+ segVisual = seg.endVisual;
93
131
  }
94
- segStart = seg.endLogical;
95
- segVisual = seg.endVisual;
96
132
  }
133
+ if (!this._segmentCache) {
134
+ this._segmentCache = [];
135
+ }
136
+ this._segmentCache[line] = segs;
97
137
  return segs;
98
138
  }
99
- /** Total number of visual (soft-wrapped) lines across the document. */
100
- get visualLineCount() {
139
+ /**
140
+ * Prefix sums of segment counts, built lazily from the segment cache.
141
+ * Both caches are invalidated together (see {@link invalidateIndex}), so
142
+ * they can never disagree once built.
143
+ */
144
+ buildSegmentPrefix() {
145
+ if (this._segPrefix) {
146
+ return this._segPrefix;
147
+ }
148
+ const prefix = [0];
101
149
  let total = 0;
102
150
  for (let i = 0; i < this._lines.length; i++) {
103
151
  total += this.lineSegments(i).length;
152
+ prefix.push(total);
104
153
  }
105
- return total;
154
+ this._segPrefix = prefix;
155
+ return prefix;
156
+ }
157
+ /**
158
+ * Drop the segment cache and the visual-line index. Every mutation entry
159
+ * point (`setText`, `setLine`, `insertLineAt`, `removeLineAt`, and a
160
+ * *changed* `setWrapWidth`) must call this, or queries would keep serving
161
+ * stale segments. Rebuilding is lazy — the next query pays O(total
162
+ * segments) once.
163
+ */
164
+ invalidateIndex() {
165
+ this._segmentCache = null;
166
+ this._segPrefix = null;
167
+ }
168
+ /** Total number of visual (soft-wrapped) lines across the document. */
169
+ get visualLineCount() {
170
+ return this.buildSegmentPrefix()[this._lines.length];
106
171
  }
107
172
  /**
108
173
  * Map a global visual line to its logical line and the segment's logical
109
174
  * span. `first` marks the segment that starts at the logical line start
110
- * (the one that carries the line number).
175
+ * (the one that carries the line number). Binary search over the segment
176
+ * prefix sums keeps this O(log n) — the render loop calls it once per
177
+ * visible line, so a linear scan here would be O(viewport × lines) every
178
+ * frame.
111
179
  */
112
180
  visualLineAt(vline) {
113
181
  if (vline < 0) {
114
182
  return null;
115
183
  }
116
- let remaining = vline;
117
- for (let line = 0; line < this._lines.length; line++) {
118
- const segs = this.lineSegments(line);
119
- if (remaining < segs.length) {
120
- return {
121
- line,
122
- start: segs[remaining].start,
123
- end: segs[remaining].end,
124
- first: remaining === 0,
125
- text: segs[remaining].text,
126
- };
184
+ const prefix = this.buildSegmentPrefix();
185
+ if (vline >= prefix[this._lines.length]) {
186
+ return null;
187
+ }
188
+ // Largest line index whose prefix start is <= vline.
189
+ let lo = 0;
190
+ let hi = this._lines.length - 1;
191
+ while (lo < hi) {
192
+ const mid = (lo + hi + 1) >> 1;
193
+ if (prefix[mid] <= vline) {
194
+ lo = mid;
195
+ }
196
+ else {
197
+ hi = mid - 1;
127
198
  }
128
- remaining -= segs.length;
129
199
  }
130
- return null;
200
+ const seg = this.lineSegments(lo)[vline - prefix[lo]];
201
+ return {
202
+ line: lo,
203
+ start: seg.start,
204
+ end: seg.end,
205
+ first: vline === prefix[lo],
206
+ text: seg.text,
207
+ };
131
208
  }
132
209
  /** Global visual line the cursor sits on. */
133
210
  get cursorVisualLine() {
134
211
  const { line, logical } = this._cursor;
135
- let v = 0;
136
- for (let i = 0; i < line; i++) {
137
- v += this.lineSegments(i).length;
138
- }
139
212
  const segs = this.lineSegments(line);
140
213
  let idx = 0;
141
214
  // A cursor exactly on a segment boundary belongs to the segment
@@ -144,7 +217,7 @@ export class Document {
144
217
  while (idx < segs.length - 1 && logical >= segs[idx + 1].start) {
145
218
  idx++;
146
219
  }
147
- return v + idx;
220
+ return this.buildSegmentPrefix()[line] + idx;
148
221
  }
149
222
  /** Visual offset of the cursor within its current soft-wrap segment. */
150
223
  get cursorSegmentVisual() {
@@ -175,14 +248,18 @@ export class Document {
175
248
  }
176
249
  setLine(i, text) {
177
250
  this._lines[i] = new TextLine(text);
251
+ this.invalidateIndex();
178
252
  }
179
253
  /** Insert a line at index `i` (0 <= i <= lineCount), shifting the rest down. */
180
254
  insertLineAt(i, text) {
181
255
  this._lines.splice(i, 0, new TextLine(text));
256
+ this.invalidateIndex();
182
257
  }
183
258
  /** Remove line `i` and return its text. */
184
259
  removeLineAt(i) {
185
- return this._lines.splice(i, 1)[0].text;
260
+ const removed = this._lines.splice(i, 1)[0].text;
261
+ this.invalidateIndex();
262
+ return removed;
186
263
  }
187
264
  /** Set the cursor, clamping out-of-range values (line to `[0, lineCount-1]`, column to end of line). */
188
265
  setCursor(line, logical) {
@@ -1,8 +1,17 @@
1
- import type { FileTreeSettings } from "./settings/schema.js";
1
+ import type { FileTreeSettings } from "../settings/schema.js";
2
2
  /** Clear the scan cache (refresh button, tests). */
3
3
  export declare function clearFileTreeCache(): void;
4
+ export type FileTreeState = {
5
+ ok: true;
6
+ node: FileNode;
7
+ path: string;
8
+ } | {
9
+ fail: true;
10
+ } | {
11
+ scanning: true;
12
+ };
4
13
  /** Scan a directory, serving repeat calls for the same root from the cache. */
5
- export declare function scanDirectoryCached(root: string): FileNode | null;
14
+ export declare function scanDirectoryCached(root: string): Promise<FileTreeState>;
6
15
  /** One entry of the scanned tree. Directories carry their children. */
7
16
  export type FileNode = {
8
17
  /** Entry name (not the full path). */
@@ -24,7 +33,7 @@ export declare function resolveFileTreeRoot(settings: FileTreeSettings, startupD
24
33
  *
25
34
  * @returns The root node, or null when the directory cannot be read.
26
35
  */
27
- export declare function scanDirectory(root: string): FileNode | null;
36
+ export declare function scanDirectory(root: string): Promise<FileNode | null>;
28
37
  /** One visible tree row produced by flattening with expansion state. */
29
38
  export type TreeRow = {
30
39
  node: FileNode;
@@ -1,4 +1,4 @@
1
- import { readdirSync } from "node:fs";
1
+ import fs from 'node:fs/promises';
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  /**
@@ -13,16 +13,27 @@ export function clearFileTreeCache() {
13
13
  scanCache.clear();
14
14
  }
15
15
  /** Scan a directory, serving repeat calls for the same root from the cache. */
16
- export function scanDirectoryCached(root) {
16
+ export async function scanDirectoryCached(root) {
17
17
  const hit = scanCache.get(root);
18
18
  if (hit) {
19
- return hit;
19
+ return {
20
+ ok: true,
21
+ node: hit,
22
+ path: root
23
+ };
20
24
  }
21
- const tree = scanDirectory(root);
25
+ const tree = await scanDirectory(root);
22
26
  if (tree) {
23
27
  scanCache.set(root, tree);
28
+ return {
29
+ ok: true,
30
+ node: tree,
31
+ path: root
32
+ };
24
33
  }
25
- return tree;
34
+ return {
35
+ fail: true
36
+ };
26
37
  }
27
38
  /** Directories skipped at every depth (dependency dirs, VCS internals). */
28
39
  const SKIP_DIRS = new Set(["node_modules", ".git", "dist", ".next", ".cache"]);
@@ -47,8 +58,8 @@ export function resolveFileTreeRoot(settings, startupDir) {
47
58
  *
48
59
  * @returns The root node, or null when the directory cannot be read.
49
60
  */
50
- export function scanDirectory(root) {
51
- const entries = readDirSafe(root);
61
+ export async function scanDirectory(root) {
62
+ const entries = await readDirSafe(root);
52
63
  if (!entries) {
53
64
  return null;
54
65
  }
@@ -63,7 +74,7 @@ export function scanDirectory(root) {
63
74
  if (SKIP_DIRS.has(name)) {
64
75
  continue;
65
76
  }
66
- nodes.push({ name, path, isDir: true, children: scanChildren(path) });
77
+ nodes.push({ name, path, isDir: true, children: await scanChildren(path) });
67
78
  }
68
79
  else if (entry.isFile()) {
69
80
  nodes.push({ name, path, isDir: false });
@@ -74,14 +85,14 @@ export function scanDirectory(root) {
74
85
  nodes.sort((a, b) => a.isDir === b.isDir ? (a.name < b.name ? -1 : a.name > b.name ? 1 : 0) : a.isDir ? -1 : 1);
75
86
  return { name: root, path: root, isDir: true, children: nodes };
76
87
  }
77
- function scanChildren(dir) {
78
- const child = scanDirectory(dir);
88
+ async function scanChildren(dir) {
89
+ const child = await scanDirectory(dir);
79
90
  return child?.children;
80
91
  }
81
92
  /** Read a directory without throwing; null when unreadable (EACCES etc.). */
82
- function readDirSafe(path) {
93
+ async function readDirSafe(path) {
83
94
  try {
84
- return readdirSync(path, { withFileTypes: true });
95
+ return await fs.readdir(path, { withFileTypes: true });
85
96
  }
86
97
  catch {
87
98
  return null;
@@ -1,4 +1,4 @@
1
- import { EditorController, type EditorOptions } from "./editor-controller.js";
1
+ import { EditorController, type EditorOptions } from "../editor-controller.js";
2
2
  export type ActionResult = {
3
3
  ok: true;
4
4
  } | {
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFileSync, writeFileSync } from "node:fs";
3
3
  import { basename } from "node:path";
4
- import { EditorController } from "./editor-controller.js";
4
+ import { EditorController } from "../editor-controller.js";
5
5
  /**
6
6
  * File-session coordinator shared by the editor, file tree, and toolbar.
7
7
  *
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@
2
2
  import { render } from "ink";
3
3
  import React from "react";
4
4
  import { CurrentScreen, KeyboardProvider, LanguageProvider, registerComponent, ScenarioManagementProvider, } from "ink-cartridge";
5
- import { resources } from "./i18n-resources.js";
5
+ import { resources } from "./utils/view/i18n-resources.js";
6
6
  import { settingsStore } from "./core/settings/useSettings.js";
7
- import { MainMenu } from "./view/main-menu.js";
8
- import { Editor } from "./view/editor.js";
9
- import { Settings } from "./view/settings.js";
7
+ import { MainMenu } from "./view/page/main-menu.js";
8
+ import { Editor } from "./view/page/editor.js";
9
+ import { Settings } from "./view/page/settings.js";
10
10
  registerComponent(MainMenu, {});
11
11
  registerComponent(Editor, {}, { parent: MainMenu });
12
12
  registerComponent(Settings, {}, { parent: MainMenu });
@@ -1,4 +1,4 @@
1
- import type { Document } from "../core/document/document.js";
1
+ import type { Document } from "../../core/document/document.js";
2
2
  export type ClickTarget = {
3
3
  line: number;
4
4
  logical: number;
@@ -8,3 +8,7 @@ export declare function clampFrame(left: number, top: number, columns: number, r
8
8
  left: number;
9
9
  top: number;
10
10
  };
11
+ export declare function clampBarPos(left: number, top: number, width: number, height: number, columns: number, rows: number, fileTreeOpen: boolean, treeWidth: number): {
12
+ left: number;
13
+ top: number;
14
+ };
@@ -10,3 +10,11 @@ export function clampFrame(left, top, columns, rows, width, height) {
10
10
  top: Math.min(Math.max(0, top), Math.max(0, rows - height)),
11
11
  };
12
12
  }
13
+ export function clampBarPos(left, top, width, height, columns, rows, fileTreeOpen, treeWidth) {
14
+ const clamped = clampFrame(left, top, columns, rows, width, height);
15
+ const maxLeft = fileTreeOpen ? columns - treeWidth - width : columns - width;
16
+ return {
17
+ left: Math.min(Math.max(0, clamped.left), Math.max(0, maxLeft)),
18
+ top: Math.max(1, clamped.top),
19
+ };
20
+ }
@@ -8,7 +8,7 @@ import { readFileSync } from "node:fs";
8
8
  * global installs work regardless of cwd — the JSON files ship inside the
9
9
  * package via the `asset` entry in `files`.
10
10
  */
11
- const LANGUAGE_DIR = new URL("../asset/language/", import.meta.url);
11
+ const LANGUAGE_DIR = new URL("../../../asset/language/", import.meta.url);
12
12
  function loadPack(locale) {
13
13
  return JSON.parse(readFileSync(new URL(`${locale}.json`, LANGUAGE_DIR), "utf8"));
14
14
  }
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type { EditorSession } from "../core/session.js";
2
+ import type { EditorSession } from "../../core/io/session.js";
3
3
  export type CommandBarProps = {
4
4
  /** File session used by `:save`. */
5
5
  session?: EditorSession | null;
@@ -1,8 +1,8 @@
1
1
  import { Box, Text } from "ink";
2
2
  import { applyElement, back, closeLayer, LayerElementContext, openLayer, useI18n, useKeyboard, useMouseRegion, } from "ink-cartridge";
3
3
  import React, { useCallback, useContext, useEffect, useState } from "react";
4
- import { SettingsEntries } from "./settings.js";
5
- import { ModalFrame } from "./modal-frame.js";
4
+ import { SettingsEntries } from "../page/settings.js";
5
+ import { ModalFrame } from "../utils/modal-frame.js";
6
6
  /** Layer hosting the settings panel, opened on top of the menu layer. */
7
7
  const SETTINGS_PANEL_LAYER_ID = "settings-panel";
8
8
  const SETTINGS_PANEL_Z_INDEX = 3;
@@ -1,9 +1,13 @@
1
1
  import React from "react";
2
- import type { EditorSession } from "../core/session.js";
2
+ import type { FileTreeState } from "../../core/io/file-tree-model.js";
3
+ import type { EditorSession } from "../../core/io/session.js";
3
4
  export type FileTreeProps = {
4
5
  /** The shared file session; clicking a file opens it here. */
5
6
  session: EditorSession;
6
7
  };
8
+ export type FileTreeDirState = FileTreeState | {
9
+ noDir?: boolean;
10
+ };
7
11
  /**
8
12
  * VSCode-style file tree pinned to the right edge of the terminal (regular
9
13
  * layer element). Recursively scans the configured root directory once per