@danypops/pi-lector 0.13.7 → 0.13.9

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.
@@ -9,7 +9,14 @@ import type { EditorTheme } from "./editor-theme.ts";
9
9
  import type { ExplorerDiff, ExplorerEntry } from "./explorer-diff.ts";
10
10
  import { diffExplorerLines, formatExplorerLine, parseExplorerLine } from "./explorer-diff.ts";
11
11
 
12
- export type ExplorerResult = { kind: "quit" } | { kind: "open-file"; absolutePath: string };
12
+ /** Restores one Workspace's directory and active entry when its Explorer becomes visible again. */
13
+ export interface ExplorerViewState {
14
+ readonly relativePath: string;
15
+ readonly selectedEntryName?: string;
16
+ }
17
+
18
+ /** Reports why the Explorer closed plus its latest restorable location when the host supports view-state persistence. */
19
+ export type ExplorerResult = { kind: "quit"; viewState?: ExplorerViewState } | { kind: "open-file"; absolutePath: string; viewState?: ExplorerViewState };
13
20
 
14
21
  /** Joins a directory-relative name onto `directory` ("" means the resolved root itself). */
15
22
  export function joinExplorerPath(directory: string, name: string): string {
@@ -51,12 +58,19 @@ export class ExplorerComponent implements Component {
51
58
  private statusMessage = "";
52
59
  private confirming: PendingConfirmation | undefined;
53
60
 
54
- constructor(tui: TUI, theme: EditorTheme, session: DirectoryExplorerSession, initialRelativePath: string, done: (result: ExplorerResult) => void) {
61
+ constructor(
62
+ tui: TUI,
63
+ theme: EditorTheme,
64
+ session: DirectoryExplorerSession,
65
+ initialRelativePath: string,
66
+ done: (result: ExplorerResult) => void,
67
+ initialSelectedEntryName?: string,
68
+ ) {
55
69
  this.tui = tui;
56
70
  this.theme = theme;
57
71
  this.session = session;
58
72
  this.done = done;
59
- this.pending = this.loadDirectory(initialRelativePath).catch((error: unknown) => this.reportError(error));
73
+ this.pending = this.loadDirectory(initialRelativePath, initialSelectedEntryName).catch((error: unknown) => this.reportError(error));
60
74
  }
61
75
 
62
76
  private pending: Promise<void> = Promise.resolve();
@@ -111,13 +125,20 @@ export class ExplorerComponent implements Component {
111
125
  this.tui.requestRender();
112
126
  }
113
127
 
114
- private async loadDirectory(relativePath: string): Promise<void> {
128
+ private async loadDirectory(relativePath: string, selectedEntryName?: string): Promise<void> {
115
129
  const listing = await this.session.listDirectory(relativePath);
116
130
  this.currentPath = relativePath;
117
131
  this.nextId = 1;
118
132
  this.entries = listing.entries.map((entry) => ({ id: this.nextId++, name: entry.name, kind: entry.kind }));
119
133
  const text = this.entries.length > 0 ? this.entries.map((entry) => formatExplorerLine(entry)).join("\n") : "";
120
134
  this.state = new EditorState(text);
135
+ const selectedIndex = selectedEntryName === undefined ? -1 : this.entries.findIndex((entry) => entry.name === selectedEntryName);
136
+ const selectedEntry = selectedIndex >= 0 ? this.entries[selectedIndex] : undefined;
137
+ if (selectedEntry) {
138
+ this.state.cursorLine = selectedIndex + 1;
139
+ this.state.cursorCharacter = `${selectedEntry.id} `.length + 1;
140
+ this.scrollToKeepCursorVisible();
141
+ }
121
142
  this.confirming = undefined;
122
143
  this.statusMessage = "";
123
144
  this.tui.requestRender();
@@ -133,12 +154,17 @@ export class ExplorerComponent implements Component {
133
154
  await this.loadDirectory(joinExplorerPath(this.currentPath, entry.name));
134
155
  return;
135
156
  }
136
- this.done({ kind: "open-file", absolutePath: join(this.session.root, joinExplorerPath(this.currentPath, entry.name)) });
157
+ this.done({
158
+ kind: "open-file",
159
+ absolutePath: join(this.session.root, joinExplorerPath(this.currentPath, entry.name)),
160
+ viewState: { relativePath: this.currentPath, selectedEntryName: entry.name },
161
+ });
137
162
  }
138
163
 
139
164
  private async navigateToParent(): Promise<void> {
140
165
  if (this.currentPath === "") return; // already at the resolved root -- v1 never widens scope above it
141
- await this.loadDirectory(parentExplorerPath(this.currentPath));
166
+ const childName = posix.basename(this.currentPath);
167
+ await this.loadDirectory(parentExplorerPath(this.currentPath), childName);
142
168
  }
143
169
 
144
170
  private async performAction(action: EditorAction): Promise<void> {
@@ -149,14 +175,14 @@ export class ExplorerComponent implements Component {
149
175
  this.state.dirty = false;
150
176
  if (diffs.length === 0) {
151
177
  this.statusMessage = "no changes";
152
- if (action.kind === "save-and-quit") this.done({ kind: "quit" });
178
+ if (action.kind === "save-and-quit") this.done({ kind: "quit", viewState: this.viewState() });
153
179
  return;
154
180
  }
155
181
  this.confirming = { diffs, andQuit: action.kind === "save-and-quit" };
156
182
  return;
157
183
  }
158
184
  case "quit":
159
- this.done({ kind: "quit" });
185
+ this.done({ kind: "quit", viewState: this.viewState() });
160
186
  return;
161
187
  case "hover":
162
188
  this.statusMessage = "hover is not applicable in the file explorer";
@@ -186,13 +212,19 @@ export class ExplorerComponent implements Component {
186
212
  return;
187
213
  }
188
214
  if (andQuit) {
189
- this.done({ kind: "quit" });
215
+ this.done({ kind: "quit", viewState: this.viewState() });
190
216
  return;
191
217
  }
192
218
  await this.loadDirectory(this.currentPath);
193
219
  this.statusMessage = "applied";
194
220
  }
195
221
 
222
+ private viewState(): ExplorerViewState {
223
+ const parsed = parseExplorerLine(this.state.currentLineText);
224
+ const entry = !parsed || parsed.id === null ? undefined : this.entries.find((candidate) => candidate.id === parsed.id);
225
+ return entry ? { relativePath: this.currentPath, selectedEntryName: entry.name } : { relativePath: this.currentPath };
226
+ }
227
+
196
228
  private scrollToKeepCursorVisible(): void {
197
229
  const viewportHeight = Math.max(1, this.tui.terminal.rows - 2);
198
230
  if (this.state.cursorLine < this.scrollTop) this.scrollTop = this.state.cursorLine;
@@ -1,10 +1,10 @@
1
- import { dirname, relative } from "node:path";
1
+ import { basename, dirname, relative } from "node:path";
2
2
  import type { DirectoryExplorerSession } from "./directory-explorer-operations.ts";
3
- import type { ExplorerResult } from "./explorer-component.ts";
3
+ import type { ExplorerResult, ExplorerViewState } from "./explorer-component.ts";
4
4
 
5
5
  export interface ExplorerFlowHost {
6
- /** Shows the explorer at `relativePath` (root-relative, "" for the resolved root) and resolves once the user quits or opens a file. */
7
- showExplorer(session: DirectoryExplorerSession, relativePath: string): Promise<ExplorerResult>;
6
+ /** Shows the explorer at `relativePath` (root-relative, "" for the resolved root), optionally revealing one active entry, and resolves once the user quits or opens a file. */
7
+ showExplorer(session: DirectoryExplorerSession, relativePath: string, selectedEntryName?: string): Promise<ExplorerResult>;
8
8
  /** Shows the real file editor for `absolutePath` and resolves once the user quits it. */
9
9
  showEditor(absolutePath: string): Promise<void>;
10
10
  }
@@ -14,14 +14,21 @@ export interface ExplorerFlowHost {
14
14
  * the explorer -- at the directory the opened file lives in, not the resolved root -- once that
15
15
  * editor quits, rather than closing the whole session after the first file opened.
16
16
  */
17
- export async function runExplorerFlow(session: DirectoryExplorerSession, host: ExplorerFlowHost): Promise<void> {
18
- let relativePath = "";
17
+ export async function runExplorerFlow(
18
+ session: DirectoryExplorerSession,
19
+ host: ExplorerFlowHost,
20
+ initialViewState: ExplorerViewState = { relativePath: "" },
21
+ ): Promise<ExplorerViewState> {
22
+ let viewState = initialViewState;
19
23
  for (;;) {
20
- const result = await host.showExplorer(session, relativePath);
21
- if (result.kind === "quit") return;
24
+ const result = await host.showExplorer(session, viewState.relativePath, viewState.selectedEntryName);
25
+ if (result.kind === "quit") return result.viewState ?? viewState;
22
26
 
23
27
  await host.showEditor(result.absolutePath);
24
28
  // path.relative(root, root) is "" directly, matching ExplorerComponent's own root-relative convention -- no "." case to normalize.
25
- relativePath = relative(session.root, dirname(result.absolutePath));
29
+ viewState = result.viewState ?? {
30
+ relativePath: relative(session.root, dirname(result.absolutePath)),
31
+ selectedEntryName: basename(result.absolutePath),
32
+ };
26
33
  }
27
34
  }
@@ -31,7 +31,7 @@
31
31
  export type { DirectoryExplorerSession } from "./directory-explorer-operations.ts";
32
32
  export { type EditorAction, type EditorMode, EditorState } from "./editor-state.ts";
33
33
  export type { EditorTheme } from "./editor-theme.ts";
34
- export { ExplorerComponent, type ExplorerResult, joinExplorerPath } from "./explorer-component.ts";
34
+ export { ExplorerComponent, type ExplorerResult, type ExplorerViewState, joinExplorerPath } from "./explorer-component.ts";
35
35
  // runExplorerFlow is pure orchestration over the two interfaces above (browse, open a file into
36
36
  // the real editor, return to the explorer at that file's own directory once it quits) -- exported
37
37
  // for the same reason ExplorerComponent/DirectoryExplorerSession are: any real host can drive this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-lector",
3
- "version": "0.13.7",
3
+ "version": "0.13.9",
4
4
  "description": "Pi host adapter for Lector: overrides read/write/edit with a daemon-backed, hash-guarded filesystem",
5
5
  "license": "MIT",
6
6
  "type": "module",