@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
|
-
|
|
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(
|
|
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({
|
|
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
|
-
|
|
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(
|
|
18
|
-
|
|
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
|
-
|
|
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