UNPKG

@jupyterlab/git

Version:

A JupyterLab extension for version control using git

309 lines (308 loc) 11.2 kB
import { IEditorMimeTypeService } from '@jupyterlab/codeeditor'; import { CodeMirrorEditorFactory, EditorExtensionRegistry, EditorLanguageRegistry } from '@jupyterlab/codemirror'; import { nullTranslator } from '@jupyterlab/translation'; import { PromiseDelegate } from '@lumino/coreutils'; import { Panel, Widget } from '@lumino/widgets'; import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch } from 'diff-match-patch'; import { createNbdimeMergeView } from 'nbdime/lib/common/mergeview'; import { StringDiffModel, createDirectStringDiffModel } from 'nbdime/lib/diff/model'; import { DiffRangeRaw } from 'nbdime/lib/diff/range'; /** * Diff callback to be registered for plain-text files. * * @param model Diff model * @param toolbar MainAreaWidget toolbar * @returns PlainText diff widget */ export const createPlainTextDiff = async ({ editorFactory, languageRegistry, model, toolbar, translator }) => { const widget = new PlainTextDiff({ model, languageRegistry, editorFactory, trans: (translator !== null && translator !== void 0 ? translator : nullTranslator).load('jupyterlab_git') }); await widget.ready; return widget; }; /** * Plain Text Diff widget */ export class PlainTextDiff extends Panel { constructor({ model, languageRegistry, trans, editorFactory }) { var _a, _b, _c; super(); this._reference = null; this._challenger = null; this._base = null; this.addClass('jp-git-diff-root'); this.addClass('nbdime-root'); this.addWidget(new Widget({ node: PlainTextDiff.createHeader(model.reference.label, (_a = model.base) === null || _a === void 0 ? void 0 : _a.label, model.challenger.label) })); const getReady = new PromiseDelegate(); this._isReady = getReady.promise; this._languageRegistry = languageRegistry !== null && languageRegistry !== void 0 ? languageRegistry : new EditorLanguageRegistry(); this._editorFactory = editorFactory !== null && editorFactory !== void 0 ? editorFactory : createEditorFactory(this._languageRegistry); this._model = model; this._trans = trans !== null && trans !== void 0 ? trans : nullTranslator.load('jupyterlab_git'); // Load file content early Promise.all([ this._model.reference.content(), this._model.challenger.content(), (_c = (_b = this._model.base) === null || _b === void 0 ? void 0 : _b.content()) !== null && _c !== void 0 ? _c : Promise.resolve(null) ]) .then(([reference, challenger, base]) => { this._reference = reference; this._challenger = challenger; this._base = base; getReady.resolve(); }) .catch(reason => { this.showError(reason); getReady.resolve(); }); } /** * Helper to determine if three-way diff should be shown. */ get _hasConflict() { var _a; return (_a = this._model.hasConflict) !== null && _a !== void 0 ? _a : false; } /** * Checks if the conflicted file has been resolved. */ get isFileResolved() { return true; } /** * Diff model */ get model() { return this._model; } /** * Promise which fulfills when the widget is ready. */ get ready() { return this._isReady; } /** * Gets the file model of a resolved merge conflict, * and rejects if unable to retrieve. */ getResolvedFile() { var _a, _b; const value = (_b = (_a = this._mergeView) === null || _a === void 0 ? void 0 : _a.getMergedValue()) !== null && _b !== void 0 ? _b : null; if (value !== null) { return Promise.resolve({ type: 'file', format: 'text', content: value }); } else { return Promise.reject(this._trans.__('Failed to get a valid file value.')); } } /** * Callback to create the diff widget once the widget * is attached so CodeMirror get proper size. */ onAfterAttach() { this.ready .then(() => { if (this._challenger !== null && this._reference !== null) { this.createDiffView(this._challenger, this._reference, this._hasConflict ? this._base : null); } }) .catch(reason => { this.showError(reason); }); } /** * Refresh diff * * Note: Update the content and recompute the diff */ async refresh() { var _a, _b; await this.ready; try { // Clear all this._mergeView.dispose(); // ENH request content only if it changed if (this._reference !== null) { this._reference = await this._model.reference.content(); } if (this._challenger !== null) { this._challenger = await this._model.challenger.content(); } if (this._base !== null) { this._base = (_b = (await ((_a = this._model.base) === null || _a === void 0 ? void 0 : _a.content()))) !== null && _b !== void 0 ? _b : null; } this.createDiffView(this._challenger, this._reference, this._hasConflict ? this._base : null); this._challenger = null; this._reference = null; this._base = null; } catch (reason) { this.showError(reason); } } /** * Create wrapper node */ static createHeader(...labels) { const bannerClass = labels[1] !== undefined ? 'jp-git-merge-banner' : 'jp-git-diff-banner'; const head = document.createElement('div'); head.classList.add(bannerClass); head.innerHTML = labels .filter(label => !!label) .map(label => `<span>${label}</span>`) .join(''); return head; } /** * Create the Plain Text Diff view * * Note: baseContent will only be passed when displaying * a three-way merge conflict. */ async createDiffView(challengerContent, referenceContent, baseContent = null) { var _a, _b, _c, _d; if (!this._mergeView) { const mimetypes = (_d = (_b = (_a = this._languageRegistry.findByFileName(this._model.filename)) === null || _a === void 0 ? void 0 : _a.mime) !== null && _b !== void 0 ? _b : (_c = this._languageRegistry.findBest(this._model.filename)) === null || _c === void 0 ? void 0 : _c.mime) !== null && _d !== void 0 ? _d : IEditorMimeTypeService.defaultMimeType; const mimetype = Array.isArray(mimetypes) ? mimetypes[0] : mimetypes; let remote; let local = undefined; let merged = undefined; if (baseContent !== null) { remote = createStringDiffModel(baseContent, referenceContent); local = createStringDiffModel(baseContent, challengerContent); local.mimetype = mimetype; merged = createDirectStringDiffModel(baseContent, baseContent); merged.mimetype = mimetype; } else { remote = createStringDiffModel(referenceContent, challengerContent); } remote.mimetype = mimetype; this._mergeView = createNbdimeMergeView({ remote, local, merged, // factory: this._editorFactory showBase: false }); this._mergeView.addClass('jp-git-PlainText-diff'); this.addWidget(this._mergeView); } return Promise.resolve(); } /** * Display an error instead of the file diff * * @param error Error object */ showError(error) { var _a; console.error(this._trans.__('Failed to load file diff.'), error, error === null || error === void 0 ? void 0 : error.traceback); const msg = (error.message || error).replace('\n', '<br />'); while (this.widgets.length > 0) { const w = this.widgets[0]; (_a = this.layout) === null || _a === void 0 ? void 0 : _a.removeWidget(w); w.dispose(); } this.node.innerHTML = `<p class="jp-git-diff-error"> <span>${this._trans.__('Error Loading File Diff:')}</span> <span class="jp-git-diff-error-message">${msg}</span> </p>`; } } /** * Diff status */ var DiffStatus; (function (DiffStatus) { DiffStatus[DiffStatus["Equal"] = DIFF_EQUAL] = "Equal"; DiffStatus[DiffStatus["Delete"] = DIFF_DELETE] = "Delete"; DiffStatus[DiffStatus["Insert"] = DIFF_INSERT] = "Insert"; })(DiffStatus || (DiffStatus = {})); /** * Pointer to the diff algorithm */ let dmp; /** * Compute the diff between two strings. * * @param a Reference * @param b Challenger * @param ignoreWhitespace Whether to ignore white spaces or not * @returns Diff list */ function getDiff(a, b, ignoreWhitespace) { if (!dmp) { dmp = new diff_match_patch(); } const diff = dmp.diff_main(a, b); dmp.diff_cleanupSemantic(diff); // The library sometimes leaves in empty parts, which confuse the algorithm for (let i = 0; i < diff.length; ++i) { const part = diff[i]; if (ignoreWhitespace ? !/[^ \t]/.test(part[1]) : !part[1]) { diff.splice(i--, 1); } else if (i && diff[i - 1][0] === part[0]) { diff.splice(i--, 1); diff[i][1] += part[1]; } } return diff; } /** * Create nbdime diff model from two strings. * * @param reference Reference text * @param challenger Challenger text * @param ignoreWhitespace Whether to ignore white spaces or not * @returns The nbdime diff model */ function createStringDiffModel(reference, challenger, ignoreWhitespace) { const diffs = getDiff(reference, challenger, ignoreWhitespace); const additions = []; const deletions = []; let referencePos = 0; let challengerPos = 0; diffs.forEach(([status, str]) => { switch (status) { case DiffStatus.Delete: deletions.push(new DiffRangeRaw(referencePos, str.length)); referencePos += str.length; break; case DiffStatus.Insert: additions.push(new DiffRangeRaw(challengerPos, str.length)); challengerPos += str.length; break; // Equal is not represented in nbdime case DiffStatus.Equal: referencePos += str.length; challengerPos += str.length; break; } }); return new StringDiffModel(reference, challenger, additions, deletions); } /** * Create a default editor factory. * * @returns Editor factory */ function createEditorFactory(languages) { const factory = new CodeMirrorEditorFactory({ extensions: new EditorExtensionRegistry(), languages }); return factory.newInlineEditor.bind(factory); }