Skip to content

Commit

Permalink
First working version for diff text file
Browse files Browse the repository at this point in the history
  • Loading branch information
fcollonval committed Sep 21, 2023
1 parent 2f7c360 commit 57b5c1b
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 211 deletions.
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@
"@lumino/polling": "^2.0.0",
"@lumino/signaling": "^2.0.0",
"@lumino/widgets": "^2.0.1",
"@mui/core": "^5.0.0-alpha.54",
"@mui/icons-material": "^5.11.16",
"@mui/lab": "^5.0.0-alpha.127",
"@mui/material": "^5.12.1",
Expand Down Expand Up @@ -175,13 +174,13 @@
"extension": true,
"schemaDir": "schema",
"sharedPackages": {
"@material-ui/core": {
"@mui/core": {
"singleton": true
},
"@material-ui/icons": {
"@mui/icons": {
"singleton": true
},
"@material-ui/lab": {
"@mui/lab": {
"singleton": true
},
"nbdime": {
Expand Down
19 changes: 13 additions & 6 deletions src/commandsAndMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { AdvancedPushForm } from './widgets/AdvancedPushForm';
import { GitCredentialsForm } from './widgets/CredentialsBox';
import { discardAllChanges } from './widgets/discardAllChanges';
import { CheckboxForm } from './widgets/GitResetToRemoteForm';
import { IEditorLanguageRegistry } from '@jupyterlab/codemirror';

export interface IGitCloneArgs {
/**
Expand Down Expand Up @@ -126,6 +127,7 @@ export function addCommands(
app: JupyterFrontEnd,
gitModel: GitExtension,
editorFactory: CodeEditor.Factory,
languageRegistry: IEditorLanguageRegistry,
fileBrowserModel: FileBrowserModel,
settings: ISettingRegistry.ISettings,
translator: ITranslator
Expand Down Expand Up @@ -550,18 +552,23 @@ export function addCommands(
const buildDiffWidget =
getDiffProvider(fullPath) ??
(isText &&
(options => createPlainTextDiff({ ...options, editorFactory })));
(options =>
createPlainTextDiff({
...options,
editorFactory,
languageRegistry
})));

if (buildDiffWidget) {
const id = `git-diff-${fullPath}-${model.reference.label}-${model.challenger.label}`;
const mainAreaItems = shell.widgets('main');
let mainAreaItem = mainAreaItems.next().value;
while (mainAreaItem) {
if (mainAreaItem.id === id) {
let mainAreaItem: Widget | null = null;
for (const item of mainAreaItems) {
if (item.id === id) {
shell.activateById(id);
mainAreaItem = item;
break;
}
mainAreaItem = mainAreaItems.next().value;
}

if (!mainAreaItem) {
Expand Down Expand Up @@ -693,7 +700,7 @@ export function addCommands(
);
if (!targetFile || targetFile.status === 'unmodified') {
gitModel.statusChanged.disconnect(maybeClose);
mainAreaItem.dispose();
mainAreaItem!.dispose();
}
};
gitModel.statusChanged.connect(maybeClose);
Expand Down
197 changes: 151 additions & 46 deletions src/components/diff/PlainTextDiff.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { CodeEditor } from '@jupyterlab/codeeditor';
import { CodeEditor, IEditorMimeTypeService } from '@jupyterlab/codeeditor';
import {
CodeMirrorEditorFactory,
EditorExtensionRegistry,
EditorLanguageRegistry
EditorLanguageRegistry,
IEditorLanguageRegistry
} from '@jupyterlab/codemirror';
import { Contents } from '@jupyterlab/services';
import { nullTranslator, TranslationBundle } from '@jupyterlab/translation';
import { TranslationBundle, nullTranslator } from '@jupyterlab/translation';
import { PromiseDelegate } from '@lumino/coreutils';
import { Widget } from '@lumino/widgets';
import { createNbdimeMergeView, MergeView } from 'nbdime/lib/common/mergeview';
import { StringDiffModel } from 'nbdime/lib/diff/model';
import { Panel, Widget } from '@lumino/widgets';
import {
DIFF_DELETE,
DIFF_EQUAL,
DIFF_INSERT,
diff_match_patch
} from 'diff-match-patch';
import { MergeView, createNbdimeMergeView } from 'nbdime/lib/common/mergeview';
import { IStringDiffModel, StringDiffModel } from 'nbdime/lib/diff/model';
import { DiffRangeRaw } from 'nbdime/lib/diff/range';
import { Git } from '../../tokens';

/**
Expand All @@ -21,14 +29,17 @@ import { Git } from '../../tokens';
*/
export const createPlainTextDiff = async ({
editorFactory,
languageRegistry,
model,
toolbar,
translator
}: Git.Diff.IFactoryOptions & {
languageRegistry: IEditorLanguageRegistry;
editorFactory?: CodeEditor.Factory;
}): Promise<PlainTextDiff> => {
const widget = new PlainTextDiff({
model,
languageRegistry,
editorFactory,
trans: (translator ?? nullTranslator).load('jupyterlab_git')
});
Expand All @@ -39,27 +50,34 @@ export const createPlainTextDiff = async ({
/**
* Plain Text Diff widget
*/
export class PlainTextDiff extends Widget implements Git.Diff.IDiffWidget {
export class PlainTextDiff extends Panel implements Git.Diff.IDiffWidget {
constructor({
model,
languageRegistry,
trans,
editorFactory
}: {
model: Git.Diff.IModel;
languageRegistry: IEditorLanguageRegistry;
editorFactory?: CodeEditor.Factory;
trans?: TranslationBundle;
}) {
super({
node: PlainTextDiff.createNode(
model.reference.label,
model.base?.label ?? '',
model.challenger.label
)
});
super();
this.addClass('jp-git-diff-root');
this.addClass('nbdime-root');
this.addWidget(
new Widget({
node: PlainTextDiff.createHeader(
model.reference.label,
model.base?.label,
model.challenger.label
)
})
);
const getReady = new PromiseDelegate<void>();
this._isReady = getReady.promise;
this._container = this.node.lastElementChild as HTMLElement;
this._editorFactory = editorFactory ?? createEditorFactory();
this._languageRegistry = languageRegistry;
this._model = model;
this._trans = trans ?? nullTranslator.load('jupyterlab_git');

Expand Down Expand Up @@ -140,7 +158,6 @@ export class PlainTextDiff extends Widget implements Git.Diff.IDiffWidget {
this.createDiffView(
this._challenger,
this._reference,
this._languageRegistry,
this._hasConflict ? this._base : null
);
}
Expand All @@ -150,13 +167,6 @@ export class PlainTextDiff extends Widget implements Git.Diff.IDiffWidget {
});
}

/**
* Undo onAfterAttach
*/
onBeforeDetach(): void {
this._container.innerHTML = '';
}

/**
* Refresh diff
*
Expand All @@ -166,7 +176,6 @@ export class PlainTextDiff extends Widget implements Git.Diff.IDiffWidget {
await this.ready;
try {
// Clear all
this._container.innerHTML = '';
this._mergeView.dispose();

// ENH request content only if it changed
Expand All @@ -183,7 +192,6 @@ export class PlainTextDiff extends Widget implements Git.Diff.IDiffWidget {
this.createDiffView(
this._challenger!,
this._reference!,
this._languageRegistry,
this._hasConflict ? this._base : null
);

Expand All @@ -198,19 +206,17 @@ export class PlainTextDiff extends Widget implements Git.Diff.IDiffWidget {
/**
* Create wrapper node
*/
protected static createNode(...labels: string[]): HTMLElement {
protected static createHeader(
...labels: (string | undefined)[]
): HTMLElement {
const bannerClass =
labels[1] !== undefined ? 'jp-git-merge-banner' : 'jp-git-diff-banner';
const head = document.createElement('div');
head.className = 'jp-git-diff-root';
head.innerHTML = `
<div class="${bannerClass}">
${labels
.filter(label => !!label)
.map(label => `<span>${label}</span>`)
.join('<span class="jp-spacer"></span>')}
</div>
<div class="jp-git-PlainText-diff"></div>`;
head.classList.add(bannerClass);
head.innerHTML = labels
.filter(label => !!label)
.map(label => `<span>${label}</span>`)
.join('<span class="jp-spacer"></span>');
return head;
}

Expand All @@ -223,21 +229,24 @@ export class PlainTextDiff extends Widget implements Git.Diff.IDiffWidget {
protected async createDiffView(
challengerContent: string,
referenceContent: string,
languageRegistry: EditorLanguageRegistry | null = null,
baseContent: string | null = null
): Promise<void> {
if (!this._mergeView) {
const remote = new StringDiffModel(
referenceContent,
challengerContent,
[],
[]
);
const remote = createStringDiffModel(referenceContent, challengerContent);

const mimetype =
this._languageRegistry.findByFileName(this._model.filename)?.mime ??
this._languageRegistry.findBest(this._model.filename)?.mime ??
IEditorMimeTypeService.defaultMimeType;
remote.mimetype = Array.isArray(mimetype) ? mimetype[0] : mimetype;

this._mergeView = createNbdimeMergeView({
remote,
factory: this._editorFactory
remote
// factory: this._editorFactory
});
this._mergeView.addClass('jp-git-PlainText-diff');

this.addWidget(this._mergeView);
}

return Promise.resolve();
Expand All @@ -255,13 +264,17 @@ export class PlainTextDiff extends Widget implements Git.Diff.IDiffWidget {
(error as any)?.traceback
);
const msg = ((error.message || error) as string).replace('\n', '<br />');
while (this.widgets.length > 0) {
const w = this.widgets[0];
this.layout?.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>`;
}

protected _container: HTMLElement;
protected _editorFactory: CodeEditor.Factory;
protected _isReady: Promise<void>;
// @ts-expect-error complex initialization
Expand All @@ -271,10 +284,102 @@ export class PlainTextDiff extends Widget implements Git.Diff.IDiffWidget {

private _reference: string | null = null;
private _challenger: string | null = null;
private _languageRegistry: EditorLanguageRegistry | null = null;
private _languageRegistry: IEditorLanguageRegistry;
private _base: string | null = null;
}

/**
* Diff status
*/
enum DiffStatus {
Equal = DIFF_EQUAL,
Delete = DIFF_DELETE,
Insert = DIFF_INSERT
}

/**
* Diff type
*/
type Diff = [DiffStatus, string];

/**
* Pointer to the diff algorithm
*/
let dmp: any;
/**
* 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: string, b: string, ignoreWhitespace?: boolean): Diff[] {
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: string,
challenger: string,
ignoreWhitespace?: boolean
): IStringDiffModel {
const diffs = getDiff(reference, challenger, ignoreWhitespace);

const additions: DiffRangeRaw[] = [];
const deletions: DiffRangeRaw[] = [];

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(): CodeEditor.Factory {
const factory = new CodeMirrorEditorFactory({
extensions: new EditorExtensionRegistry(),
Expand Down
Loading

0 comments on commit 57b5c1b

Please sign in to comment.