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.
Files changed (56) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +69 -0
  3. package/lib/fileBrowser/commands.d.ts +62 -0
  4. package/lib/fileBrowser/commands.js +418 -0
  5. package/lib/fileBrowser/contents.d.ts +28 -0
  6. package/lib/fileBrowser/contents.js +48 -0
  7. package/lib/fileBrowser/fileBrowser.d.ts +24 -0
  8. package/lib/fileBrowser/fileBrowser.js +535 -0
  9. package/lib/fileBrowser/icons.d.ts +37 -0
  10. package/lib/fileBrowser/icons.js +153 -0
  11. package/lib/fileBrowser/index.d.ts +7 -0
  12. package/lib/fileBrowser/index.js +38 -0
  13. package/lib/fileBrowser/widget.d.ts +97 -0
  14. package/lib/fileBrowser/widget.js +95 -0
  15. package/lib/git/api.d.ts +39 -0
  16. package/lib/git/api.js +164 -0
  17. package/lib/git/commands.d.ts +69 -0
  18. package/lib/git/commands.js +67 -0
  19. package/lib/git/diffWidget.d.ts +108 -0
  20. package/lib/git/diffWidget.js +624 -0
  21. package/lib/git/icons.d.ts +14 -0
  22. package/lib/git/icons.js +19 -0
  23. package/lib/git/index.d.ts +11 -0
  24. package/lib/git/index.js +74 -0
  25. package/lib/git/notebookDiff.d.ts +176 -0
  26. package/lib/git/notebookDiff.js +805 -0
  27. package/lib/git/panel.d.ts +29 -0
  28. package/lib/git/panel.js +42 -0
  29. package/lib/git/panelComponent.d.ts +26 -0
  30. package/lib/git/panelComponent.js +300 -0
  31. package/lib/git/tokens.d.ts +78 -0
  32. package/lib/git/tokens.js +6 -0
  33. package/lib/index.d.ts +10 -0
  34. package/lib/index.js +14 -0
  35. package/package.json +216 -0
  36. package/schema/plugin.json +67 -0
  37. package/src/fileBrowser/commands.ts +507 -0
  38. package/src/fileBrowser/contents.ts +66 -0
  39. package/src/fileBrowser/fileBrowser.tsx +635 -0
  40. package/src/fileBrowser/icons.ts +172 -0
  41. package/src/fileBrowser/index.ts +56 -0
  42. package/src/fileBrowser/widget.tsx +202 -0
  43. package/src/git/api.ts +207 -0
  44. package/src/git/commands.ts +143 -0
  45. package/src/git/diffWidget.tsx +903 -0
  46. package/src/git/icons.ts +20 -0
  47. package/src/git/index.ts +109 -0
  48. package/src/git/notebookDiff.tsx +1294 -0
  49. package/src/git/panel.tsx +56 -0
  50. package/src/git/panelComponent.tsx +558 -0
  51. package/src/git/tokens.ts +91 -0
  52. package/src/index.ts +18 -0
  53. package/style/base.css +175 -0
  54. package/style/git.css +770 -0
  55. package/style/index.css +2 -0
  56. 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,7 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ /**
3
+ * The file browser plugin. Adds a `@pierre/trees`-powered file browser to the
4
+ * JupyterLab left sidebar.
5
+ */
6
+ declare const plugin: JupyterFrontEndPlugin<void>;
7
+ export default plugin;
@@ -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
+ }
@@ -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 };