@punica/editor 1.11.2 → 1.13.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@punica/editor",
3
- "version": "1.11.2",
3
+ "version": "1.13.0",
4
4
  "description": "Punica Editor",
5
5
  "private": false,
6
6
  "type": "module",
package/types/index.d.ts CHANGED
@@ -25,6 +25,7 @@
25
25
  /// <reference path="punica.module.extensions.settings.d.ts" />
26
26
  /// <reference path="punica.module.extensions.i18n.d.ts" />
27
27
  /// <reference path="punica.module.runtime.fs.d.ts" />
28
+ /// <reference path="punica.module.runtime.fileOperations.d.ts" />
28
29
  /// <reference path="punica.module.runtime.search.d.ts" />
29
30
  /// <reference path="punica.module.runtime.vcs.d.ts" />
30
31
  /// <reference path="punica.module.runtime.workspace.d.ts" />
@@ -70,6 +70,12 @@ declare module 'punica' {
70
70
  lifecyclePhase?: LifecyclePhase;
71
71
  fs: FileSystemProvider;
72
72
  fileOpeners: FileOpenersApi;
73
+ /**
74
+ * Rename/move and delete, announced to the layers that key state by
75
+ * path (content tabs, editors). Prefer this over calling the
76
+ * equivalent `fs` methods directly.
77
+ */
78
+ fileOperations: FileOperationsApi;
73
79
  search: SearchApi;
74
80
  vcs: VcsApi;
75
81
  terminals: TerminalApi;
@@ -0,0 +1,141 @@
1
+ declare module 'punica' {
2
+ export namespace runtime {
3
+ /**
4
+ * A rename (or move — the same thing at the filesystem level) that is
5
+ * about to happen, or has just happened. Both paths are
6
+ * workspace-relative, the same shape `FileSystemProvider` takes.
7
+ */
8
+ export interface FileRenameEvent {
9
+ oldPath: string;
10
+ newPath: string;
11
+ isDirectory: boolean;
12
+ }
13
+
14
+ /** A delete that is about to happen, or has just happened. */
15
+ export interface FileDeleteEvent {
16
+ path: string;
17
+ isDirectory: boolean;
18
+ }
19
+
20
+ /**
21
+ * A `will*` hook's refusal. Returning this aborts the operation before
22
+ * the filesystem is touched; `reason` becomes the thrown error's message,
23
+ * so it is read by a human and should say what the user can do about it.
24
+ */
25
+ export interface ParticipantVeto {
26
+ veto: true;
27
+ reason?: string;
28
+ }
29
+
30
+ /**
31
+ * A layer that needs to know about — or object to — file operations.
32
+ *
33
+ * This exists because a path is an identity, not just an argument. The
34
+ * shell keys open content tabs by path, the code editor keys its Monaco
35
+ * instances by path, notebook controllers key their models by path. When
36
+ * a file is renamed underneath them they are all holding a key that no
37
+ * longer names anything, and the failure is silent: a save writes to the
38
+ * old name, or finds no editor and writes nothing at all.
39
+ *
40
+ * So the operation cannot just be `fs.renameFile`. It has to be a
41
+ * transaction the interested layers take part in:
42
+ *
43
+ * - `will*` runs BEFORE the filesystem call and may veto (returning
44
+ * `{ veto: true }` or throwing). Use it for "this must not happen
45
+ * yet" — not for cleanup.
46
+ * - `did*` runs AFTER the filesystem call succeeded, awaited and in
47
+ * registration order. Use it to re-key state. A throwing `did*` hook
48
+ * is logged and skipped; it does not fail the operation, because the
49
+ * file has already moved and reporting failure would be a lie.
50
+ *
51
+ * Participants are host/shell territory. A headless boot registers none,
52
+ * and then an operation is exactly the filesystem call plus its event.
53
+ */
54
+ export interface FileOperationParticipant {
55
+ /** Stable id, used in logs and to make double-registration visible. */
56
+ id: string;
57
+ willRename?(
58
+ event: FileRenameEvent
59
+ ): Promise<void | ParticipantVeto> | void | ParticipantVeto;
60
+ didRename?(event: FileRenameEvent): Promise<void> | void;
61
+ willDelete?(
62
+ event: FileDeleteEvent
63
+ ): Promise<void | ParticipantVeto> | void | ParticipantVeto;
64
+ didDelete?(event: FileDeleteEvent): Promise<void> | void;
65
+ }
66
+
67
+ /** Options for `FileOperationsApi.rename`. */
68
+ export interface FileRenameOptions {
69
+ /**
70
+ * Whether the target is a directory. Callers that already know (the
71
+ * explorer reads it off its tree) should pass it; otherwise it is
72
+ * resolved via `fs.stat` and, on providers without `stat`, assumed to
73
+ * be a file.
74
+ */
75
+ isDirectory?: boolean;
76
+ }
77
+
78
+ /** Options for `FileOperationsApi.delete`. */
79
+ export interface FileDeleteOperationOptions {
80
+ isDirectory?: boolean;
81
+ /** Passed through to `FileSystemProvider.deleteFile` for idempotency. */
82
+ ifExists?: boolean;
83
+ }
84
+
85
+ /**
86
+ * The single seam every file mutation that changes a path should go
87
+ * through: rename/move and delete.
88
+ *
89
+ * `runtime.fs` stays the raw filesystem surface and is still the right
90
+ * call for reads and for content writes, where the path is unchanged and
91
+ * nobody's key breaks. Creating and deleting a path, or moving one, is
92
+ * different: it invalidates state held elsewhere in the app, and the
93
+ * whole point of routing it here is that the invalidation is announced
94
+ * instead of discovered.
95
+ *
96
+ * Two consumers, one code path: the explorer's context menu and an agent
97
+ * invoking the `file-explorer.rename` capability both land here, so tab
98
+ * state stays correct whether a human or a model moved the file.
99
+ *
100
+ * ## Dirty (unsaved) tracking
101
+ *
102
+ * `setDirty` / `isDirty` / `getDirtyPaths` are a working-copy registry.
103
+ * The shell mirrors its content tabs into it, which lets a caller ask
104
+ * "would deleting this lose unsaved work?" without knowing that content
105
+ * tabs exist — the explorer's delete confirmation reads it to say so in
106
+ * the dialog, in one prompt rather than two.
107
+ */
108
+ export interface FileOperationsApi {
109
+ /**
110
+ * Add a participant. Registering the same `id` twice replaces the
111
+ * first. Dispose to remove it — a shell that is torn down must not
112
+ * leave a hook pointing at dead state.
113
+ */
114
+ registerParticipant(participant: FileOperationParticipant): {
115
+ dispose(): void;
116
+ };
117
+
118
+ /** Record (or clear) unsaved-changes state for a path. */
119
+ setDirty(path: string, dirty: boolean): void;
120
+ isDirty(path: string): boolean;
121
+ /**
122
+ * Every path with unsaved changes; with `prefix`, only that path and
123
+ * its descendants (so a folder delete can name what it would lose).
124
+ */
125
+ getDirtyPaths(prefix?: string): string[];
126
+
127
+ /**
128
+ * Move `oldPath` to `newPath`, announcing it. Rejects if a participant
129
+ * vetoes or the filesystem call fails; in both cases nothing has moved.
130
+ */
131
+ rename(
132
+ oldPath: string,
133
+ newPath: string,
134
+ opts?: FileRenameOptions
135
+ ): Promise<void>;
136
+
137
+ /** Delete `path`, announcing it. Same failure contract as `rename`. */
138
+ delete(path: string, opts?: FileDeleteOperationOptions): Promise<void>;
139
+ }
140
+ }
141
+ }
@@ -55,11 +55,32 @@ declare module 'punica' {
55
55
  command?: string;
56
56
  menu?: AppBarMenuItem[];
57
57
  /**
58
- * Command id returning a one-line summary rendered as the menu header
59
- * (e.g. the licence state). Dropped when the command is not registered,
60
- * which is how a host without the surface stays quiet.
58
+ * Command id returning the summary rendered as the menu header (e.g. the
59
+ * licence state, or who the app is licensed to). Dropped when the
60
+ * command is not registered, which is how a host without the surface
61
+ * stays quiet.
62
+ *
63
+ * A returned string may carry newlines, and each line is rendered as its
64
+ * own row with the first one emphasised — enough for a name over an
65
+ * address. Lines are clipped rather than wrapped, so a long address
66
+ * cannot widen the menu.
61
67
  */
62
68
  summaryCommand?: string;
69
+ /**
70
+ * Command id returning the avatar's glyph, at most two characters (a
71
+ * user's initials). Avatar items only.
72
+ *
73
+ * This exists because the glyph is identity and identity is not known
74
+ * when a profile is written: `label` is static, and a host cannot patch
75
+ * one item without re-declaring the whole thing, menu included. So the
76
+ * profile names a command and the host answers it.
77
+ *
78
+ * Called when the item renders and again whenever its menu opens, which
79
+ * is what makes a licence activated mid-session show up. Anything other
80
+ * than a non-empty string leaves the glyph as `label`'s first letter, so
81
+ * a host with no account has nothing to implement.
82
+ */
83
+ initialsCommand?: string;
63
84
  }
64
85
 
65
86
  /** A profile's app bar composition (see `ProfileShellSpec.appBar`). */
@@ -1,7 +1,24 @@
1
1
  declare module 'punica' {
2
2
  export namespace shell {
3
+ /** One open content tab, as reported by `ContentTabsApi.list`. */
4
+ export interface ContentTabInfo {
5
+ /** Workspace-relative file path, or a scheme URI for command-only views. */
6
+ path: string;
7
+ /** Label shown on the tab (without the unsaved-changes marker). */
8
+ title: string;
9
+ /** Set by whichever extension opened it (e.g. "code", "markdown"). */
10
+ viewType?: string;
11
+ /** Whether the tab holds unsaved edits. */
12
+ dirty: boolean;
13
+ }
14
+
3
15
  /**
4
16
  * Content tab management capability for the main editor area.
17
+ *
18
+ * Tabs are identified by `path` — the same workspace-relative path the
19
+ * filesystem uses — which is what makes `rename` necessary rather than
20
+ * cosmetic: after a file moves, every tab keyed by the old path is
21
+ * holding an identity that no longer exists.
5
22
  */
6
23
  export interface ContentTabsApi {
7
24
  open(options: {
@@ -9,9 +26,18 @@ declare module 'punica' {
9
26
  title?: string;
10
27
  viewType?: string;
11
28
  }): HTMLElement | null;
12
- close(path: string): void;
13
- closeOthers(path: string): void;
14
- closeAll(): void;
29
+ /**
30
+ * Close the tab at `path`, prompting first when it has unsaved changes
31
+ * (Save / Don't Save / Cancel). Resolves when the tab is gone or the
32
+ * user cancelled — check `list()` if you need to know which.
33
+ */
34
+ close(path: string): Promise<void>;
35
+ /** As `close`, addressed by tab id rather than path. */
36
+ closeById(id: string): Promise<void>;
37
+ closeOthers(path: string): Promise<void>;
38
+ /** As `closeOthers`, addressed by tab id rather than path. */
39
+ closeOthersById(id: string): Promise<void>;
40
+ closeAll(): Promise<void>;
15
41
  setDirty(path: string, dirty: boolean): void;
16
42
  /**
17
43
  * Returns the path of the currently active content tab, or null if none.
@@ -23,6 +49,34 @@ declare module 'punica' {
23
49
  * (e.g. "code", "flow-book", "markdown").
24
50
  */
25
51
  getActiveViewType(): string | null;
52
+
53
+ /**
54
+ * Re-key the tab at `oldPath` to `newPath`, keeping its content,
55
+ * dirty marker and selection. When `oldPath` names a directory, every
56
+ * tab beneath it moves too — renaming `src/` has to carry
57
+ * `src/app.ts` with it.
58
+ *
59
+ * This does not touch the filesystem; it is the tab half of a rename
60
+ * that already happened. Callers should not invoke it directly —
61
+ * `runtime.fileOperations.rename` drives it through the shell's
62
+ * participant, so an agent-initiated rename syncs the same way a
63
+ * context-menu one does.
64
+ */
65
+ rename(oldPath: string, newPath: string): void;
66
+
67
+ /**
68
+ * Close the tab at `path` (and, for a directory, everything beneath it)
69
+ * without prompting. For when the file is already gone: there is
70
+ * nothing left to save, so the unsaved-changes dialog would offer a
71
+ * choice that cannot be honored.
72
+ */
73
+ forceClose(path: string): void;
74
+
75
+ /** Whether the tab at `path` holds unsaved edits. False when no such tab. */
76
+ isDirty(path: string): boolean;
77
+
78
+ /** Every open tab, in the order they appear in the tab strip. */
79
+ list(): readonly ContentTabInfo[];
26
80
  }
27
81
 
28
82
  /**