@jupyterlab/git
Version:
A JupyterLab extension for version control using git
309 lines (308 loc) • 11.2 kB
JavaScript
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);
}