@rigee/plugin-api 0.0.0-stage → 0.2.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/README.md +137 -2
- package/dist/context.d.ts +130 -0
- package/dist/context.js +23 -0
- package/dist/data.d.ts +62 -0
- package/dist/data.js +14 -0
- package/dist/host.d.ts +460 -0
- package/dist/host.js +234 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/manifest.d.ts +331 -0
- package/dist/manifest.js +70 -0
- package/dist/ui.d.ts +106 -0
- package/dist/ui.js +12 -0
- package/package.json +45 -4
package/dist/host.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
let host = null;
|
|
3
|
+
/**
|
|
4
|
+
* Called by the shell, exactly once, before any plugin activates.
|
|
5
|
+
*
|
|
6
|
+
* Throwing on a second call is not pedantry: a second install means a second copy of this module
|
|
7
|
+
* got loaded, and the plugins that imported the first copy are now talking to a host the shell has
|
|
8
|
+
* stopped updating. That failure is invisible — components render, calls resolve, and one plugin's
|
|
9
|
+
* tabs quietly go to the wrong place. Far better to fail at the install.
|
|
10
|
+
*/
|
|
11
|
+
export function installShellHost(impl) {
|
|
12
|
+
if (host)
|
|
13
|
+
throw new Error('@rigee/plugin-api: the shell host is already installed');
|
|
14
|
+
host = impl;
|
|
15
|
+
Object.assign(icons, impl.icons);
|
|
16
|
+
}
|
|
17
|
+
/** True once the shell has installed itself. For the shell's own start-up assertions. */
|
|
18
|
+
export function shellHostInstalled() {
|
|
19
|
+
return host !== null;
|
|
20
|
+
}
|
|
21
|
+
function shell() {
|
|
22
|
+
if (!host) {
|
|
23
|
+
throw new Error('@rigee/plugin-api: no shell host installed. Either this is being called outside Rigee, ' +
|
|
24
|
+
'or a second copy of @rigee/plugin-api was bundled into the plugin instead of being ' +
|
|
25
|
+
'left external and resolved through the import map.');
|
|
26
|
+
}
|
|
27
|
+
return host;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The shell's icons, filled in at install.
|
|
31
|
+
*
|
|
32
|
+
* A mutable object rather than named exports because named exports cannot be assigned from
|
|
33
|
+
* outside the module, and a per-icon forwarding component would add a React layer to every row of
|
|
34
|
+
* a file tree. After install these are the shell's own components, reached by one property read.
|
|
35
|
+
*/
|
|
36
|
+
export const icons = {};
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// The functions. Each is a forward; the comments that matter are on ShellHost.
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
export const isMac = () => shell().isMac();
|
|
41
|
+
export const monoFamily = () => shell().monoFamily();
|
|
42
|
+
export const editorFontSize = () => shell().editorFontSize();
|
|
43
|
+
export const activeWorkspaceId = () => shell().activeWorkspaceId();
|
|
44
|
+
export const useActiveWorkspace = () => shell().useActiveWorkspace();
|
|
45
|
+
export const workspaceDir = (workspace) => shell().workspaceDir(workspace);
|
|
46
|
+
export const addWorkspaceFolders = (paths) => shell().addWorkspaceFolders(paths);
|
|
47
|
+
export const removeWorkspaceFolders = (paths) => shell().removeWorkspaceFolders(paths);
|
|
48
|
+
export const openTerminal = (request) => shell().openTerminal(request);
|
|
49
|
+
export const popupMenu = (items) => shell().popupMenu(items);
|
|
50
|
+
/**
|
|
51
|
+
* Open (or re-activate) a tab this plugin owns, in the active workspace's active group.
|
|
52
|
+
*
|
|
53
|
+
* Returns the tab's id, or null when there is no workspace to put it in. The view type must be one
|
|
54
|
+
* this plugin declared; the shell throws if it belongs to somebody else.
|
|
55
|
+
*/
|
|
56
|
+
export const openPluginTab = (pluginId, request) => shell().openPluginTab(pluginId, request);
|
|
57
|
+
/**
|
|
58
|
+
* Open a file in whichever editor claims it.
|
|
59
|
+
*
|
|
60
|
+
* The resolution is the SHELL's — it reads the manifests, so no plugin is loaded to answer "who
|
|
61
|
+
* opens a .pdf" and the caller does not need to know that editors have priorities. This is what a
|
|
62
|
+
* file tree calls; before it existed the explorer reached into the store, invented a tab id and
|
|
63
|
+
* resolved the view type itself, which meant three shell internals in one click handler.
|
|
64
|
+
*/
|
|
65
|
+
export const openFile = (request) => shell().openFile(request);
|
|
66
|
+
/**
|
|
67
|
+
* The editors that open this file only when asked — a Markdown file's preview — in the words to
|
|
68
|
+
* offer them in. What a file tree puts in a file's menu; picking one is `openFile({ viewType })`.
|
|
69
|
+
* Read off the manifests, so nothing is loaded to answer, and empty for most files.
|
|
70
|
+
*/
|
|
71
|
+
export const editorsOnRequest = (path) => shell().editorsOnRequest(path);
|
|
72
|
+
export const patchPluginTab = (tabId, patch) => shell().patchPluginTab(tabId, patch);
|
|
73
|
+
export const closePluginTab = (tabId) => shell().closePluginTab(tabId);
|
|
74
|
+
/**
|
|
75
|
+
* Make this tab the one its column shows, and its column the active one. For content that takes
|
|
76
|
+
* the keyboard where the shell cannot see it — a click inside a native view — so that the shell's
|
|
77
|
+
* idea of "where the user is working", which every shortcut acts on, follows. A no-op when it is
|
|
78
|
+
* already so.
|
|
79
|
+
*/
|
|
80
|
+
export const activateTab = (tabId) => shell().activateTab(tabId);
|
|
81
|
+
/**
|
|
82
|
+
* Every open tab of one of this plugin's own view types, across every workspace.
|
|
83
|
+
*
|
|
84
|
+
* Exists for one job: repairing tabs restored from a persisted state older than the plugin. The
|
|
85
|
+
* shell can move an old tab's fields into `state`, but it cannot invent a dedup key — only the
|
|
86
|
+
* plugin knows what identifies one of its tabs.
|
|
87
|
+
*/
|
|
88
|
+
export const pluginTabs = (viewType) => shell().pluginTabs(viewType);
|
|
89
|
+
/**
|
|
90
|
+
* Every tab id that exists, in every group of every workspace.
|
|
91
|
+
*
|
|
92
|
+
* For plugins holding a resource per tab — a native browser view, a parsed document — that has to
|
|
93
|
+
* be released when the tab goes. Callers treat anything absent from this set as closed, so it spans
|
|
94
|
+
* every workspace deliberately: switching workspace must not read as "every tab was closed".
|
|
95
|
+
*/
|
|
96
|
+
export const liveTabIds = () => shell().liveTabIds();
|
|
97
|
+
/** Watch for tabs disappearing. Wrap the returned undo in `ctx.effect` so it dies with the plugin. */
|
|
98
|
+
export const subscribeTabs = (listener) => shell().subscribeTabs(listener);
|
|
99
|
+
/**
|
|
100
|
+
* The tab this one was split from, or null.
|
|
101
|
+
*
|
|
102
|
+
* CONSUME-ONCE. Read it on first mount and keep what you got; the second caller, and every render
|
|
103
|
+
* after the first, gets null. StrictMode's double-invoked effect counts as a second caller, so this
|
|
104
|
+
* belongs in a `useState` initialiser or a ref, never in an effect body.
|
|
105
|
+
*/
|
|
106
|
+
export const duplicatedFrom = (tabId) => shell().duplicatedFrom(tabId);
|
|
107
|
+
/**
|
|
108
|
+
* The one tab with the keyboard: the active group's active tab, in the active workspace.
|
|
109
|
+
*
|
|
110
|
+
* One rule, in one place. Two copies of it is how two visible panes end up disagreeing about which
|
|
111
|
+
* of them should be typing.
|
|
112
|
+
*/
|
|
113
|
+
export const focusedTabId = () => shell().focusedTabId();
|
|
114
|
+
/** The same rule as a hook, for a pane that has to re-render when focus moves to another column. */
|
|
115
|
+
export const useFocusedTabId = () => shell().useFocusedTabId();
|
|
116
|
+
/**
|
|
117
|
+
* The active tab of every group in the active workspace, left to right.
|
|
118
|
+
*
|
|
119
|
+
* What is actually on screen. A plugin compositing something outside the DOM — a native view — has
|
|
120
|
+
* to know exactly this set, and only for the active workspace: the tabs of the workspace you
|
|
121
|
+
* switched away from are alive but not visible.
|
|
122
|
+
*/
|
|
123
|
+
export const mountedTabIds = () => shell().mountedTabIds();
|
|
124
|
+
/** Fires when `mountedTabIds` changes — a split, a drag between groups, a workspace switch. */
|
|
125
|
+
export const subscribeMountedTabs = (listener) => shell().subscribeMountedTabs(listener);
|
|
126
|
+
/**
|
|
127
|
+
* A plugin's own persisted state for one workspace.
|
|
128
|
+
*
|
|
129
|
+
* Saved and restored with the workspace by the shell, which never looks inside it. For the things
|
|
130
|
+
* a panel wants to remember per project — which directories are expanded, a filter, a scroll
|
|
131
|
+
* position. Undefined means "nothing recorded yet", which is different from "recorded as empty":
|
|
132
|
+
* the file tree opens its root folders on first sight and must not do it again after the user
|
|
133
|
+
* collapses everything.
|
|
134
|
+
*/
|
|
135
|
+
export const pluginState = (pluginId, workspaceId) => shell().pluginState(pluginId, workspaceId);
|
|
136
|
+
/** The same, as a hook. Null workspace yields undefined rather than throwing. */
|
|
137
|
+
export const usePluginState = (pluginId, workspaceId) => shell().usePluginState(pluginId, workspaceId);
|
|
138
|
+
/** Merge into this plugin's state for one workspace, and persist it. */
|
|
139
|
+
export const setPluginState = (pluginId, workspaceId, patch) => shell().setPluginState(pluginId, workspaceId, patch);
|
|
140
|
+
/**
|
|
141
|
+
* Say whether a file has unsaved changes, and how to save or drop them.
|
|
142
|
+
*
|
|
143
|
+
* The shell marks its tabs with a dot and, when one would close — or the app would quit — with
|
|
144
|
+
* the changes unsaved, asks the user and calls `save` or `discard`. Keyed by PATH, not tab: one
|
|
145
|
+
* document can be open in two columns, and it is the document that is unsaved.
|
|
146
|
+
*/
|
|
147
|
+
export const setFileDirty = (path, dirty, handlers) => shell().setFileDirty(path, dirty, handlers);
|
|
148
|
+
/**
|
|
149
|
+
* Replace every decoration one source publishes, keyed by absolute path. A path ending in a
|
|
150
|
+
* separator covers the folder and everything inside it. An empty record withdraws the source.
|
|
151
|
+
* Folders above a decorated path take its colour, so collapsed trees still show where changes are.
|
|
152
|
+
*/
|
|
153
|
+
export const setFileDecorations = (source, decorations) => shell().setFileDecorations(source, decorations);
|
|
154
|
+
/** The decoration to draw for a path. A hook: re-renders when that path's decoration changes. */
|
|
155
|
+
export const useFileDecoration = (path) => shell().useFileDecoration(path);
|
|
156
|
+
/**
|
|
157
|
+
* Where the cursor should go in the file this tab shows — the target of a search result or any
|
|
158
|
+
* other `openFile({ selection })`. A request made before the editor subscribed is delivered as it
|
|
159
|
+
* subscribes, so subscribe on mount. Wrap the returned undo in the unmount.
|
|
160
|
+
*/
|
|
161
|
+
export const onRevealInFile = (tabId, cb) => shell().onRevealInFile(tabId, cb);
|
|
162
|
+
/**
|
|
163
|
+
* Called when the user asks for this side panel by keyboard — the panel should take the keyboard,
|
|
164
|
+
* as a search box does. Delivered on subscribe if the request came before the panel mounted.
|
|
165
|
+
*/
|
|
166
|
+
export const onViewRevealed = (viewId, cb) => shell().onViewRevealed(viewId, cb);
|
|
167
|
+
/**
|
|
168
|
+
* Whether this tab was opened with `preserveFocus`, so its viewer should leave the keyboard where
|
|
169
|
+
* it is instead of taking it as it mounts. Answers true once, for the open that asked: call it at
|
|
170
|
+
* the moment the viewer would focus itself, not before.
|
|
171
|
+
*/
|
|
172
|
+
export const openedInBackground = (tabId) => shell().openedInBackground(tabId);
|
|
173
|
+
/**
|
|
174
|
+
* Files a plugin has just renamed or moved for the user — a file tree's Rename, its Cut and Paste,
|
|
175
|
+
* a drag — so the tabs showing them follow, under their new names. A folder covers everything in
|
|
176
|
+
* it. Files moved by other programs are not reported here, and their tabs stay where they were.
|
|
177
|
+
*/
|
|
178
|
+
export const filesMoved = (moves) => shell().filesMoved(moves);
|
|
179
|
+
/**
|
|
180
|
+
* Files a plugin has just deleted for the user. Their tabs close, and unsaved changes in them are
|
|
181
|
+
* dropped — so ask about those first, with `unsavedFiles`. A folder covers everything in it.
|
|
182
|
+
*/
|
|
183
|
+
export const filesDeleted = (paths) => shell().filesDeleted(paths);
|
|
184
|
+
/** Which files at these paths, or inside them, have unsaved changes. */
|
|
185
|
+
export const unsavedFiles = (paths) => shell().unsavedFiles(paths);
|
|
186
|
+
/**
|
|
187
|
+
* Needs `files:read`. `notUtf8` means the bytes were not valid UTF-8 and `content` is a lossy
|
|
188
|
+
* decoding — show it, but never write it back.
|
|
189
|
+
*/
|
|
190
|
+
export const readFile = (path) => shell().readFile(path);
|
|
191
|
+
/** Needs `files:write`. */
|
|
192
|
+
export const writeFile = (path, content) => shell().writeFile(path, content);
|
|
193
|
+
/** Needs `files:read`. */
|
|
194
|
+
export const listDir = (path) => shell().listDir(path);
|
|
195
|
+
/**
|
|
196
|
+
* Tokenise code with the text editor's own tokenizers: the tokens of each line, in order, or null
|
|
197
|
+
* when no language goes by that name. The name may be a language id, an alias or an extension —
|
|
198
|
+
* `ts`, `typescript` and `TypeScript` all find TypeScript — so a Markdown fence's word can be
|
|
199
|
+
* passed as it is.
|
|
200
|
+
*
|
|
201
|
+
* The tokens' text is the caller's: slice each line at the offsets. That is the point of asking
|
|
202
|
+
* for tokens rather than coloured HTML — HTML from the editor writes spaces as no-break spaces, so
|
|
203
|
+
* code copied out of it would not be the code that was written.
|
|
204
|
+
*
|
|
205
|
+
* Loads the editor's tokenizers the first time, which is megabytes: call it for code that is about
|
|
206
|
+
* to be shown, not speculatively. Since Rigee 1.1.0.
|
|
207
|
+
*/
|
|
208
|
+
export const tokenizeCode = (text, language) => shell().tokenizeCode(text, language);
|
|
209
|
+
/**
|
|
210
|
+
* The URL to start a Web Worker from, for a script that ships with this plugin.
|
|
211
|
+
*
|
|
212
|
+
* A window may only start a worker from its own origin, and a plugin's files are served from
|
|
213
|
+
* another — `rigee-plugin://` — so `new Worker(new URL('./x.js', import.meta.url))` is refused.
|
|
214
|
+
* Pass the same URL here instead and start the worker from what comes back, as a module worker:
|
|
215
|
+
*
|
|
216
|
+
* new Worker(workerUrl(new URL('./parse.worker.js', import.meta.url)), { type: 'module' })
|
|
217
|
+
*
|
|
218
|
+
* The script must be self-contained: a worker has no import map, so `react` and this package
|
|
219
|
+
* cannot be imported from inside one. Messages posted before the script has loaded are held until
|
|
220
|
+
* it has, and then delivered. The worker gets no Content Security Policy of its own, so it may
|
|
221
|
+
* compile WebAssembly, which the window may not. Since Rigee 1.1.0.
|
|
222
|
+
*/
|
|
223
|
+
export const workerUrl = (script) => shell().workerUrl(script);
|
|
224
|
+
/**
|
|
225
|
+
* The shell's "this pane has nothing to show" state.
|
|
226
|
+
*
|
|
227
|
+
* A wrapper rather than an entry in `icons`, because a notice is rendered once per failed pane
|
|
228
|
+
* where an icon is rendered once per row — the extra component layer costs nothing here and reads
|
|
229
|
+
* better at the call site.
|
|
230
|
+
*/
|
|
231
|
+
export function Notice(props) {
|
|
232
|
+
const Impl = shell().Notice;
|
|
233
|
+
return _jsx(Impl, { ...props });
|
|
234
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rigee/plugin-api` — everything a plugin is allowed to know about Rigee.
|
|
3
|
+
*
|
|
4
|
+
* A plugin should import from here and from nowhere else in the app. That is the whole point of
|
|
5
|
+
* the package: what is exported here is versioned and will be kept working, and what is not is the
|
|
6
|
+
* shell's own business and will be moved without warning.
|
|
7
|
+
*
|
|
8
|
+
* TWO ENTRY POINTS, and the split is load-bearing rather than tidy:
|
|
9
|
+
*
|
|
10
|
+
* `@rigee/plugin-api` the full surface. Needs a DOM and React. Renderer halves.
|
|
11
|
+
* `@rigee/plugin-api/manifest` the declarative contract alone: no React, no DOM, no runtime.
|
|
12
|
+
*
|
|
13
|
+
* The main process reads manifests too — to decide which plugin owns a command before importing a
|
|
14
|
+
* line of it — and importing this barrel there would drag React into a process that can never
|
|
15
|
+
* render. The same reason the manifests themselves hold no import thunks.
|
|
16
|
+
*/
|
|
17
|
+
export * from './data';
|
|
18
|
+
export * from './manifest';
|
|
19
|
+
export * from './ui';
|
|
20
|
+
export * from './host';
|
|
21
|
+
export * from './context';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rigee/plugin-api` — everything a plugin is allowed to know about Rigee.
|
|
3
|
+
*
|
|
4
|
+
* A plugin should import from here and from nowhere else in the app. That is the whole point of
|
|
5
|
+
* the package: what is exported here is versioned and will be kept working, and what is not is the
|
|
6
|
+
* shell's own business and will be moved without warning.
|
|
7
|
+
*
|
|
8
|
+
* TWO ENTRY POINTS, and the split is load-bearing rather than tidy:
|
|
9
|
+
*
|
|
10
|
+
* `@rigee/plugin-api` the full surface. Needs a DOM and React. Renderer halves.
|
|
11
|
+
* `@rigee/plugin-api/manifest` the declarative contract alone: no React, no DOM, no runtime.
|
|
12
|
+
*
|
|
13
|
+
* The main process reads manifests too — to decide which plugin owns a command before importing a
|
|
14
|
+
* line of it — and importing this barrel there would drag React into a process that can never
|
|
15
|
+
* render. The same reason the manifests themselves hold no import thunks.
|
|
16
|
+
*/
|
|
17
|
+
export * from './data';
|
|
18
|
+
export * from './manifest';
|
|
19
|
+
export * from './ui';
|
|
20
|
+
export * from './host';
|
|
21
|
+
export * from './context';
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin contract, shared by main / preload / renderer.
|
|
3
|
+
*
|
|
4
|
+
* Rigee's core is a shell: a window, workspaces, a tab framework, terminals, persistence and the
|
|
5
|
+
* IPC spine. Everything that shows a document or a panel — the text editor, source control, the
|
|
6
|
+
* browser, the PDF viewer — is a plugin. This file is the contract they are written against.
|
|
7
|
+
*
|
|
8
|
+
* A plugin is not one object but up to two halves. Electron leaves no choice: a PDF renders in the
|
|
9
|
+
* sandboxed renderer, and the bytes it renders can only be read in main. So a plugin declares a
|
|
10
|
+
* `main` half and a `renderer` half, sharing an id and talking over one IPC channel.
|
|
11
|
+
*
|
|
12
|
+
* THREE RULES THAT SHAPE EVERYTHING HERE
|
|
13
|
+
*
|
|
14
|
+
* 1. This file holds the declarative part only — the shape read at boot to decide *what exists*.
|
|
15
|
+
* Knowing that some plugin claims `*.pdf` must not cost the 437 kB of pdf.js that renders one.
|
|
16
|
+
* The imperative halves load on demand, through the per-realm registries.
|
|
17
|
+
*
|
|
18
|
+
* 2. A manifest carries NO import thunks. They used to live here, and they cannot: TypeScript
|
|
19
|
+
* resolves the literal specifier of a dynamic `import()` into the program, so one manifest
|
|
20
|
+
* naming both halves drags the main half (node:fs) into the web project and the renderer half
|
|
21
|
+
* (.tsx) into the node project. Both fail `tsc -b`. The thunks live in `registry.main.ts` and
|
|
22
|
+
* `registry.renderer.ts`, each covered by exactly one tsconfig.
|
|
23
|
+
*
|
|
24
|
+
* 3. Contributions name components; they never hold them. cordis service access is between 700x
|
|
25
|
+
* and 2000x the cost of a plain property read (measured: 15.5 us for `ctx.svc.method()` against
|
|
26
|
+
* 8 ns), so the framework stays on the activation path and every render-path lookup goes
|
|
27
|
+
* through the plain Maps in the renderer's `bindings.ts`.
|
|
28
|
+
*
|
|
29
|
+
* This file is the root of `@rigee/plugin-api`, which is the version the two sides agree on. It
|
|
30
|
+
* must stay free of runtime imports: it is read at boot in main AND in the renderer, and anything
|
|
31
|
+
* it reaches lands in both bundles.
|
|
32
|
+
*/
|
|
33
|
+
/** Stable identity of a plugin. Namespaces its commands, its view types and its events. */
|
|
34
|
+
export type PluginId = string;
|
|
35
|
+
/**
|
|
36
|
+
* A view type is a `<pluginId>.<name>` pair, e.g. `pdf.preview`.
|
|
37
|
+
*
|
|
38
|
+
* Dotted rather than colon-separated so it stays distinguishable at a glance from an IPC channel
|
|
39
|
+
* (`fs:readFile`) and from a command (`pdf:read`) in a stack trace or a log line.
|
|
40
|
+
*/
|
|
41
|
+
export type ViewType = string;
|
|
42
|
+
/**
|
|
43
|
+
* Icons a contribution may ask for, by name.
|
|
44
|
+
*
|
|
45
|
+
* A name rather than a component, because a manifest is read at boot in both realms and must stay
|
|
46
|
+
* plain data — a React element here would put the whole icon module, and with it a value import of
|
|
47
|
+
* React, into the main process. The names map to `src/renderer/src/icons.tsx`.
|
|
48
|
+
*/
|
|
49
|
+
export type IconName = 'file' | 'files' | 'diff' | 'globe' | 'terminal' | 'branch' | 'folder' | 'plugin' | 'search' | 'image' | 'video' | 'preview';
|
|
50
|
+
/**
|
|
51
|
+
* A file matcher, in the only two forms worth supporting before someone asks for a third:
|
|
52
|
+
* an extension list, or a predicate on the absolute path.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately not a glob. A glob library is 20 kB to express `*.pdf`, and the moment one appears
|
|
55
|
+
* in a manifest people start writing `**\/node_modules/**` in it and expecting gitignore
|
|
56
|
+
* semantics.
|
|
57
|
+
*/
|
|
58
|
+
export type FileSelector =
|
|
59
|
+
/** Lower-case, no leading dot: `['pdf']`. Matched against the path's final extension. */
|
|
60
|
+
{
|
|
61
|
+
extensions: string[];
|
|
62
|
+
} | {
|
|
63
|
+
match: (absolutePath: string) => boolean;
|
|
64
|
+
};
|
|
65
|
+
/** One editor a plugin contributes. Declared in the manifest, bound when the plugin activates. */
|
|
66
|
+
export interface EditorContribution {
|
|
67
|
+
viewType: ViewType;
|
|
68
|
+
/** Shown in the "Open with" affordance and in the tab's tooltip. */
|
|
69
|
+
displayName: string;
|
|
70
|
+
selector: FileSelector;
|
|
71
|
+
icon?: IconName;
|
|
72
|
+
/**
|
|
73
|
+
* Higher wins when several editors claim the same file; ties break on manifest order.
|
|
74
|
+
*
|
|
75
|
+
* The text editor contributes at -Infinity and claims everything, so it is the floor: any other
|
|
76
|
+
* editor beats it, and a file nothing else claims still opens.
|
|
77
|
+
*/
|
|
78
|
+
priority?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Open a file in this editor only when the user asks for it by name, never because it claims the
|
|
81
|
+
* file — and say how they ask.
|
|
82
|
+
*
|
|
83
|
+
* For a second way of looking at a file that has a first one already: a Markdown file is source,
|
|
84
|
+
* written in the text editor, and its preview is something you ask for. Claiming `*.md` outright
|
|
85
|
+
* would take the editing away; claiming it at a priority below the text editor is impossible,
|
|
86
|
+
* because the text editor is the floor. So `priority` does not apply here, and the file opens in
|
|
87
|
+
* whatever else claims it — this editor is offered in its menu instead.
|
|
88
|
+
*
|
|
89
|
+
* Such an editor is taken to SHOW the file, never to edit it: its tab is not somewhere unsaved
|
|
90
|
+
* changes can be, so closing the file's own tab beside it still asks about them.
|
|
91
|
+
*/
|
|
92
|
+
onRequest?: EditorRequest;
|
|
93
|
+
}
|
|
94
|
+
/** How an editor that opens only on request is asked for, and how its tabs read. */
|
|
95
|
+
export interface EditorRequest {
|
|
96
|
+
/** The item in a file's menu that opens it: `Open Preview`. */
|
|
97
|
+
label: string;
|
|
98
|
+
/**
|
|
99
|
+
* Put before the file's name on the tab, so it reads apart from the file's own tab beside it:
|
|
100
|
+
* `Preview` gives `Preview README.md`.
|
|
101
|
+
*/
|
|
102
|
+
tabPrefix?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A tab type a plugin owns end to end — a browser tab, a diff.
|
|
106
|
+
*
|
|
107
|
+
* Distinct from an editor, which is "a viewer for a file the shell opened". A custom tab carries
|
|
108
|
+
* its own state and the shell knows nothing about it beyond what is declared here.
|
|
109
|
+
*/
|
|
110
|
+
export interface TabContribution {
|
|
111
|
+
viewType: ViewType;
|
|
112
|
+
displayName: string;
|
|
113
|
+
icon?: IconName;
|
|
114
|
+
/**
|
|
115
|
+
* Whether this tab shows a document — a diff does, a browser tab does not — which is how the
|
|
116
|
+
* tab strip tells files from everything else.
|
|
117
|
+
*
|
|
118
|
+
* It used to decide what the bulk-close commands reached. They reach every tab now, as in VS
|
|
119
|
+
* Code, and a program a sweep would stop is asked about instead.
|
|
120
|
+
*/
|
|
121
|
+
isDocument?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* A tab kind from a persisted state older than the plugin system, to be rewritten into this
|
|
124
|
+
* view type on load.
|
|
125
|
+
*
|
|
126
|
+
* Migration lives in the manifest rather than in the shell so the shell keeps no list of the
|
|
127
|
+
* features it used to own. `sanitizeTabs` reads this; it is pure data and costs one scan.
|
|
128
|
+
*/
|
|
129
|
+
legacyKind?: string;
|
|
130
|
+
/**
|
|
131
|
+
* What the split button does to a tab of this type: put a copy in the new group, or move it.
|
|
132
|
+
*
|
|
133
|
+
* Defaults to 'move', which is the safe answer — a plugin that has not thought about being in two
|
|
134
|
+
* places at once probably cannot be. A browser tab cannot: its native view is keyed by tab id, so
|
|
135
|
+
* a copy would either share the view or quietly start a second page with no session. A diff can,
|
|
136
|
+
* and should, because reading two revisions of the same file side by side is the point.
|
|
137
|
+
*
|
|
138
|
+
* The shell reads this from the manifest alone, so pressing split does not activate the plugin
|
|
139
|
+
* just to ask it.
|
|
140
|
+
*/
|
|
141
|
+
split?: 'copy' | 'move';
|
|
142
|
+
/**
|
|
143
|
+
* Offer a button in the tab strip that opens a new one of these.
|
|
144
|
+
*
|
|
145
|
+
* The label is declarative, so the button exists before the plugin does; pressing it activates
|
|
146
|
+
* the plugin and asks the factory it bound for the new tab's title and initial state. That
|
|
147
|
+
* ordering is why the factory cannot live here: only the plugin knows what an empty browser tab
|
|
148
|
+
* is, and the whole point is not to have loaded it yet.
|
|
149
|
+
*/
|
|
150
|
+
creatable?: {
|
|
151
|
+
label: string;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A panel in one of the two side bars.
|
|
156
|
+
*
|
|
157
|
+
* The left slot is an activity bar: one icon per view, one view visible at a time, the way VS Code
|
|
158
|
+
* arranges it. The right slot stacks whatever is contributed to it — source control lives there so
|
|
159
|
+
* the file tree and the changes stay visible together, which is the one thing this layout does
|
|
160
|
+
* better than VS Code's.
|
|
161
|
+
*/
|
|
162
|
+
export interface ViewContribution {
|
|
163
|
+
/** `<pluginId>.<name>`, like a view type: the same namespace rule, the same reason. */
|
|
164
|
+
id: ViewType;
|
|
165
|
+
title: string;
|
|
166
|
+
slot: 'left' | 'right';
|
|
167
|
+
/**
|
|
168
|
+
* The activity bar button. Required, not optional: a left-slot view with no icon would be a
|
|
169
|
+
* button nobody can identify, and an icon invented by the shell on its behalf would be worse.
|
|
170
|
+
*/
|
|
171
|
+
icon: IconName;
|
|
172
|
+
/** Lower sorts first within a slot, and top to bottom in the activity bar. */
|
|
173
|
+
order?: number;
|
|
174
|
+
}
|
|
175
|
+
/** An item in the status bar. */
|
|
176
|
+
export interface StatusBarContribution {
|
|
177
|
+
id: ViewType;
|
|
178
|
+
align: 'left' | 'right';
|
|
179
|
+
order?: number;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* A component mounted at the shell root while the plugin is active — a modal, a prompt.
|
|
183
|
+
*
|
|
184
|
+
* Mounted on activation rather than at boot, which is what keeps this honest: the browser plugin's
|
|
185
|
+
* HTTP auth dialog cannot be needed before a browser view exists, and by then the plugin that owns
|
|
186
|
+
* the view is already loaded.
|
|
187
|
+
*/
|
|
188
|
+
export interface OverlayContribution {
|
|
189
|
+
id: ViewType;
|
|
190
|
+
}
|
|
191
|
+
export interface PluginContributions {
|
|
192
|
+
editors?: EditorContribution[];
|
|
193
|
+
tabs?: TabContribution[];
|
|
194
|
+
views?: ViewContribution[];
|
|
195
|
+
statusBar?: StatusBarContribution[];
|
|
196
|
+
overlays?: OverlayContribution[];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A capability a plugin must ASK FOR before the shell will give it.
|
|
200
|
+
*
|
|
201
|
+
* Deliberately coarse, and deliberately short. A long list of fine-grained permissions reads well
|
|
202
|
+
* in a design document and is worthless in a dialog: nobody can weigh nine checkboxes, so everyone
|
|
203
|
+
* presses yes, and the nine were only ever a way of not deciding which three mattered. These four
|
|
204
|
+
* are the ones a reviewer can actually reason about.
|
|
205
|
+
*
|
|
206
|
+
* BE CLEAR ABOUT WHAT THESE DO, BECAUSE IT IS NOT WHAT THE WORD "PERMISSION" SUGGESTS. Nothing
|
|
207
|
+
* enforces them at the call site, and nothing can in this architecture: a main half is an ES module
|
|
208
|
+
* in the main process and imports `node:fs` itself, and a renderer half runs in the shell's own
|
|
209
|
+
* JavaScript context and can reach `window.rigee` directly. A declaration is a statement of intent,
|
|
210
|
+
* not a capability the shell hands over and can withhold.
|
|
211
|
+
*
|
|
212
|
+
* What they are actually for is CHANGE DETECTION, and that part is enforced. The set is recorded
|
|
213
|
+
* with the trust grant, so a plugin that asked only to read files and comes back next version
|
|
214
|
+
* wanting to run programs cannot be silently loaded — approval is withdrawn and the user is asked
|
|
215
|
+
* again. Nobody re-reads a plugin they already trusted; this is the moment they would.
|
|
216
|
+
*
|
|
217
|
+
* Real enforcement needs a separate context per plugin — a worker for the logic, a locked-down
|
|
218
|
+
* frame for the UI, every contribution point becoming a message — which is a much larger change
|
|
219
|
+
* and is not pretended at here. Installing a plugin runs its code with the application's
|
|
220
|
+
* privileges, and Rigee's install dialog says exactly that.
|
|
221
|
+
*/
|
|
222
|
+
export type PluginPermission =
|
|
223
|
+
/** Read files anywhere the user can. Any viewer needs this. */
|
|
224
|
+
'files:read'
|
|
225
|
+
/** Write files. The text editor needs it; a reader does not. */
|
|
226
|
+
| 'files:write'
|
|
227
|
+
/** Run programs. Source control needs it to run `git`; almost nothing else should. */
|
|
228
|
+
| 'process:spawn'
|
|
229
|
+
/** Reach the network, directly or by showing a remote page. */
|
|
230
|
+
| 'net';
|
|
231
|
+
export declare const ALL_PERMISSIONS: readonly PluginPermission[];
|
|
232
|
+
/** One line each, for the install dialog. The user reads these, not the identifiers. */
|
|
233
|
+
export declare const PERMISSION_LABELS: Record<PluginPermission, string>;
|
|
234
|
+
/**
|
|
235
|
+
* What the host reads at boot.
|
|
236
|
+
*
|
|
237
|
+
* Shaped so it can move into a plugin's `package.json` unchanged once plugins load from disk
|
|
238
|
+
* rather than from the bundled barrel — `id`/`version`/`displayName`/`contributes` are the same
|
|
239
|
+
* four fields VS Code reads, for the same reason. A plugin on disk supplies exactly this object
|
|
240
|
+
* under a `rigee` key, plus the two entry points (see `DiskPluginManifest`).
|
|
241
|
+
*/
|
|
242
|
+
export interface PluginManifest {
|
|
243
|
+
id: PluginId;
|
|
244
|
+
version: string;
|
|
245
|
+
displayName: string;
|
|
246
|
+
description?: string;
|
|
247
|
+
/**
|
|
248
|
+
* What it needs to be allowed to do. Absent means none, and a plugin that asks for nothing is
|
|
249
|
+
* installable without a prompt beyond "this code will run inside rigee".
|
|
250
|
+
*/
|
|
251
|
+
permissions?: PluginPermission[];
|
|
252
|
+
contributes?: PluginContributions;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The extra fields a plugin loaded FROM DISK carries, on top of the manifest itself.
|
|
256
|
+
*
|
|
257
|
+
* Bundled plugins do not need them: their halves are reached through `registry.main.ts` and
|
|
258
|
+
* `registry.renderer.ts`, which the build resolves statically. A plugin from somebody else's
|
|
259
|
+
* repository has no such entry, so it names its own files — relative to its own directory, and
|
|
260
|
+
* checked for containment before anything is read.
|
|
261
|
+
*/
|
|
262
|
+
export interface DiskPluginManifest extends PluginManifest {
|
|
263
|
+
/** ESM entry for the main half, relative to the plugin directory. Omit if it has none. */
|
|
264
|
+
main?: string;
|
|
265
|
+
/** ESM entry for the renderer half, relative to the plugin directory. Omit if it has none. */
|
|
266
|
+
renderer?: string;
|
|
267
|
+
/**
|
|
268
|
+
* Major version of `@rigee/plugin-api` this was built against.
|
|
269
|
+
*
|
|
270
|
+
* Checked before anything is loaded. A plugin built against a contract the shell no longer
|
|
271
|
+
* speaks must be refused with that as the reason, not allowed to fail later as a missing export
|
|
272
|
+
* in a stack trace nobody can act on.
|
|
273
|
+
*/
|
|
274
|
+
apiVersion: number;
|
|
275
|
+
/**
|
|
276
|
+
* The oldest Rigee this runs in, as a version: `1.1.0`. Omit if any Rigee that speaks
|
|
277
|
+
* `apiVersion` will do.
|
|
278
|
+
*
|
|
279
|
+
* The other half of what `apiVersion` cannot say. Adding a function to this package does not
|
|
280
|
+
* bump `apiVersion`, because a plugin that never calls it is unaffected — but a plugin that DOES
|
|
281
|
+
* call it fails to link in an older shell, whose copy of the package has no such export, and the
|
|
282
|
+
* error names a module rather than the problem. Declaring the version that added the function
|
|
283
|
+
* turns that into a refusal at discovery, with "needs Rigee X" as the reason.
|
|
284
|
+
*/
|
|
285
|
+
minAppVersion?: string;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The API generation the shell implements.
|
|
289
|
+
*
|
|
290
|
+
* Bumped when something is REMOVED or changed shape — adding a function does not break a plugin
|
|
291
|
+
* that never calls it. A disk plugin whose `apiVersion` differs is refused at discovery.
|
|
292
|
+
*/
|
|
293
|
+
export declare const PLUGIN_API_VERSION = 1;
|
|
294
|
+
/**
|
|
295
|
+
* Every renderer→main plugin call rides one channel, `plugin:invoke`.
|
|
296
|
+
*
|
|
297
|
+
* The alternative was to let plugins add entries to `REQUEST_CHANNELS`, which would mean the
|
|
298
|
+
* preload allowlist is no longer a literal — it would have to be assembled at runtime from
|
|
299
|
+
* whatever main happens to have loaded, and a static allowlist that is actually static is worth
|
|
300
|
+
* more than the ergonomics of a per-plugin channel name. So the allowlist grows by exactly two
|
|
301
|
+
* entries, once, and never again.
|
|
302
|
+
*
|
|
303
|
+
* Authorisation lives in main: `plugin:invoke` reaches the command registry, and a command that
|
|
304
|
+
* was never registered is rejected there. Registration is where the namespace rule is enforced —
|
|
305
|
+
* a plugin may only register commands prefixed with its own id, so one plugin cannot shadow
|
|
306
|
+
* another's.
|
|
307
|
+
*/
|
|
308
|
+
export interface PluginInvocation {
|
|
309
|
+
/** `<pluginId>:<verb>`, e.g. `pdf:read`. */
|
|
310
|
+
command: string;
|
|
311
|
+
payload: unknown;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The other direction: main→renderer push, also on one channel.
|
|
315
|
+
*
|
|
316
|
+
* Plugins need this for anything they do not own the timing of — a git status arriving because a
|
|
317
|
+
* watcher fired, a page finishing its navigation. The envelope carries the plugin id so the
|
|
318
|
+
* renderer can hand the payload to that plugin's listeners without every plugin seeing every
|
|
319
|
+
* other plugin's events.
|
|
320
|
+
*/
|
|
321
|
+
export interface PluginEventEnvelope {
|
|
322
|
+
pluginId: PluginId;
|
|
323
|
+
/** Plugin-local name, NOT prefixed: `status`, not `scm:status`. */
|
|
324
|
+
event: string;
|
|
325
|
+
payload: unknown;
|
|
326
|
+
}
|
|
327
|
+
/** Prefix a command must carry to be registerable by `id`. */
|
|
328
|
+
export declare function commandNamespace(id: PluginId): string;
|
|
329
|
+
/** Prefix a view type, tab type, view or status bar item must carry to belong to `id`. */
|
|
330
|
+
export declare function viewNamespace(id: PluginId): string;
|
|
331
|
+
export declare function matchesSelector(selector: FileSelector, absolutePath: string): boolean;
|