xtralab 0.1.1 → 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 +86 -0
- package/lib/fileBrowser/fileBrowser.js +71 -1
- package/lib/fileBrowser/gitignore.d.ts +29 -0
- package/lib/fileBrowser/gitignore.js +74 -0
- package/lib/fileBrowser/index.js +1 -1
- package/lib/git/api.d.ts +11 -0
- package/lib/git/api.js +19 -0
- package/lib/git/commands.d.ts +3 -1
- package/lib/git/commands.js +10 -4
- package/lib/git/diffWidget.d.ts +11 -0
- package/lib/git/diffWidget.js +90 -9
- package/lib/git/index.js +23 -7
- package/lib/git/panelComponent.d.ts +3 -1
- package/lib/git/panelComponent.js +6 -3
- package/lib/index.js +9 -1
- package/lib/launcher/agents.d.ts +78 -0
- package/lib/launcher/agents.js +168 -0
- package/lib/launcher/availability.d.ts +12 -0
- package/lib/launcher/availability.js +53 -0
- package/lib/launcher/commands.d.ts +22 -0
- package/lib/launcher/commands.js +89 -0
- package/lib/launcher/dashboard.d.ts +46 -0
- package/lib/launcher/dashboard.js +164 -0
- package/lib/launcher/icons.d.ts +33 -0
- package/lib/launcher/icons.js +113 -0
- package/lib/launcher/index.d.ts +23 -0
- package/lib/launcher/index.js +191 -0
- package/lib/launcher/invocation.d.ts +29 -0
- package/lib/launcher/invocation.js +42 -0
- package/lib/menus/index.d.ts +14 -0
- package/lib/menus/index.js +102 -0
- package/lib/sidebar/index.d.ts +18 -0
- package/lib/sidebar/index.js +155 -0
- package/lib/statusBar/index.d.ts +14 -0
- package/lib/statusBar/index.js +89 -0
- package/lib/statusBar/widget.d.ts +121 -0
- package/lib/statusBar/widget.js +237 -0
- package/package.json +10 -2
- package/schema/launcher.json +70 -0
- package/schema/sidebar.json +57 -0
- package/src/fileBrowser/fileBrowser.tsx +75 -1
- package/src/fileBrowser/gitignore.ts +86 -0
- package/src/fileBrowser/index.ts +1 -1
- package/src/git/api.ts +20 -0
- package/src/git/commands.ts +19 -5
- package/src/git/diffWidget.tsx +109 -13
- package/src/git/index.ts +32 -6
- package/src/git/panelComponent.tsx +17 -5
- package/src/index.ts +9 -1
- package/src/launcher/agents.ts +248 -0
- package/src/launcher/availability.ts +73 -0
- package/src/launcher/commands.ts +103 -0
- package/src/launcher/dashboard.tsx +356 -0
- package/src/launcher/icons.ts +123 -0
- package/src/launcher/index.ts +228 -0
- package/src/launcher/invocation.ts +45 -0
- package/src/menus/index.ts +124 -0
- package/src/sidebar/index.ts +195 -0
- package/src/statusBar/index.ts +115 -0
- package/src/statusBar/widget.tsx +318 -0
- package/style/git.css +4 -8
- package/style/index.css +2 -0
- package/style/index.js +2 -0
- package/style/launcher.css +323 -0
- package/style/statusBar.css +95 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import ignore from 'ignore';
|
|
2
|
+
import type { Ignore } from 'ignore';
|
|
3
|
+
|
|
4
|
+
import { Contents } from '@jupyterlab/services';
|
|
5
|
+
|
|
6
|
+
import type { GitStatusEntry } from '@pierre/trees';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The path of the workspace `.gitignore` file. Loaded relative to the
|
|
10
|
+
* Jupyter contents root, which matches the directory the gitignore patterns
|
|
11
|
+
* are evaluated against — patterns and tested paths share the same base.
|
|
12
|
+
*/
|
|
13
|
+
const GITIGNORE_PATH = '.gitignore';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load the workspace `.gitignore` and return an `ignore` matcher built from
|
|
17
|
+
* its contents. Returns `null` when the file is missing, empty, or fails to
|
|
18
|
+
* load — callers should treat any of those as "no ignored entries".
|
|
19
|
+
*/
|
|
20
|
+
export async function loadGitignoreMatcher(
|
|
21
|
+
contentsManager: Contents.IManager
|
|
22
|
+
): Promise<Ignore | null> {
|
|
23
|
+
let model: Contents.IModel;
|
|
24
|
+
try {
|
|
25
|
+
model = await contentsManager.get(GITIGNORE_PATH, {
|
|
26
|
+
content: true,
|
|
27
|
+
format: 'text',
|
|
28
|
+
type: 'file'
|
|
29
|
+
});
|
|
30
|
+
} catch {
|
|
31
|
+
// The most common failure here is `.gitignore` simply not existing.
|
|
32
|
+
// Other transient errors (permission denied, server unreachable) are
|
|
33
|
+
// also treated as "no matcher" so the file browser still works.
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (model.type !== 'file' || typeof model.content !== 'string') {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (model.content.length === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return ignore().add(model.content);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build `GitStatusEntry`s for the canonical paths that match the given
|
|
47
|
+
* gitignore matcher.
|
|
48
|
+
*
|
|
49
|
+
* `@pierre/trees` propagates the `ignored` style from a directory entry
|
|
50
|
+
* down to every descendant via its internal `ignoredDirectoryPaths` set, so
|
|
51
|
+
* we only emit one entry per ignored subtree root: emitting descendants
|
|
52
|
+
* would also pollute the library's `directoriesWithChanges` set, causing
|
|
53
|
+
* parent directories of nested ignored items to render a "contains git
|
|
54
|
+
* status items" dot.
|
|
55
|
+
*
|
|
56
|
+
* Paths are sorted lexicographically so a directory always precedes its
|
|
57
|
+
* descendants, allowing a simple ancestor-suppression check via
|
|
58
|
+
* `path.startsWith(dir)`. The trailing slash on canonical directory paths
|
|
59
|
+
* (required by `@pierre/trees`, also recognized by `ignore` as the
|
|
60
|
+
* "directory" indicator) keeps the prefix check from accidentally matching
|
|
61
|
+
* sibling paths that happen to share a name prefix (e.g. `node_modules_old`
|
|
62
|
+
* vs `node_modules/`).
|
|
63
|
+
*/
|
|
64
|
+
export function buildIgnoredEntries(
|
|
65
|
+
matcher: Ignore,
|
|
66
|
+
canonicalPaths: Iterable<string>
|
|
67
|
+
): GitStatusEntry[] {
|
|
68
|
+
const sorted = Array.from(canonicalPaths).sort();
|
|
69
|
+
const emittedIgnoredDirs: string[] = [];
|
|
70
|
+
const entries: GitStatusEntry[] = [];
|
|
71
|
+
for (const path of sorted) {
|
|
72
|
+
if (path.length === 0) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (emittedIgnoredDirs.some(dir => path.startsWith(dir))) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (matcher.ignores(path)) {
|
|
79
|
+
entries.push({ path, status: 'ignored' });
|
|
80
|
+
if (path.endsWith('/')) {
|
|
81
|
+
emittedIgnoredDirs.push(path);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return entries;
|
|
86
|
+
}
|
package/src/fileBrowser/index.ts
CHANGED
|
@@ -37,7 +37,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
37
37
|
void app.commands.execute('docmanager:open', { path: serverPath });
|
|
38
38
|
}
|
|
39
39
|
});
|
|
40
|
-
app.shell.add(browser, 'left', { rank:
|
|
40
|
+
app.shell.add(browser, 'left', { rank: 50 });
|
|
41
41
|
if (restorer) {
|
|
42
42
|
restorer.add(browser, browser.id);
|
|
43
43
|
}
|
package/src/git/api.ts
CHANGED
|
@@ -145,6 +145,26 @@ export async function checkout(
|
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Expand the porcelain `files` array into one entry per logical change.
|
|
150
|
+
* A single porcelain row may map to two `IFileChange` rows (a file that's
|
|
151
|
+
* both staged and modified again in the worktree); callers that just want
|
|
152
|
+
* a flat list of all changes — without any staged-vs-unstaged grouping —
|
|
153
|
+
* can use this helper.
|
|
154
|
+
*
|
|
155
|
+
* Provided as a manual concat rather than `Array.prototype.flatMap` so we
|
|
156
|
+
* don't pin our `tsconfig.lib` to ES2019.
|
|
157
|
+
*/
|
|
158
|
+
export function expandStatusFiles(files: IGitStatusFile[]): IFileChange[] {
|
|
159
|
+
const result: IFileChange[] = [];
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
for (const change of porcelainToFileChanges(file)) {
|
|
162
|
+
result.push(change);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
148
168
|
/**
|
|
149
169
|
* Translate one entry in the porcelain `files` array into the panel's
|
|
150
170
|
* preferred shape. A single porcelain entry may represent up to two changes
|
package/src/git/commands.ts
CHANGED
|
@@ -5,7 +5,11 @@ import { Contents } from '@jupyterlab/services';
|
|
|
5
5
|
import { refreshIcon } from '@jupyterlab/ui-components';
|
|
6
6
|
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
createGitDiffWidget,
|
|
10
|
+
diffWidgetId,
|
|
11
|
+
updateGitDiffWidget
|
|
12
|
+
} from './diffWidget';
|
|
9
13
|
import { GitPanel } from './panel';
|
|
10
14
|
import { IFileChange } from './tokens';
|
|
11
15
|
|
|
@@ -29,6 +33,7 @@ export namespace CommandArguments {
|
|
|
29
33
|
export interface IOpenDiff {
|
|
30
34
|
repoPath: string;
|
|
31
35
|
change: IFileChange;
|
|
36
|
+
pin?: boolean;
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -65,8 +70,10 @@ export interface IRegisterGitCommandsOptions {
|
|
|
65
70
|
* `undefined` when none is open.
|
|
66
71
|
*/
|
|
67
72
|
findDiff(
|
|
68
|
-
change: IFileChange
|
|
73
|
+
change: IFileChange,
|
|
74
|
+
pin?: boolean
|
|
69
75
|
): ReturnType<typeof createGitDiffWidget> | undefined;
|
|
76
|
+
onPinned(widget: ReturnType<typeof createGitDiffWidget>): void;
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
/**
|
|
@@ -86,7 +93,8 @@ export function registerGitCommands(
|
|
|
86
93
|
rendermime,
|
|
87
94
|
onChanged,
|
|
88
95
|
trackDiff,
|
|
89
|
-
findDiff
|
|
96
|
+
findDiff,
|
|
97
|
+
onPinned
|
|
90
98
|
} = options;
|
|
91
99
|
const { commands } = app;
|
|
92
100
|
|
|
@@ -107,8 +115,12 @@ export function registerGitCommands(
|
|
|
107
115
|
if (typed?.change === undefined) {
|
|
108
116
|
return;
|
|
109
117
|
}
|
|
110
|
-
const
|
|
118
|
+
const pin = typed.pin === true;
|
|
119
|
+
const existing = findDiff(typed.change, pin);
|
|
111
120
|
if (existing !== undefined && !existing.isDisposed) {
|
|
121
|
+
if (!pin) {
|
|
122
|
+
updateGitDiffWidget(existing, typed.change);
|
|
123
|
+
}
|
|
112
124
|
app.shell.activateById(existing.id);
|
|
113
125
|
return;
|
|
114
126
|
}
|
|
@@ -118,7 +130,9 @@ export function registerGitCommands(
|
|
|
118
130
|
themeManager,
|
|
119
131
|
contentsManager,
|
|
120
132
|
rendermime,
|
|
121
|
-
onChanged
|
|
133
|
+
onChanged,
|
|
134
|
+
pinned: pin,
|
|
135
|
+
onPinned
|
|
122
136
|
});
|
|
123
137
|
await trackDiff(widget);
|
|
124
138
|
app.shell.add(widget, 'main', { mode: 'tab-after' });
|
package/src/git/diffWidget.tsx
CHANGED
|
@@ -4,7 +4,12 @@ import { IThemeManager, MainAreaWidget } from '@jupyterlab/apputils';
|
|
|
4
4
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
5
5
|
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
6
6
|
import { Contents } from '@jupyterlab/services';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
ReactWidget,
|
|
9
|
+
ToolbarButton,
|
|
10
|
+
launchIcon,
|
|
11
|
+
undoIcon
|
|
12
|
+
} from '@jupyterlab/ui-components';
|
|
8
13
|
import { ISignal, Signal } from '@lumino/signaling';
|
|
9
14
|
import { FileDiff } from '@pierre/diffs/react';
|
|
10
15
|
import {
|
|
@@ -30,6 +35,12 @@ import { GitReference, IFileChange } from './tokens';
|
|
|
30
35
|
*/
|
|
31
36
|
export const DIFF_WIDGET_CSS_CLASS = 'jp-xtralab-DiffWidget';
|
|
32
37
|
|
|
38
|
+
export const PREVIEW_DIFF_WIDGET_ID = 'xtralab:diff:preview';
|
|
39
|
+
|
|
40
|
+
export function pinnedDiffWidgetId(change: IFileChange): string {
|
|
41
|
+
return `xtralab:diff:pinned:${change.group}:${change.path}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
/**
|
|
34
45
|
* `localStorage` key for the user's last-chosen split ratio. Persisted so
|
|
35
46
|
* the preference survives reloads — the library has no built-in way to
|
|
@@ -143,6 +154,10 @@ export function diffWidgetId(change: IFileChange): string {
|
|
|
143
154
|
return `xtralab:diff:${change.group}:${change.path}`;
|
|
144
155
|
}
|
|
145
156
|
|
|
157
|
+
function toServerPath(repoPath: string, filePath: string): string {
|
|
158
|
+
return repoPath.length > 0 ? PathExt.join(repoPath, filePath) : filePath;
|
|
159
|
+
}
|
|
160
|
+
|
|
146
161
|
/**
|
|
147
162
|
* Resolve the pair of git references whose `content` should appear on the
|
|
148
163
|
* left and right side of the diff for a given file change. Mirrors VS
|
|
@@ -190,6 +205,8 @@ export interface IGitDiffWidgetOptions {
|
|
|
190
205
|
* would just be visual noise.
|
|
191
206
|
*/
|
|
192
207
|
onEmpty?: () => void;
|
|
208
|
+
onPinned?: (widget: MainAreaWidget<DiffContentWidget>) => void;
|
|
209
|
+
pinned?: boolean;
|
|
193
210
|
}
|
|
194
211
|
|
|
195
212
|
/**
|
|
@@ -216,44 +233,88 @@ export function createGitDiffWidget(
|
|
|
216
233
|
}
|
|
217
234
|
});
|
|
218
235
|
widget = new MainAreaWidget<DiffContentWidget>({ content });
|
|
219
|
-
widget.id =
|
|
236
|
+
widget.id =
|
|
237
|
+
options.pinned === true
|
|
238
|
+
? pinnedDiffWidgetId(options.change)
|
|
239
|
+
: PREVIEW_DIFF_WIDGET_ID;
|
|
220
240
|
widget.title.label = formatTitle(options.change);
|
|
221
241
|
widget.title.caption = options.change.path;
|
|
222
242
|
widget.title.closable = true;
|
|
223
243
|
widget.title.icon = getTreeIcon(options.change.path);
|
|
244
|
+
widget.title.className = options.pinned === true ? '' : 'jp-mod-preview';
|
|
245
|
+
const onPinned = options.onPinned ?? (() => {});
|
|
224
246
|
widget.addClass(DIFF_WIDGET_CSS_CLASS);
|
|
225
247
|
// Wire the notebook view-mode selector into the `MainAreaWidget` toolbar
|
|
226
248
|
// — the proper home for document chrome controls. The toolbar starts
|
|
227
249
|
// hidden and becomes visible once the React content reports a parsed
|
|
228
250
|
// notebook diff via `setHasNotebookView`. Plain text / code diffs never
|
|
229
|
-
// flip that flag,
|
|
230
|
-
//
|
|
231
|
-
|
|
251
|
+
// flip that flag, and pin state can hide the toolbar again, so the host
|
|
252
|
+
// chrome tracks the active mode.
|
|
253
|
+
const pinButton = new ToolbarButton({
|
|
254
|
+
icon: launchIcon,
|
|
255
|
+
tooltip: 'Pin tab',
|
|
256
|
+
onClick: () => content.pin()
|
|
257
|
+
});
|
|
258
|
+
if (content.pinned) {
|
|
259
|
+
pinButton.hide();
|
|
260
|
+
}
|
|
261
|
+
widget.toolbar.addItem('pin', pinButton);
|
|
232
262
|
widget.toolbar.addItem(
|
|
233
263
|
'notebook-view-mode',
|
|
234
264
|
new NotebookViewModeToolbarItem(content)
|
|
235
265
|
);
|
|
236
|
-
|
|
237
|
-
const onHasNotebookViewChanged = (
|
|
238
|
-
_sender: DiffContentWidget,
|
|
239
|
-
value: boolean
|
|
240
|
-
): void => {
|
|
266
|
+
const syncToolbarVisibility = (): void => {
|
|
241
267
|
if (widget === null || widget.isDisposed) {
|
|
242
268
|
return;
|
|
243
269
|
}
|
|
244
|
-
if (
|
|
270
|
+
if (!content.pinned || content.hasNotebookView) {
|
|
245
271
|
widget.toolbar.show();
|
|
246
272
|
} else {
|
|
247
273
|
widget.toolbar.hide();
|
|
248
274
|
}
|
|
249
275
|
};
|
|
276
|
+
const onHasNotebookViewChanged = (): void => {
|
|
277
|
+
if (widget === null || widget.isDisposed) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
syncToolbarVisibility();
|
|
281
|
+
};
|
|
282
|
+
const onPinnedChanged = (sender: DiffContentWidget, value: boolean): void => {
|
|
283
|
+
if (widget === null || widget.isDisposed) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
void sender;
|
|
287
|
+
if (value) {
|
|
288
|
+
pinButton.hide();
|
|
289
|
+
// Keep toolbar state and title styling consistent for promoted tabs.
|
|
290
|
+
onPinned(widget);
|
|
291
|
+
if (widget.isDisposed) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
widget.title.className = '';
|
|
295
|
+
}
|
|
296
|
+
syncToolbarVisibility();
|
|
297
|
+
};
|
|
250
298
|
content.hasNotebookViewChanged.connect(onHasNotebookViewChanged);
|
|
299
|
+
content.pinnedChanged.connect(onPinnedChanged);
|
|
300
|
+
syncToolbarVisibility();
|
|
251
301
|
widget.disposed.connect(() => {
|
|
252
302
|
content.hasNotebookViewChanged.disconnect(onHasNotebookViewChanged);
|
|
303
|
+
content.pinnedChanged.disconnect(onPinnedChanged);
|
|
253
304
|
});
|
|
254
305
|
return widget;
|
|
255
306
|
}
|
|
256
307
|
|
|
308
|
+
export function updateGitDiffWidget(
|
|
309
|
+
widget: MainAreaWidget<DiffContentWidget>,
|
|
310
|
+
change: IFileChange
|
|
311
|
+
): void {
|
|
312
|
+
widget.content.setChange(change);
|
|
313
|
+
widget.title.label = formatTitle(change);
|
|
314
|
+
widget.title.caption = change.path;
|
|
315
|
+
widget.title.icon = getTreeIcon(change.path);
|
|
316
|
+
}
|
|
317
|
+
|
|
257
318
|
/**
|
|
258
319
|
* The actual React-rendered region of the diff viewer. Held in its own
|
|
259
320
|
* `ReactWidget` so the surrounding `MainAreaWidget` can manage chrome
|
|
@@ -270,6 +331,7 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
270
331
|
constructor(options: IGitDiffWidgetOptions) {
|
|
271
332
|
super();
|
|
272
333
|
this._options = options;
|
|
334
|
+
this._pinned = options.pinned === true;
|
|
273
335
|
this._notebookViewMode = readStoredNotebookViewMode();
|
|
274
336
|
}
|
|
275
337
|
|
|
@@ -320,6 +382,27 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
320
382
|
return this._hasNotebookViewChanged;
|
|
321
383
|
}
|
|
322
384
|
|
|
385
|
+
get pinned(): boolean {
|
|
386
|
+
return this._pinned;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
pin(): void {
|
|
390
|
+
if (this._pinned) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
this._pinned = true;
|
|
394
|
+
this._pinnedChanged.emit(true);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
get pinnedChanged(): ISignal<this, boolean> {
|
|
398
|
+
return this._pinnedChanged;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
setChange(change: IFileChange): void {
|
|
402
|
+
this._options = { ...this._options, change };
|
|
403
|
+
this.update();
|
|
404
|
+
}
|
|
405
|
+
|
|
323
406
|
protected render(): React.ReactElement {
|
|
324
407
|
return <DiffViewer {...this._options} widget={this} />;
|
|
325
408
|
}
|
|
@@ -331,6 +414,8 @@ export class DiffContentWidget extends ReactWidget {
|
|
|
331
414
|
);
|
|
332
415
|
private _hasNotebookView = false;
|
|
333
416
|
private _hasNotebookViewChanged = new Signal<this, boolean>(this);
|
|
417
|
+
private _pinned: boolean;
|
|
418
|
+
private _pinnedChanged = new Signal<this, boolean>(this);
|
|
334
419
|
}
|
|
335
420
|
|
|
336
421
|
interface IDiffState {
|
|
@@ -505,9 +590,16 @@ function DiffViewer(
|
|
|
505
590
|
|
|
506
591
|
React.useEffect(() => {
|
|
507
592
|
let cancelled = false;
|
|
593
|
+
const serverPath = toServerPath(repoPath, change.path);
|
|
594
|
+
setState({
|
|
595
|
+
loading: true,
|
|
596
|
+
metadata: null,
|
|
597
|
+
notebookDiff: null,
|
|
598
|
+
serverPath,
|
|
599
|
+
isBinary: false,
|
|
600
|
+
error: null
|
|
601
|
+
});
|
|
508
602
|
const { oldRef, newRef } = resolveReferences(change);
|
|
509
|
-
const serverPath =
|
|
510
|
-
repoPath.length > 0 ? PathExt.join(repoPath, change.path) : change.path;
|
|
511
603
|
void (async () => {
|
|
512
604
|
try {
|
|
513
605
|
if (change.isBinary === true) {
|
|
@@ -861,6 +953,10 @@ function NotebookViewModeControl(props: {
|
|
|
861
953
|
content.notebookViewModeChanged.disconnect(handler);
|
|
862
954
|
};
|
|
863
955
|
}, [content]);
|
|
956
|
+
if (!content.hasNotebookView) {
|
|
957
|
+
return <></>;
|
|
958
|
+
}
|
|
959
|
+
|
|
864
960
|
return (
|
|
865
961
|
<div
|
|
866
962
|
className="jp-xtralab-DiffWidget-segmented"
|
package/src/git/index.ts
CHANGED
|
@@ -16,7 +16,11 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
|
16
16
|
import { ReadonlyPartialJSONObject } from '@lumino/coreutils';
|
|
17
17
|
|
|
18
18
|
import { CommandArguments, CommandIDs, registerGitCommands } from './commands';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
DiffContentWidget,
|
|
21
|
+
PREVIEW_DIFF_WIDGET_ID,
|
|
22
|
+
pinnedDiffWidgetId
|
|
23
|
+
} from './diffWidget';
|
|
20
24
|
import type { MainAreaWidget } from '@jupyterlab/apputils';
|
|
21
25
|
import { GitPanel } from './panel';
|
|
22
26
|
import { IFileChange } from './tokens';
|
|
@@ -62,15 +66,22 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
62
66
|
});
|
|
63
67
|
|
|
64
68
|
const findDiff = (
|
|
65
|
-
change: IFileChange
|
|
69
|
+
change: IFileChange,
|
|
70
|
+
pin = false
|
|
66
71
|
): MainAreaWidget<DiffContentWidget> | undefined => {
|
|
67
|
-
const id =
|
|
68
|
-
|
|
72
|
+
const id = pin ? pinnedDiffWidgetId(change) : PREVIEW_DIFF_WIDGET_ID;
|
|
73
|
+
const existing = tracker.find(
|
|
74
|
+
widget => !widget.isDisposed && widget.id === id
|
|
75
|
+
);
|
|
76
|
+
return existing ?? undefined;
|
|
69
77
|
};
|
|
70
78
|
|
|
71
79
|
const panel = new GitPanel({
|
|
72
|
-
openDiff: change => {
|
|
80
|
+
openDiff: (change, options) => {
|
|
73
81
|
const args: CommandArguments.IOpenDiff = { repoPath, change };
|
|
82
|
+
if (options?.pin === true) {
|
|
83
|
+
args.pin = true;
|
|
84
|
+
}
|
|
74
85
|
void app.commands.execute(
|
|
75
86
|
CommandIDs.openDiff,
|
|
76
87
|
args as unknown as ReadonlyPartialJSONObject
|
|
@@ -87,7 +98,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
87
98
|
showError: showErrorMessage
|
|
88
99
|
});
|
|
89
100
|
|
|
90
|
-
app.shell.add(panel, 'left', { rank:
|
|
101
|
+
app.shell.add(panel, 'left', { rank: 60 });
|
|
91
102
|
if (restorer !== null) {
|
|
92
103
|
restorer.add(panel, panel.id);
|
|
93
104
|
}
|
|
@@ -101,6 +112,21 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
101
112
|
rendermime,
|
|
102
113
|
onChanged: () => panel.refresh(),
|
|
103
114
|
trackDiff: widget => tracker.add(widget),
|
|
115
|
+
onPinned: current => {
|
|
116
|
+
const existing = findDiff(current.content.change, true);
|
|
117
|
+
if (
|
|
118
|
+
existing !== undefined &&
|
|
119
|
+
existing !== current &&
|
|
120
|
+
!existing.isDisposed
|
|
121
|
+
) {
|
|
122
|
+
app.shell.activateById(existing.id);
|
|
123
|
+
current.close();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
current.id = pinnedDiffWidgetId(current.content.change);
|
|
127
|
+
current.title.className = '';
|
|
128
|
+
app.shell.activateById(current.id);
|
|
129
|
+
},
|
|
104
130
|
findDiff
|
|
105
131
|
});
|
|
106
132
|
}
|
|
@@ -21,7 +21,12 @@ import { FileChangeStatus, IFileChange, IGitStatusResult } from './tokens';
|
|
|
21
21
|
*/
|
|
22
22
|
export interface IGitPanelHandlers {
|
|
23
23
|
/** Open a diff viewer for the given file in the main area. */
|
|
24
|
-
openDiff(
|
|
24
|
+
openDiff(
|
|
25
|
+
change: IFileChange,
|
|
26
|
+
options?: {
|
|
27
|
+
pin?: boolean;
|
|
28
|
+
}
|
|
29
|
+
): void;
|
|
25
30
|
/** Show a confirmation dialog. Returns true when the user accepted. */
|
|
26
31
|
confirm(title: string, body: string, accept: string): Promise<boolean>;
|
|
27
32
|
/** Show an error dialog. */
|
|
@@ -281,7 +286,11 @@ export function GitPanelComponent(props: {
|
|
|
281
286
|
key={`${change.group}:${change.path}`}
|
|
282
287
|
change={change}
|
|
283
288
|
pendingAction={pendingActions[`${change.group}:${change.path}`]}
|
|
284
|
-
onClick={
|
|
289
|
+
onClick={event =>
|
|
290
|
+
handlers.openDiff(change, {
|
|
291
|
+
pin: event.ctrlKey || event.metaKey
|
|
292
|
+
})
|
|
293
|
+
}
|
|
285
294
|
actions={[
|
|
286
295
|
{
|
|
287
296
|
label: 'Unstage changes',
|
|
@@ -314,7 +323,11 @@ export function GitPanelComponent(props: {
|
|
|
314
323
|
key={`${change.group}:${change.path}`}
|
|
315
324
|
change={change}
|
|
316
325
|
pendingAction={pendingActions[`${change.group}:${change.path}`]}
|
|
317
|
-
onClick={
|
|
326
|
+
onClick={event =>
|
|
327
|
+
handlers.openDiff(change, {
|
|
328
|
+
pin: event.ctrlKey || event.metaKey
|
|
329
|
+
})
|
|
330
|
+
}
|
|
318
331
|
actions={[
|
|
319
332
|
{
|
|
320
333
|
label: 'Discard changes',
|
|
@@ -365,7 +378,6 @@ function PanelHeader(props: {
|
|
|
365
378
|
return (
|
|
366
379
|
<div className="jp-xtralab-GitPanel-header">
|
|
367
380
|
<div className="jp-xtralab-GitPanel-headerInfo">
|
|
368
|
-
<div className="jp-xtralab-GitPanel-title">Source Control</div>
|
|
369
381
|
{repoName !== null ? (
|
|
370
382
|
<div className="jp-xtralab-GitPanel-repo" title={topLevel ?? ''}>
|
|
371
383
|
{repoName}
|
|
@@ -442,7 +454,7 @@ function ChangeSection(props: {
|
|
|
442
454
|
function ChangeRow(props: {
|
|
443
455
|
change: IFileChange;
|
|
444
456
|
pendingAction: string | undefined;
|
|
445
|
-
onClick: () => void;
|
|
457
|
+
onClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
446
458
|
actions: IRowAction[];
|
|
447
459
|
}): React.ReactElement {
|
|
448
460
|
const { change, pendingAction, onClick, actions } = props;
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
|
|
|
2
2
|
|
|
3
3
|
import fileBrowserPlugin from './fileBrowser';
|
|
4
4
|
import gitPlugin from './git';
|
|
5
|
+
import launcherPlugin from './launcher';
|
|
6
|
+
import menusPlugin from './menus';
|
|
7
|
+
import sidebarPlugin from './sidebar';
|
|
8
|
+
import statusBarPlugin from './statusBar';
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* Every plugin contributed by `xtralab`. The entry point of the
|
|
@@ -12,7 +16,11 @@ import gitPlugin from './git';
|
|
|
12
16
|
*/
|
|
13
17
|
const plugins: JupyterFrontEndPlugin<unknown>[] = [
|
|
14
18
|
fileBrowserPlugin,
|
|
15
|
-
gitPlugin
|
|
19
|
+
gitPlugin,
|
|
20
|
+
launcherPlugin,
|
|
21
|
+
menusPlugin,
|
|
22
|
+
sidebarPlugin,
|
|
23
|
+
statusBarPlugin
|
|
16
24
|
];
|
|
17
25
|
|
|
18
26
|
export default plugins;
|