xtralab 0.1.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/LICENSE +28 -0
- package/README.md +69 -0
- package/lib/fileBrowser/commands.d.ts +62 -0
- package/lib/fileBrowser/commands.js +418 -0
- package/lib/fileBrowser/contents.d.ts +28 -0
- package/lib/fileBrowser/contents.js +48 -0
- package/lib/fileBrowser/fileBrowser.d.ts +24 -0
- package/lib/fileBrowser/fileBrowser.js +535 -0
- package/lib/fileBrowser/icons.d.ts +37 -0
- package/lib/fileBrowser/icons.js +153 -0
- package/lib/fileBrowser/index.d.ts +7 -0
- package/lib/fileBrowser/index.js +38 -0
- package/lib/fileBrowser/widget.d.ts +97 -0
- package/lib/fileBrowser/widget.js +95 -0
- package/lib/git/api.d.ts +39 -0
- package/lib/git/api.js +164 -0
- package/lib/git/commands.d.ts +69 -0
- package/lib/git/commands.js +67 -0
- package/lib/git/diffWidget.d.ts +108 -0
- package/lib/git/diffWidget.js +624 -0
- package/lib/git/icons.d.ts +14 -0
- package/lib/git/icons.js +19 -0
- package/lib/git/index.d.ts +11 -0
- package/lib/git/index.js +74 -0
- package/lib/git/notebookDiff.d.ts +176 -0
- package/lib/git/notebookDiff.js +805 -0
- package/lib/git/panel.d.ts +29 -0
- package/lib/git/panel.js +42 -0
- package/lib/git/panelComponent.d.ts +26 -0
- package/lib/git/panelComponent.js +300 -0
- package/lib/git/tokens.d.ts +78 -0
- package/lib/git/tokens.js +6 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +14 -0
- package/package.json +216 -0
- package/schema/plugin.json +67 -0
- package/src/fileBrowser/commands.ts +507 -0
- package/src/fileBrowser/contents.ts +66 -0
- package/src/fileBrowser/fileBrowser.tsx +635 -0
- package/src/fileBrowser/icons.ts +172 -0
- package/src/fileBrowser/index.ts +56 -0
- package/src/fileBrowser/widget.tsx +202 -0
- package/src/git/api.ts +207 -0
- package/src/git/commands.ts +143 -0
- package/src/git/diffWidget.tsx +903 -0
- package/src/git/icons.ts +20 -0
- package/src/git/index.ts +109 -0
- package/src/git/notebookDiff.tsx +1294 -0
- package/src/git/panel.tsx +56 -0
- package/src/git/panelComponent.tsx +558 -0
- package/src/git/tokens.ts +91 -0
- package/src/index.ts +18 -0
- package/style/base.css +175 -0
- package/style/git.css +770 -0
- package/style/index.css +2 -0
- package/style/index.js +2 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import { LabIcon, fileIcon } from '@jupyterlab/ui-components';
|
|
3
|
+
import { createFileTreeIconResolver, getBuiltInFileIconColor, getBuiltInSpriteSheet } from '@pierre/trees';
|
|
4
|
+
/**
|
|
5
|
+
* The glyph used as the Xtralab file browser's sidebar tab icon. Defined as a
|
|
6
|
+
* `LabIcon` (rather than the default `folderIcon` from
|
|
7
|
+
* `@jupyterlab/ui-components`) so the Xtralab browser is visually distinguishable
|
|
8
|
+
* from JupyterLab's built-in file browser, which also lives in the left
|
|
9
|
+
* sidebar.
|
|
10
|
+
*
|
|
11
|
+
* Source: `file-tree` from SVG Repo (https://www.svgrepo.com/svg/371275/file-tree).
|
|
12
|
+
* The original `fill="#444"` is replaced with `fill="currentColor"` and the
|
|
13
|
+
* root element is given the `jp-icon3` class so the icon picks up JupyterLab's
|
|
14
|
+
* theme color and the sidebar's inverse-color painting automatically.
|
|
15
|
+
*/
|
|
16
|
+
export const xtralabFileBrowserIcon = new LabIcon({
|
|
17
|
+
name: 'xtralab:file-browser',
|
|
18
|
+
svgstr: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="jp-icon3" fill="currentColor">
|
|
19
|
+
<path d="M16 10v-4h-11v1h-2v-3h9v-4h-12v4h2v10h3v2h11v-4h-11v1h-2v-5h2v2z"/>
|
|
20
|
+
</svg>`
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Sprite sheet injected into the `@pierre/trees` shadow DOM. Contains a single
|
|
24
|
+
* `<symbol>` for the Jupyter notebook icon, drawn from the JupyterLab core
|
|
25
|
+
* notebook icon (`@jupyterlab/ui-components/style/icons/filetype/notebook.svg`).
|
|
26
|
+
*
|
|
27
|
+
* `@pierre/trees` does not ship a Jupyter token, so `.ipynb` files would
|
|
28
|
+
* otherwise fall through to the generic file icon. The symbol id below is
|
|
29
|
+
* referenced from `byFileExtension` in {@link FILE_BROWSER_ICONS}.
|
|
30
|
+
*
|
|
31
|
+
* The orange `#EF6C00` fill is set as a presentation attribute on the inner
|
|
32
|
+
* `<g>` so it survives the `fill: currentColor` rule the tree applies on the
|
|
33
|
+
* icon container — the inherited color does not override a presentation
|
|
34
|
+
* attribute set directly on a child.
|
|
35
|
+
*/
|
|
36
|
+
const JUPYTER_NOTEBOOK_SPRITE_SHEET = `<svg data-icon-sprite aria-hidden="true" width="0" height="0">
|
|
37
|
+
<symbol id="jupyter-notebook" viewBox="0 0 22 22">
|
|
38
|
+
<g fill="#EF6C00">
|
|
39
|
+
<path d="M18.7 3.3v15.4H3.3V3.3zm1.5-1.5H1.8v18.3h18.3z"/>
|
|
40
|
+
<path d="m16.5 16.5-5.4-4.3-5.6 4.3v-11h11z"/>
|
|
41
|
+
</g>
|
|
42
|
+
</symbol>
|
|
43
|
+
</svg>`;
|
|
44
|
+
/**
|
|
45
|
+
* Icon configuration for the Xtralab file browser. Keeps the default `complete`
|
|
46
|
+
* built-in icon set (the colored language icons) and layers a Jupyter notebook
|
|
47
|
+
* symbol on top so `.ipynb` files render with the JupyterLab notebook glyph.
|
|
48
|
+
*
|
|
49
|
+
* `set` must be set explicitly: when a config object includes any custom
|
|
50
|
+
* overrides without `set`, `@pierre/trees` defaults `set` to `'none'`, which
|
|
51
|
+
* disables the built-in icons entirely.
|
|
52
|
+
*/
|
|
53
|
+
export const FILE_BROWSER_ICONS = {
|
|
54
|
+
set: 'complete',
|
|
55
|
+
colored: true,
|
|
56
|
+
spriteSheet: JUPYTER_NOTEBOOK_SPRITE_SHEET,
|
|
57
|
+
byFileExtension: {
|
|
58
|
+
ipynb: 'jupyter-notebook'
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Concatenated sprite-sheet text. Combines the built-in sheet for the active
|
|
63
|
+
* icon set with our custom Jupyter notebook sheet so {@link extractSymbol}
|
|
64
|
+
* can find the body of any symbol the tree itself can render.
|
|
65
|
+
*/
|
|
66
|
+
const COMBINED_SPRITE_SHEETS = [
|
|
67
|
+
getBuiltInSpriteSheet((_a = FILE_BROWSER_ICONS.set) !== null && _a !== void 0 ? _a : 'complete'),
|
|
68
|
+
JUPYTER_NOTEBOOK_SPRITE_SHEET
|
|
69
|
+
].join('\n');
|
|
70
|
+
/**
|
|
71
|
+
* Resolver instance reused across calls to {@link getTreeIcon}. The resolver
|
|
72
|
+
* is pure relative to the icon config, so building it once at module load
|
|
73
|
+
* avoids re-walking the icon rules on every diff that opens.
|
|
74
|
+
*/
|
|
75
|
+
const treeIconResolver = createFileTreeIconResolver(FILE_BROWSER_ICONS);
|
|
76
|
+
/**
|
|
77
|
+
* Cache of LabIcons keyed by the resolved sprite symbol id (e.g.
|
|
78
|
+
* `file-tree-builtin-python`). All Python files share the same icon, so
|
|
79
|
+
* caching by symbol id prevents repeated LabIcon registration warnings and
|
|
80
|
+
* keeps the JupyterLab icon registry small.
|
|
81
|
+
*/
|
|
82
|
+
const TREE_ICON_CACHE = new Map();
|
|
83
|
+
/**
|
|
84
|
+
* Extract the inner SVG markup and viewBox of the `<symbol>` with the given
|
|
85
|
+
* id from {@link COMBINED_SPRITE_SHEETS}. Returns `null` if no matching
|
|
86
|
+
* symbol exists, in which case the caller falls back to JupyterLab's default
|
|
87
|
+
* file icon.
|
|
88
|
+
*/
|
|
89
|
+
function extractSymbol(symbolId) {
|
|
90
|
+
var _a;
|
|
91
|
+
// Match a `<symbol>` whose `id` attribute equals `symbolId` (allowing other
|
|
92
|
+
// attributes in any order). The `[\s\S]*?` body is non-greedy so we stop
|
|
93
|
+
// at the first matching `</symbol>` even when multiple symbols sit
|
|
94
|
+
// back-to-back in the sheet.
|
|
95
|
+
const escaped = symbolId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
96
|
+
const symbolRegex = new RegExp(`<symbol\\b[^>]*\\bid="${escaped}"[^>]*>([\\s\\S]*?)</symbol>`, 'i');
|
|
97
|
+
const match = symbolRegex.exec(COMBINED_SPRITE_SHEETS);
|
|
98
|
+
if (match === null) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const viewBoxMatch = /\bviewBox="([^"]*)"/i.exec(match[0]);
|
|
102
|
+
return {
|
|
103
|
+
viewBox: (_a = viewBoxMatch === null || viewBoxMatch === void 0 ? void 0 : viewBoxMatch[1]) !== null && _a !== void 0 ? _a : '0 0 16 16',
|
|
104
|
+
inner: match[1]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Return a JupyterLab `LabIcon` matching the icon the Xtralab file tree would
|
|
109
|
+
* render for the given file path. Used by other plugins (e.g. the git diff
|
|
110
|
+
* viewer) that want their main-area widget tabs to display the same per-file
|
|
111
|
+
* glyph the tree shows in the sidebar.
|
|
112
|
+
*
|
|
113
|
+
* Resolves the symbol via the same `@pierre/trees` resolver the tree itself
|
|
114
|
+
* uses, then inlines the symbol's body into a self-contained SVG so the icon
|
|
115
|
+
* works outside the tree's shadow DOM. Falls back to JupyterLab's default
|
|
116
|
+
* `fileIcon` when the sprite cannot be located in either sheet.
|
|
117
|
+
*/
|
|
118
|
+
export function getTreeIcon(filePath) {
|
|
119
|
+
var _a;
|
|
120
|
+
const resolved = treeIconResolver.resolveIcon('file-tree-icon-file', filePath);
|
|
121
|
+
const cached = TREE_ICON_CACHE.get(resolved.name);
|
|
122
|
+
if (cached !== undefined) {
|
|
123
|
+
return cached;
|
|
124
|
+
}
|
|
125
|
+
const symbol = extractSymbol(resolved.name);
|
|
126
|
+
if (symbol === null) {
|
|
127
|
+
return fileIcon;
|
|
128
|
+
}
|
|
129
|
+
// Built-in language icons paint with `fill="currentColor"` so the colored
|
|
130
|
+
// tier of the tree can recolor them by setting `color` on the host
|
|
131
|
+
// element. Reproduce that here by setting the same color value as an
|
|
132
|
+
// inline `style="color: …"` on the outer `<svg>`. `getBuiltInFileIconColor`
|
|
133
|
+
// returns a `var(--trees-…)` chain that bottoms out in `light-dark(…)`,
|
|
134
|
+
// which respects the page's `color-scheme` declaration so dark themes pick
|
|
135
|
+
// up the brighter variants automatically.
|
|
136
|
+
const colored = FILE_BROWSER_ICONS.colored === true;
|
|
137
|
+
const color = colored && resolved.token !== undefined
|
|
138
|
+
? getBuiltInFileIconColor(resolved.token)
|
|
139
|
+
: undefined;
|
|
140
|
+
const styleAttr = color !== undefined ? ` style="color: ${color};"` : '';
|
|
141
|
+
// Prefer a viewBox the resolver supplies (custom sprite overrides may set
|
|
142
|
+
// one), then fall back to the symbol's own viewBox so 22x22 sprites like
|
|
143
|
+
// the Jupyter notebook glyph render at the right scale rather than being
|
|
144
|
+
// cropped to a 16x16 box.
|
|
145
|
+
const viewBox = (_a = resolved.viewBox) !== null && _a !== void 0 ? _a : symbol.viewBox;
|
|
146
|
+
const svgstr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}"${styleAttr}>${symbol.inner}</svg>`;
|
|
147
|
+
const icon = new LabIcon({
|
|
148
|
+
name: `xtralab:tree-icon-${resolved.name}`,
|
|
149
|
+
svgstr
|
|
150
|
+
});
|
|
151
|
+
TREE_ICON_CACHE.set(resolved.name, icon);
|
|
152
|
+
return icon;
|
|
153
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ILayoutRestorer } from '@jupyterlab/application';
|
|
2
|
+
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
3
|
+
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
4
|
+
import { populateToolbar, registerCommands } from './commands';
|
|
5
|
+
import { XtralabFileBrowser } from './widget';
|
|
6
|
+
const PLUGIN_ID = 'xtralab:plugin';
|
|
7
|
+
/**
|
|
8
|
+
* The file browser plugin. Adds a `@pierre/trees`-powered file browser to the
|
|
9
|
+
* JupyterLab left sidebar.
|
|
10
|
+
*/
|
|
11
|
+
const plugin = {
|
|
12
|
+
id: PLUGIN_ID,
|
|
13
|
+
description: 'A path-first file browser for JupyterLab built on @pierre/trees.',
|
|
14
|
+
autoStart: true,
|
|
15
|
+
requires: [IDocumentManager],
|
|
16
|
+
optional: [ILayoutRestorer, ISettingRegistry],
|
|
17
|
+
activate: (app, docManager, restorer, settingRegistry) => {
|
|
18
|
+
const browser = new XtralabFileBrowser({
|
|
19
|
+
contentsManager: app.serviceManager.contents,
|
|
20
|
+
docManager,
|
|
21
|
+
onOpenFile: (serverPath) => {
|
|
22
|
+
void app.commands.execute('docmanager:open', { path: serverPath });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
app.shell.add(browser, 'left', { rank: 700 });
|
|
26
|
+
if (restorer) {
|
|
27
|
+
restorer.add(browser, browser.id);
|
|
28
|
+
}
|
|
29
|
+
registerCommands({ app, browser });
|
|
30
|
+
populateToolbar({ app, browser });
|
|
31
|
+
if (settingRegistry) {
|
|
32
|
+
settingRegistry.load(PLUGIN_ID).catch(reason => {
|
|
33
|
+
console.error('Failed to load settings for xtralab.', reason);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
export default plugin;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
2
|
+
import { Contents } from '@jupyterlab/services';
|
|
3
|
+
import { Toolbar } from '@jupyterlab/ui-components';
|
|
4
|
+
import { ISignal } from '@lumino/signaling';
|
|
5
|
+
import { Widget } from '@lumino/widgets';
|
|
6
|
+
export declare const FILE_BROWSER_ID = "xtralab:file-browser";
|
|
7
|
+
/**
|
|
8
|
+
* The CSS class added to the Xtralab file browser widget. The selectors that
|
|
9
|
+
* bind application context menu items hang off this class, so it must remain
|
|
10
|
+
* specific enough not to clash with other browsers (such as the default
|
|
11
|
+
* `jp-FileBrowser`).
|
|
12
|
+
*/
|
|
13
|
+
export declare const FILE_BROWSER_CSS_CLASS = "jp-xtralab-FileBrowser";
|
|
14
|
+
export interface IXtralabFileBrowserOptions {
|
|
15
|
+
contentsManager: Contents.IManager;
|
|
16
|
+
docManager: IDocumentManager;
|
|
17
|
+
onOpenFile?: (serverPath: string) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Public API of the Xtralab file browser used by command handlers and other
|
|
21
|
+
* collaborators. Kept narrow on purpose so the React component stays free to
|
|
22
|
+
* evolve the underlying tree integration.
|
|
23
|
+
*/
|
|
24
|
+
export interface IXtralabFileBrowser {
|
|
25
|
+
/**
|
|
26
|
+
* The contents manager backing the tree. Exposed so command handlers can
|
|
27
|
+
* issue contents operations (rename, delete, copy, …) against the same
|
|
28
|
+
* drive the tree was loaded from.
|
|
29
|
+
*/
|
|
30
|
+
readonly contentsManager: Contents.IManager;
|
|
31
|
+
/**
|
|
32
|
+
* The toolbar shown above the tree. Plugin code populates it with command
|
|
33
|
+
* buttons after the widget is constructed.
|
|
34
|
+
*/
|
|
35
|
+
readonly toolbar: Toolbar;
|
|
36
|
+
/**
|
|
37
|
+
* Canonical paths of the items currently selected in the tree. Folder
|
|
38
|
+
* paths carry a trailing slash; file paths do not.
|
|
39
|
+
*/
|
|
40
|
+
readonly selectedPaths: readonly string[];
|
|
41
|
+
/**
|
|
42
|
+
* Emits when the tree's selection changes. Commands that have an
|
|
43
|
+
* `isVisible` / `isEnabled` predicate should listen so they re-evaluate.
|
|
44
|
+
*/
|
|
45
|
+
readonly selectionChanged: ISignal<IXtralabFileBrowser, readonly string[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Emits when {@link refresh} is called. The React component listens and
|
|
48
|
+
* re-fetches the directories that were previously loaded so the tree
|
|
49
|
+
* matches the contents on disk again.
|
|
50
|
+
*/
|
|
51
|
+
readonly refreshRequested: ISignal<IXtralabFileBrowser, void>;
|
|
52
|
+
/**
|
|
53
|
+
* Emits when the widget is asked to surface a newly-created path
|
|
54
|
+
* (for example, after the toolbar's "new folder" command). The React
|
|
55
|
+
* component listens and inserts the path into the model so the user
|
|
56
|
+
* sees the new item without a full refresh.
|
|
57
|
+
*/
|
|
58
|
+
readonly pathAdded: ISignal<IXtralabFileBrowser, string>;
|
|
59
|
+
/** Trigger a refresh of every loaded directory in the tree. */
|
|
60
|
+
refresh(): void;
|
|
61
|
+
/**
|
|
62
|
+
* Notify the React component that a new path was created and should
|
|
63
|
+
* appear in the tree. `canonicalPath` follows the `@pierre/trees`
|
|
64
|
+
* convention: directories carry a trailing slash, files do not.
|
|
65
|
+
*/
|
|
66
|
+
notifyPathAdded(canonicalPath: string): void;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Lumino widget that hosts the React-based `@pierre/trees` file browser.
|
|
70
|
+
* The widget is laid out with a JupyterLab `Toolbar` on top and the tree
|
|
71
|
+
* filling the remaining space.
|
|
72
|
+
*/
|
|
73
|
+
export declare class XtralabFileBrowser extends Widget implements IXtralabFileBrowser {
|
|
74
|
+
constructor(options: IXtralabFileBrowserOptions);
|
|
75
|
+
get contentsManager(): Contents.IManager;
|
|
76
|
+
get toolbar(): Toolbar;
|
|
77
|
+
get selectedPaths(): readonly string[];
|
|
78
|
+
get selectionChanged(): ISignal<this, readonly string[]>;
|
|
79
|
+
get refreshRequested(): ISignal<this, void>;
|
|
80
|
+
get pathAdded(): ISignal<this, string>;
|
|
81
|
+
/**
|
|
82
|
+
* Update the cached selection. Called from the React tree when the
|
|
83
|
+
* underlying `@pierre/trees` model emits a selection change.
|
|
84
|
+
*/
|
|
85
|
+
updateSelection(paths: readonly string[]): void;
|
|
86
|
+
refresh(): void;
|
|
87
|
+
notifyPathAdded(canonicalPath: string): void;
|
|
88
|
+
private _contentsManager;
|
|
89
|
+
private _docManager;
|
|
90
|
+
private _onOpenFile;
|
|
91
|
+
private _selectedPaths;
|
|
92
|
+
private _selectionChanged;
|
|
93
|
+
private _refreshRequested;
|
|
94
|
+
private _pathAdded;
|
|
95
|
+
private _toolbar;
|
|
96
|
+
private _content;
|
|
97
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ReactWidget, Toolbar } from '@jupyterlab/ui-components';
|
|
3
|
+
import { Signal } from '@lumino/signaling';
|
|
4
|
+
import { PanelLayout, Widget } from '@lumino/widgets';
|
|
5
|
+
import { FileBrowserComponent } from './fileBrowser';
|
|
6
|
+
import { xtralabFileBrowserIcon } from './icons';
|
|
7
|
+
export const FILE_BROWSER_ID = 'xtralab:file-browser';
|
|
8
|
+
/**
|
|
9
|
+
* The CSS class added to the Xtralab file browser widget. The selectors that
|
|
10
|
+
* bind application context menu items hang off this class, so it must remain
|
|
11
|
+
* specific enough not to clash with other browsers (such as the default
|
|
12
|
+
* `jp-FileBrowser`).
|
|
13
|
+
*/
|
|
14
|
+
export const FILE_BROWSER_CSS_CLASS = 'jp-xtralab-FileBrowser';
|
|
15
|
+
const TOOLBAR_CSS_CLASS = 'jp-xtralab-FileBrowser-toolbar';
|
|
16
|
+
const CONTENT_CSS_CLASS = 'jp-xtralab-FileBrowser-content';
|
|
17
|
+
/**
|
|
18
|
+
* Lumino widget that hosts the React-based `@pierre/trees` file browser.
|
|
19
|
+
* The widget is laid out with a JupyterLab `Toolbar` on top and the tree
|
|
20
|
+
* filling the remaining space.
|
|
21
|
+
*/
|
|
22
|
+
export class XtralabFileBrowser extends Widget {
|
|
23
|
+
constructor(options) {
|
|
24
|
+
super();
|
|
25
|
+
this._selectedPaths = [];
|
|
26
|
+
this._selectionChanged = new Signal(this);
|
|
27
|
+
this._refreshRequested = new Signal(this);
|
|
28
|
+
this._pathAdded = new Signal(this);
|
|
29
|
+
const layout = (this.layout = new PanelLayout());
|
|
30
|
+
this._contentsManager = options.contentsManager;
|
|
31
|
+
this._docManager = options.docManager;
|
|
32
|
+
this._onOpenFile = options.onOpenFile;
|
|
33
|
+
this.id = FILE_BROWSER_ID;
|
|
34
|
+
this.title.icon = xtralabFileBrowserIcon;
|
|
35
|
+
this.title.caption = 'Xtralab File Browser';
|
|
36
|
+
this.addClass(FILE_BROWSER_CSS_CLASS);
|
|
37
|
+
this._toolbar = new Toolbar();
|
|
38
|
+
this._toolbar.addClass(TOOLBAR_CSS_CLASS);
|
|
39
|
+
this._toolbar.node.setAttribute('aria-label', 'file browser');
|
|
40
|
+
layout.addWidget(this._toolbar);
|
|
41
|
+
this._content = new XtralabFileTreeContent({
|
|
42
|
+
contentsManager: this._contentsManager,
|
|
43
|
+
docManager: this._docManager,
|
|
44
|
+
onOpenFile: this._onOpenFile,
|
|
45
|
+
browser: this
|
|
46
|
+
});
|
|
47
|
+
this._content.addClass(CONTENT_CSS_CLASS);
|
|
48
|
+
layout.addWidget(this._content);
|
|
49
|
+
}
|
|
50
|
+
get contentsManager() {
|
|
51
|
+
return this._contentsManager;
|
|
52
|
+
}
|
|
53
|
+
get toolbar() {
|
|
54
|
+
return this._toolbar;
|
|
55
|
+
}
|
|
56
|
+
get selectedPaths() {
|
|
57
|
+
return this._selectedPaths;
|
|
58
|
+
}
|
|
59
|
+
get selectionChanged() {
|
|
60
|
+
return this._selectionChanged;
|
|
61
|
+
}
|
|
62
|
+
get refreshRequested() {
|
|
63
|
+
return this._refreshRequested;
|
|
64
|
+
}
|
|
65
|
+
get pathAdded() {
|
|
66
|
+
return this._pathAdded;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Update the cached selection. Called from the React tree when the
|
|
70
|
+
* underlying `@pierre/trees` model emits a selection change.
|
|
71
|
+
*/
|
|
72
|
+
updateSelection(paths) {
|
|
73
|
+
this._selectedPaths = paths;
|
|
74
|
+
this._selectionChanged.emit(paths);
|
|
75
|
+
}
|
|
76
|
+
refresh() {
|
|
77
|
+
this._refreshRequested.emit();
|
|
78
|
+
}
|
|
79
|
+
notifyPathAdded(canonicalPath) {
|
|
80
|
+
this._pathAdded.emit(canonicalPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The React-rendered region of the file browser. Lives inside the host
|
|
85
|
+
* widget below the toolbar and renders the actual tree.
|
|
86
|
+
*/
|
|
87
|
+
class XtralabFileTreeContent extends ReactWidget {
|
|
88
|
+
constructor(options) {
|
|
89
|
+
super();
|
|
90
|
+
this._options = options;
|
|
91
|
+
}
|
|
92
|
+
render() {
|
|
93
|
+
return (React.createElement(FileBrowserComponent, { contentsManager: this._options.contentsManager, docManager: this._options.docManager, onOpenFile: this._options.onOpenFile, widget: this._options.browser }));
|
|
94
|
+
}
|
|
95
|
+
}
|
package/lib/git/api.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { GitReference, IFileChange, IGitContentResult, IGitStatusFile, IGitStatusResult } from './tokens';
|
|
2
|
+
/**
|
|
3
|
+
* Fetch the absolute filesystem path of the git top-level for the given
|
|
4
|
+
* server-relative path. Returns `null` when the path is not in a git
|
|
5
|
+
* repository — the server returns a non-zero `code` (or `path: null`) in
|
|
6
|
+
* that case, which is expected and not a fatal error.
|
|
7
|
+
*/
|
|
8
|
+
export declare function showTopLevel(repoPath: string): Promise<string | null>;
|
|
9
|
+
/**
|
|
10
|
+
* Fetch `git status --porcelain` for the repository the server resolves at
|
|
11
|
+
* `repoPath`. When `repoPath` is the empty string, git resolves the repo
|
|
12
|
+
* from the JupyterLab server's root directory.
|
|
13
|
+
*/
|
|
14
|
+
export declare function status(repoPath: string): Promise<IGitStatusResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Fetch a file's contents at a particular git reference. `filename` is the
|
|
17
|
+
* file's path relative to the git repository root (matching the `to` field
|
|
18
|
+
* of {@link status}'s response).
|
|
19
|
+
*/
|
|
20
|
+
export declare function content(repoPath: string, filename: string, reference: GitReference): Promise<IGitContentResult>;
|
|
21
|
+
/** Stage a single file. */
|
|
22
|
+
export declare function add(repoPath: string, filename: string): Promise<void>;
|
|
23
|
+
/** Stage every changed file (including untracked). */
|
|
24
|
+
export declare function addAll(repoPath: string): Promise<void>;
|
|
25
|
+
/** Unstage a single file. */
|
|
26
|
+
export declare function reset(repoPath: string, filename: string): Promise<void>;
|
|
27
|
+
/** Unstage every file. */
|
|
28
|
+
export declare function resetAll(repoPath: string): Promise<void>;
|
|
29
|
+
/** Restore a single file to the version in the index (i.e. discard worktree changes). */
|
|
30
|
+
export declare function checkout(repoPath: string, filename: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Translate one entry in the porcelain `files` array into the panel's
|
|
33
|
+
* preferred shape. A single porcelain entry may represent up to two changes
|
|
34
|
+
* (one in the index, one in the worktree); we emit one {@link IFileChange}
|
|
35
|
+
* per non-empty side, so a "modified-staged-and-then-modified-again" file
|
|
36
|
+
* shows up twice in the panel — once under "Staged Changes" and once under
|
|
37
|
+
* "Changes" — matching the layout the user sees in VS Code.
|
|
38
|
+
*/
|
|
39
|
+
export declare function porcelainToFileChanges(file: IGitStatusFile): IFileChange[];
|
package/lib/git/api.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { URLExt } from '@jupyterlab/coreutils';
|
|
2
|
+
import { ServerConnection } from '@jupyterlab/services';
|
|
3
|
+
/**
|
|
4
|
+
* Root URL segment used by the `jupyterlab_git` server extension. Every
|
|
5
|
+
* endpoint hangs off this prefix; the `<repoPath>` between it and the
|
|
6
|
+
* endpoint suffix is the JupyterLab server-relative path of the git
|
|
7
|
+
* repository (URL-encoded). For an empty `repoPath` the URL collapses to
|
|
8
|
+
* `/git/<endpoint>` — a shape git's URL routing also accepts.
|
|
9
|
+
*/
|
|
10
|
+
const GIT_NAMESPACE = 'git';
|
|
11
|
+
/**
|
|
12
|
+
* Issue a `POST` against a `jupyterlab_git` endpoint that takes a `<path>`
|
|
13
|
+
* prefix (e.g. `/git/<path>/status`). The response body is decoded as JSON;
|
|
14
|
+
* non-2xx responses are surfaced via the JupyterLab `ServerConnection`
|
|
15
|
+
* machinery so callers can show their `message` field as-is.
|
|
16
|
+
*/
|
|
17
|
+
async function postWithPath(endpoint, repoPath, body) {
|
|
18
|
+
var _a, _b;
|
|
19
|
+
const settings = ServerConnection.makeSettings();
|
|
20
|
+
// `URLExt.encodeParts` encodes each path segment but preserves slashes
|
|
21
|
+
// between them — the server expects the path to look like a posix path.
|
|
22
|
+
const encodedPath = repoPath.length > 0 ? URLExt.encodeParts(repoPath) : '';
|
|
23
|
+
const url = URLExt.join(settings.baseUrl, GIT_NAMESPACE, encodedPath, endpoint);
|
|
24
|
+
const init = {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
body: JSON.stringify(body !== null && body !== void 0 ? body : {})
|
|
27
|
+
};
|
|
28
|
+
const response = await ServerConnection.makeRequest(url, init, settings);
|
|
29
|
+
const text = await response.text();
|
|
30
|
+
let data;
|
|
31
|
+
if (text.length > 0) {
|
|
32
|
+
try {
|
|
33
|
+
data = JSON.parse(text);
|
|
34
|
+
}
|
|
35
|
+
catch (_err) {
|
|
36
|
+
// Fall through into the error handling below.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const message = (_b = (_a = data === null || data === void 0 ? void 0 : data.message) !== null && _a !== void 0 ? _a : text) !== null && _b !== void 0 ? _b : '';
|
|
41
|
+
throw new ServerConnection.ResponseError(response, message);
|
|
42
|
+
}
|
|
43
|
+
return (data !== null && data !== void 0 ? data : {});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Fetch the absolute filesystem path of the git top-level for the given
|
|
47
|
+
* server-relative path. Returns `null` when the path is not in a git
|
|
48
|
+
* repository — the server returns a non-zero `code` (or `path: null`) in
|
|
49
|
+
* that case, which is expected and not a fatal error.
|
|
50
|
+
*/
|
|
51
|
+
export async function showTopLevel(repoPath) {
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
result = await postWithPath('show_top_level', repoPath, {});
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (result.code !== 0 || !result.path) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return result.path;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Fetch `git status --porcelain` for the repository the server resolves at
|
|
66
|
+
* `repoPath`. When `repoPath` is the empty string, git resolves the repo
|
|
67
|
+
* from the JupyterLab server's root directory.
|
|
68
|
+
*/
|
|
69
|
+
export async function status(repoPath) {
|
|
70
|
+
return postWithPath('status', repoPath, {});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Fetch a file's contents at a particular git reference. `filename` is the
|
|
74
|
+
* file's path relative to the git repository root (matching the `to` field
|
|
75
|
+
* of {@link status}'s response).
|
|
76
|
+
*/
|
|
77
|
+
export async function content(repoPath, filename, reference) {
|
|
78
|
+
return postWithPath('content', repoPath, {
|
|
79
|
+
filename,
|
|
80
|
+
reference
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Stage a single file. */
|
|
84
|
+
export async function add(repoPath, filename) {
|
|
85
|
+
await postWithPath('add', repoPath, { add_all: false, filename });
|
|
86
|
+
}
|
|
87
|
+
/** Stage every changed file (including untracked). */
|
|
88
|
+
export async function addAll(repoPath) {
|
|
89
|
+
await postWithPath('add', repoPath, { add_all: true });
|
|
90
|
+
}
|
|
91
|
+
/** Unstage a single file. */
|
|
92
|
+
export async function reset(repoPath, filename) {
|
|
93
|
+
await postWithPath('reset', repoPath, { reset_all: false, filename });
|
|
94
|
+
}
|
|
95
|
+
/** Unstage every file. */
|
|
96
|
+
export async function resetAll(repoPath) {
|
|
97
|
+
await postWithPath('reset', repoPath, { reset_all: true });
|
|
98
|
+
}
|
|
99
|
+
/** Restore a single file to the version in the index (i.e. discard worktree changes). */
|
|
100
|
+
export async function checkout(repoPath, filename) {
|
|
101
|
+
await postWithPath('checkout', repoPath, {
|
|
102
|
+
checkout_branch: false,
|
|
103
|
+
filename
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Translate one entry in the porcelain `files` array into the panel's
|
|
108
|
+
* preferred shape. A single porcelain entry may represent up to two changes
|
|
109
|
+
* (one in the index, one in the worktree); we emit one {@link IFileChange}
|
|
110
|
+
* per non-empty side, so a "modified-staged-and-then-modified-again" file
|
|
111
|
+
* shows up twice in the panel — once under "Staged Changes" and once under
|
|
112
|
+
* "Changes" — matching the layout the user sees in VS Code.
|
|
113
|
+
*/
|
|
114
|
+
export function porcelainToFileChanges(file) {
|
|
115
|
+
const result = [];
|
|
116
|
+
// Untracked files are reported with `??` and only appear under "Changes".
|
|
117
|
+
if (file.x === '?' && file.y === '?') {
|
|
118
|
+
return [
|
|
119
|
+
{
|
|
120
|
+
path: file.to,
|
|
121
|
+
group: 'unstaged',
|
|
122
|
+
status: 'untracked',
|
|
123
|
+
isBinary: file.is_binary
|
|
124
|
+
}
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
if (file.x !== ' ' && file.x !== '?') {
|
|
128
|
+
result.push({
|
|
129
|
+
path: file.to,
|
|
130
|
+
from: file.from === file.to ? undefined : file.from,
|
|
131
|
+
group: 'staged',
|
|
132
|
+
status: porcelainCodeToStatus(file.x),
|
|
133
|
+
isBinary: file.is_binary
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (file.y !== ' ' && file.y !== '?') {
|
|
137
|
+
result.push({
|
|
138
|
+
path: file.to,
|
|
139
|
+
from: file.from === file.to ? undefined : file.from,
|
|
140
|
+
group: 'unstaged',
|
|
141
|
+
status: porcelainCodeToStatus(file.y),
|
|
142
|
+
isBinary: file.is_binary
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
function porcelainCodeToStatus(code) {
|
|
148
|
+
switch (code) {
|
|
149
|
+
case 'M':
|
|
150
|
+
return 'modified';
|
|
151
|
+
case 'A':
|
|
152
|
+
return 'added';
|
|
153
|
+
case 'D':
|
|
154
|
+
return 'deleted';
|
|
155
|
+
case 'R':
|
|
156
|
+
return 'renamed';
|
|
157
|
+
case 'U':
|
|
158
|
+
return 'unmerged';
|
|
159
|
+
case 'T':
|
|
160
|
+
return 'typechange';
|
|
161
|
+
default:
|
|
162
|
+
return 'unknown';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
|
+
import { ICommandPalette, IThemeManager } from '@jupyterlab/apputils';
|
|
3
|
+
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
|
|
4
|
+
import { Contents } from '@jupyterlab/services';
|
|
5
|
+
import { createGitDiffWidget, diffWidgetId } from './diffWidget';
|
|
6
|
+
import { GitPanel } from './panel';
|
|
7
|
+
import { IFileChange } from './tokens';
|
|
8
|
+
/**
|
|
9
|
+
* Command IDs exposed by the git plugin. Both are registered in
|
|
10
|
+
* {@link registerGitCommands} so other extensions, the command palette and
|
|
11
|
+
* keybindings can drive the panel without going through the React internals.
|
|
12
|
+
*/
|
|
13
|
+
export declare namespace CommandIDs {
|
|
14
|
+
const refresh = "xtralab:git:refresh";
|
|
15
|
+
const openDiff = "xtralab:git:open-diff";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Argument shapes used by {@link CommandIDs.openDiff}. Cast to this type
|
|
19
|
+
* inside the execute callback for type safety; the registry itself takes
|
|
20
|
+
* `ReadonlyPartialJSONObject` because Lumino does not know about our
|
|
21
|
+
* extension-specific shapes.
|
|
22
|
+
*/
|
|
23
|
+
export declare namespace CommandArguments {
|
|
24
|
+
interface IOpenDiff {
|
|
25
|
+
repoPath: string;
|
|
26
|
+
change: IFileChange;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export interface IRegisterGitCommandsOptions {
|
|
30
|
+
app: JupyterFrontEnd;
|
|
31
|
+
panel: GitPanel;
|
|
32
|
+
themeManager: IThemeManager | null;
|
|
33
|
+
commandPalette: ICommandPalette | null;
|
|
34
|
+
/**
|
|
35
|
+
* Contents manager used by the diff widget to write hunk-discard
|
|
36
|
+
* results back to the working tree.
|
|
37
|
+
*/
|
|
38
|
+
contentsManager: Contents.IManager;
|
|
39
|
+
/**
|
|
40
|
+
* Rendermime registry used by the notebook diff to render outputs and
|
|
41
|
+
* markdown cells with their actual mime-type renderers (images as
|
|
42
|
+
* images, HTML as HTML, etc.). May be `null` in stripped-down hosts —
|
|
43
|
+
* the notebook diff falls back to a text representation in that case.
|
|
44
|
+
*/
|
|
45
|
+
rendermime: IRenderMimeRegistry | null;
|
|
46
|
+
/**
|
|
47
|
+
* Called whenever the diff widget mutates the working tree (e.g. after
|
|
48
|
+
* a hunk discard) so the plugin can refresh the changes panel.
|
|
49
|
+
*/
|
|
50
|
+
onChanged: () => void;
|
|
51
|
+
/**
|
|
52
|
+
* Called for every diff widget created via the {@link CommandIDs.openDiff}
|
|
53
|
+
* command, before the widget is added to the shell. Lets the plugin track
|
|
54
|
+
* the widget for layout restoration and reveal-on-reuse.
|
|
55
|
+
*/
|
|
56
|
+
trackDiff(widget: ReturnType<typeof createGitDiffWidget>): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Look up an already-open diff widget for a given file change. Returns
|
|
59
|
+
* `undefined` when none is open.
|
|
60
|
+
*/
|
|
61
|
+
findDiff(change: IFileChange): ReturnType<typeof createGitDiffWidget> | undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Register the git commands on the application command registry. Returns
|
|
65
|
+
* a disposer that clears the command palette items the registration created
|
|
66
|
+
* (the commands themselves stay registered for the lifetime of the plugin).
|
|
67
|
+
*/
|
|
68
|
+
export declare function registerGitCommands(options: IRegisterGitCommandsOptions): () => void;
|
|
69
|
+
export { diffWidgetId };
|