@jupyter/docprovider-extension 5.0.0-alpha.0 → 5.0.0-beta.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.
@@ -0,0 +1,39 @@
1
+ import type * as nbformat from '@jupyterlab/nbformat';
2
+ import type { CodeEditor } from '@jupyterlab/codeeditor';
3
+ import type { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
+ import type { TranslationBundle } from '@jupyterlab/translation';
5
+ import { Widget } from '@lumino/widgets';
6
+ import 'nbdime/lib/styles/variables.css';
7
+ import 'nbdime/lib/styles/common.css';
8
+ import 'nbdime/lib/styles/diff.css';
9
+ import 'nbdime/lib/upstreaming/flexpanel.css';
10
+ import 'nbdime/lib/common/collapsible.css';
11
+ import '../style/conflictDiff.css';
12
+ /**
13
+ * A widget that shows a side-by-side diff between two in-memory notebooks.
14
+ * Uses nbdime's NotebookDiffModel/NotebookDiffWidget for rendering.
15
+ *
16
+ * Because nbdime bundles its own @lumino/widgets (separate from JupyterLab's),
17
+ * we extend Widget and append the nbdime widget's DOM node directly instead
18
+ * of using Panel.addWidget(), which would fail due to the class identity mismatch.
19
+ */
20
+ export declare class ConflictDiffWidget extends Widget {
21
+ private _nbdiffWidget;
22
+ private _editorFactory;
23
+ private _rendermime;
24
+ private _trans;
25
+ constructor(options: ConflictDiffWidget.IOptions);
26
+ create(options: ConflictDiffWidget.ICreateOptions): Promise<void>;
27
+ dispose(): void;
28
+ }
29
+ export declare namespace ConflictDiffWidget {
30
+ interface IOptions {
31
+ translator?: TranslationBundle;
32
+ editorFactory: CodeEditor.Factory;
33
+ rendermime: IRenderMimeRegistry;
34
+ }
35
+ interface ICreateOptions {
36
+ base: nbformat.INotebookContent;
37
+ remote: nbformat.INotebookContent;
38
+ }
39
+ }
@@ -0,0 +1,184 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { nullTranslator } from '@jupyterlab/translation';
6
+ import { Widget } from '@lumino/widgets';
7
+ import { NotebookDiffModel } from 'nbdime/lib/diff/model';
8
+ import { NotebookDiffWidget } from 'nbdime/lib/diff/widget';
9
+ import 'nbdime/lib/styles/variables.css';
10
+ import 'nbdime/lib/styles/common.css';
11
+ import 'nbdime/lib/styles/diff.css';
12
+ import 'nbdime/lib/upstreaming/flexpanel.css';
13
+ import 'nbdime/lib/common/collapsible.css';
14
+ import '../style/conflictDiff.css';
15
+ /**
16
+ * A widget that shows a side-by-side diff between two in-memory notebooks.
17
+ * Uses nbdime's NotebookDiffModel/NotebookDiffWidget for rendering.
18
+ *
19
+ * Because nbdime bundles its own @lumino/widgets (separate from JupyterLab's),
20
+ * we extend Widget and append the nbdime widget's DOM node directly instead
21
+ * of using Panel.addWidget(), which would fail due to the class identity mismatch.
22
+ */
23
+ export class ConflictDiffWidget extends Widget {
24
+ constructor(options) {
25
+ var _a;
26
+ super();
27
+ this._nbdiffWidget = null;
28
+ this.addClass('nbdime-Widget');
29
+ this._editorFactory = options.editorFactory;
30
+ this._rendermime = options.rendermime;
31
+ this._trans =
32
+ (_a = options.translator) !== null && _a !== void 0 ? _a : nullTranslator.load('jupyter_collaboration');
33
+ this.node.style.display = 'flex';
34
+ this.node.style.flexDirection = 'column';
35
+ this.node.style.height = '100%';
36
+ this.node.style.overflow = 'auto';
37
+ }
38
+ async create(options) {
39
+ const diff = _diffNotebooks(options.base, options.remote);
40
+ const model = new NotebookDiffModel(options.base, diff);
41
+ const diffWidget = new NotebookDiffWidget({
42
+ model,
43
+ rendermime: this._rendermime,
44
+ editorFactory: this._editorFactory
45
+ });
46
+ await diffWidget.init();
47
+ this._nbdiffWidget = diffWidget;
48
+ this.node.appendChild(_makeHeaderNode(this._trans.__('Server version'), this._trans.__('Local version')));
49
+ this.node.appendChild(diffWidget.node);
50
+ }
51
+ dispose() {
52
+ var _a;
53
+ if (this.isDisposed) {
54
+ return;
55
+ }
56
+ (_a = this._nbdiffWidget) === null || _a === void 0 ? void 0 : _a.dispose();
57
+ super.dispose();
58
+ }
59
+ }
60
+ function _makeHeaderNode(baseLabel, remoteLabel) {
61
+ const node = document.createElement('div');
62
+ node.className = 'jp-conflict-diff-header nbdime-Diff';
63
+ const banner = document.createElement('div');
64
+ banner.className = 'nbdime-header-banner';
65
+ const base = document.createElement('span');
66
+ base.className = 'nbdime-header-base';
67
+ base.textContent = baseLabel;
68
+ const remote = document.createElement('span');
69
+ remote.className = 'nbdime-header-remote';
70
+ remote.textContent = remoteLabel;
71
+ banner.appendChild(base);
72
+ banner.appendChild(remote);
73
+ node.appendChild(banner);
74
+ return node;
75
+ }
76
+ function _getSource(cell) {
77
+ const src = cell.source;
78
+ return Array.isArray(src) ? src.join('') : src;
79
+ }
80
+ /**
81
+ * Compute index pairs (baseIdx, remoteIdx) for the longest common subsequence
82
+ * of cell IDs between two lists.
83
+ */
84
+ function _lcs(a, b) {
85
+ const m = a.length, n = b.length;
86
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
87
+ for (let i = 1; i <= m; i++) {
88
+ for (let j = 1; j <= n; j++) {
89
+ dp[i][j] =
90
+ a[i - 1] === b[j - 1]
91
+ ? dp[i - 1][j - 1] + 1
92
+ : Math.max(dp[i - 1][j], dp[i][j - 1]);
93
+ }
94
+ }
95
+ const result = [];
96
+ let i = m, j = n;
97
+ while (i > 0 && j > 0) {
98
+ if (a[i - 1] === b[j - 1]) {
99
+ result.unshift([i - 1, j - 1]);
100
+ i--;
101
+ j--;
102
+ }
103
+ else if (dp[i - 1][j] > dp[i][j - 1]) {
104
+ i--;
105
+ }
106
+ else {
107
+ j--;
108
+ }
109
+ }
110
+ return result;
111
+ }
112
+ /**
113
+ * Compute a notebook-level IDiffEntry[] for use with NotebookDiffModel.
114
+ * Matches cells by id using LCS; changed-source cells appear as remove+add.
115
+ */
116
+ function _diffNotebooks(base, remote) {
117
+ const baseCells = base.cells;
118
+ const remoteCells = remote.cells;
119
+ const baseIds = baseCells.map(c => { var _a; return (_a = c.id) !== null && _a !== void 0 ? _a : ''; });
120
+ const remoteIds = remoteCells.map(c => { var _a; return (_a = c.id) !== null && _a !== void 0 ? _a : ''; });
121
+ const matches = _lcs(baseIds, remoteIds);
122
+ const cellsDiff = [];
123
+ let bi = 0, ri = 0;
124
+ for (const [baseIdx, remoteIdx] of matches) {
125
+ if (bi < baseIdx) {
126
+ cellsDiff.push({ op: 'removerange', key: bi, length: baseIdx - bi });
127
+ }
128
+ if (ri < remoteIdx) {
129
+ cellsDiff.push({
130
+ op: 'addrange',
131
+ key: bi,
132
+ valuelist: remoteCells.slice(ri, remoteIdx)
133
+ });
134
+ }
135
+ const sourceChanged = _getSource(baseCells[baseIdx]) !== _getSource(remoteCells[remoteIdx]);
136
+ const metaChanged = JSON.stringify(baseCells[baseIdx].metadata) !==
137
+ JSON.stringify(remoteCells[remoteIdx].metadata);
138
+ if (sourceChanged) {
139
+ cellsDiff.push({ op: 'removerange', key: baseIdx, length: 1 });
140
+ cellsDiff.push({
141
+ op: 'addrange',
142
+ key: baseIdx,
143
+ valuelist: [remoteCells[remoteIdx]]
144
+ });
145
+ }
146
+ else if (metaChanged) {
147
+ cellsDiff.push({
148
+ op: 'patch',
149
+ key: baseIdx,
150
+ diff: [
151
+ {
152
+ op: 'replace',
153
+ key: 'metadata',
154
+ value: remoteCells[remoteIdx].metadata
155
+ }
156
+ ]
157
+ });
158
+ }
159
+ bi = baseIdx + 1;
160
+ ri = remoteIdx + 1;
161
+ }
162
+ if (bi < baseCells.length) {
163
+ cellsDiff.push({
164
+ op: 'removerange',
165
+ key: bi,
166
+ length: baseCells.length - bi
167
+ });
168
+ }
169
+ if (ri < remoteCells.length) {
170
+ cellsDiff.push({
171
+ op: 'addrange',
172
+ key: bi,
173
+ valuelist: remoteCells.slice(ri)
174
+ });
175
+ }
176
+ const topDiff = [];
177
+ if (cellsDiff.length > 0) {
178
+ topDiff.push({ op: 'patch', key: 'cells', diff: cellsDiff });
179
+ }
180
+ if (JSON.stringify(base.metadata) !== JSON.stringify(remote.metadata)) {
181
+ topDiff.push({ op: 'replace', key: 'metadata', value: remote.metadata });
182
+ }
183
+ return topDiff;
184
+ }
package/lib/yprovider.js CHANGED
@@ -5,6 +5,12 @@
5
5
  import { ITranslator } from '@jupyterlab/translation';
6
6
  import { WebSocketProvider, WebSocketAwarenessProvider, IAwarenessProviderFactory, IDocumentProviderFactory } from '@jupyter/docprovider';
7
7
  import { URLExt } from '@jupyterlab/coreutils';
8
+ import { MainAreaWidget, ToolbarButton } from '@jupyterlab/apputils';
9
+ import { saveIcon, undoIcon } from '@jupyterlab/ui-components';
10
+ import { IEditorServices } from '@jupyterlab/codeeditor';
11
+ import { IDocumentManager } from '@jupyterlab/docmanager';
12
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
13
+ import { ConflictDiffWidget } from './conflictDiffWidget';
8
14
  /**
9
15
  * The plugin ID for settings.
10
16
  */
@@ -16,10 +22,61 @@ class WebSocketDocumentProviderFactory {
16
22
  constructor(options) {
17
23
  this._trans = options.translator;
18
24
  this._commands = options.commands;
25
+ this._docManager = options.docManager;
26
+ this._shell = options.shell;
27
+ this._contents = options.contents;
28
+ this._editorFactory = options.editorFactory;
29
+ this._rendermime = options.rendermime;
19
30
  }
20
31
  create(options) {
32
+ const shell = this._shell;
33
+ const contents = this._contents;
34
+ const editorFactory = this._editorFactory;
35
+ const rendermime = this._rendermime;
36
+ const path = options.path;
37
+ const onConflictShowNotebookDiff = async (localContent) => {
38
+ const serverModel = await contents.get(path, { content: true });
39
+ const widget = new ConflictDiffWidget({
40
+ translator: this._trans,
41
+ editorFactory,
42
+ rendermime
43
+ });
44
+ await widget.create({
45
+ base: serverModel.content,
46
+ remote: localContent
47
+ });
48
+ const main = new MainAreaWidget({ content: widget });
49
+ main.title.label = this._trans.__('Conflict diff: %1', path);
50
+ main.title.closable = true;
51
+ main.toolbar.addItem('revertToRemote', new ToolbarButton({
52
+ icon: undoIcon,
53
+ label: this._trans.__('Revert to Remote'),
54
+ tooltip: this._trans.__('Discard local changes and reload the server version'),
55
+ onClick: () => {
56
+ var _a;
57
+ const context = (_a = this._docManager.findWidget(path)) === null || _a === void 0 ? void 0 : _a.context;
58
+ if (context && !context.isDisposed) {
59
+ void context.revert();
60
+ }
61
+ }
62
+ }));
63
+ main.toolbar.addItem('saveLocalAs', new ToolbarButton({
64
+ icon: saveIcon,
65
+ label: this._trans.__('Save Local As'),
66
+ tooltip: this._trans.__('Save the local version with a new name'),
67
+ onClick: () => {
68
+ var _a;
69
+ const context = (_a = this._docManager.findWidget(path)) === null || _a === void 0 ? void 0 : _a.context;
70
+ if (context && !context.isDisposed) {
71
+ void context.saveAs();
72
+ }
73
+ }
74
+ }));
75
+ shell.add(main, 'main');
76
+ shell.activateById(main.id);
77
+ };
21
78
  return new WebSocketProvider({
22
- path: options.path,
79
+ path,
23
80
  contentType: options.contentType,
24
81
  format: options.format,
25
82
  model: options.model,
@@ -27,7 +84,12 @@ class WebSocketDocumentProviderFactory {
27
84
  translator: this._trans,
28
85
  serverSettings: options.serverSettings,
29
86
  onConflictSaveAs: () => this._commands.execute('docmanager:save-as'),
30
- onConflictRevert: () => this._commands.execute('docmanager:reload')
87
+ onConflictRevert: () => this._commands.execute('docmanager:reload'),
88
+ // The diff view is notebook-specific (uses nbdime), so only offer it
89
+ // when the document being opened is a notebook.
90
+ onConflictShowDiff: options.contentType === 'notebook'
91
+ ? onConflictShowNotebookDiff
92
+ : undefined
31
93
  });
32
94
  }
33
95
  }
@@ -51,14 +113,24 @@ class WebSocketAwarenessProviderFactory {
51
113
  export const documentProviderFactoryPlugin = {
52
114
  id: PLUGIN_ID + '-document-factory',
53
115
  description: 'Provides a WebSocket document provider factory.',
54
- requires: [ITranslator],
116
+ requires: [
117
+ ITranslator,
118
+ IEditorServices,
119
+ IRenderMimeRegistry,
120
+ IDocumentManager
121
+ ],
55
122
  optional: [],
56
123
  provides: IDocumentProviderFactory,
57
- activate: async (app, translator) => {
124
+ activate: async (app, translator, editorServices, rendermime, docManager) => {
58
125
  const trans = translator.load('jupyter_collaboration');
59
126
  return new WebSocketDocumentProviderFactory({
60
127
  translator: trans,
61
- commands: app.commands
128
+ commands: app.commands,
129
+ docManager,
130
+ shell: app.shell,
131
+ contents: app.serviceManager.contents,
132
+ editorFactory: editorServices.factoryService.newInlineEditor,
133
+ rendermime
62
134
  });
63
135
  }
64
136
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyter/docprovider-extension",
3
- "version": "5.0.0-alpha.0",
3
+ "version": "5.0.0-beta.0",
4
4
  "description": "JupyterLab - Collaborative Shared Models",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -41,8 +41,8 @@
41
41
  "build:lib": "tsc --sourceMap",
42
42
  "build:lib:prod": "tsc",
43
43
  "build:prod": "jlpm run clean && jlpm run build:lib:prod && jlpm run build:labextension",
44
- "build:labextension": "jupyter labextension build .",
45
- "build:labextension:dev": "jupyter labextension build --development True .",
44
+ "build:labextension": "jupyter-builder build .",
45
+ "build:labextension:dev": "jupyter-builder build --development True .",
46
46
  "clean": "jlpm run clean:lib",
47
47
  "clean:lib": "rimraf lib tsconfig.tsbuildinfo node_modules",
48
48
  "clean:labextension": "rimraf ../../projects/jupyter-docprovider/jupyter_docprovider/labextension",
@@ -50,29 +50,34 @@
50
50
  "install:extension": "jlpm run build",
51
51
  "watch": "run-p watch:src watch:labextension",
52
52
  "watch:src": "tsc -w",
53
- "watch:labextension": "jupyter labextension watch ."
53
+ "watch:labextension": "jupyter-builder watch ."
54
54
  },
55
55
  "dependencies": {
56
- "@jupyter/collaborative-drive": "^5.0.0-alpha.0",
57
- "@jupyter/docprovider": "^5.0.0-alpha.0",
58
- "@jupyter/ydoc": "^4.0.0-a3",
59
- "@jupyterlab/application": "^4.6.0-beta.1",
60
- "@jupyterlab/apputils": "^4.7.0-beta.1",
61
- "@jupyterlab/docmanager": "^4.6.0-beta.1",
62
- "@jupyterlab/docregistry": "^4.6.0-beta.1",
63
- "@jupyterlab/filebrowser": "^4.6.0-beta.1",
64
- "@jupyterlab/fileeditor": "^4.6.0-beta.1",
65
- "@jupyterlab/logconsole": "^4.6.0-beta.1",
66
- "@jupyterlab/notebook": "^4.6.0-beta.1",
67
- "@jupyterlab/settingregistry": "^4.6.0-beta.1",
68
- "@jupyterlab/translation": "^4.6.0-beta.1",
56
+ "@jupyter/collaborative-drive": "^5.0.0-beta.0",
57
+ "@jupyter/docprovider": "^5.0.0-beta.0",
58
+ "@jupyter/ydoc": "^4.0.0",
59
+ "@jupyterlab/application": "^4.6.0",
60
+ "@jupyterlab/apputils": "^4.7.0",
61
+ "@jupyterlab/codeeditor": "^4.6.0",
62
+ "@jupyterlab/docmanager": "^4.6.0",
63
+ "@jupyterlab/docregistry": "^4.6.0",
64
+ "@jupyterlab/filebrowser": "^4.6.0",
65
+ "@jupyterlab/fileeditor": "^4.6.0",
66
+ "@jupyterlab/logconsole": "^4.6.0",
67
+ "@jupyterlab/nbformat": "^4.6.0",
68
+ "@jupyterlab/notebook": "^4.6.0",
69
+ "@jupyterlab/rendermime": "^4.6.0",
70
+ "@jupyterlab/settingregistry": "^4.6.0",
71
+ "@jupyterlab/translation": "^4.6.0",
72
+ "@jupyterlab/ui-components": "^4.6.0",
69
73
  "@lumino/commands": "^2.3.2",
74
+ "nbdime": "^7.0.4",
70
75
  "y-protocols": "^1.0.5",
71
76
  "y-websocket": "^1.3.15",
72
77
  "yjs": "^13.5.40"
73
78
  },
74
79
  "devDependencies": {
75
- "@jupyterlab/builder": "^4.6.0-alpha.5",
80
+ "@jupyter/builder": "^1.2.0",
76
81
  "@types/react": "~18.3.1",
77
82
  "npm-run-all": "^4.1.5",
78
83
  "rimraf": "^4.1.2",
@@ -0,0 +1,37 @@
1
+ /* -----------------------------------------------------------------------------
2
+ | Copyright (c) Jupyter Development Team.
3
+ | Distributed under the terms of the Modified BSD License.
4
+ |---------------------------------------------------------------------------- */
5
+
6
+ .jp-conflict-diff-header.nbdime-Diff {
7
+ border-bottom: var(--jp-border-width) solid var(--jp-toolbar-border-color);
8
+ box-shadow: var(--jp-toolbar-box-shadow);
9
+ background: var(--jp-toolbar-background);
10
+ flex: 0 0 auto;
11
+ }
12
+
13
+ .jp-conflict-diff-header .nbdime-header-banner {
14
+ display: grid;
15
+ grid-template-columns: 47% 6% 47%;
16
+ }
17
+
18
+ .jp-conflict-diff-header .nbdime-header-base {
19
+ grid-column: 1;
20
+ display: inline-block;
21
+ background-color: var(--jp-diff-deleted-color0);
22
+ padding: 0 4px;
23
+ }
24
+
25
+ .jp-conflict-diff-header .nbdime-header-remote {
26
+ grid-column: 3;
27
+ display: inline-block;
28
+ background-color: var(--jp-diff-added-color0);
29
+ padding: 0 4px;
30
+ }
31
+
32
+ /* diff.css hides .jp-Cellrow-metadata on added/deleted cells by default.
33
+ Override that so metadata changes are always visible in the conflict diff. */
34
+ .nbdime-Widget .jp-Diff-added .jp-Cellrow-metadata,
35
+ .nbdime-Widget .jp-Diff-deleted .jp-Cellrow-metadata {
36
+ display: block !important;
37
+ }