Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show diff in cell widget #7

Merged
merged 4 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@
"@lumino/disposable": "^2.0.0",
"@lumino/signaling": "^2.0.0",
"@lumino/widgets": "^2.0.0",
"diff": "^7.0.0",
"typestyle": "^2.4.0"
},
"devDependencies": {
"@types/diff": "^6.0.0",
"rimraf": "^3.0.2",
"typescript": "^5"
},
Expand Down
37 changes: 26 additions & 11 deletions packages/base/src/suggestionsPanel/cellWidget.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { IYText } from '@jupyter/ydoc';
import { Cell, CodeCell, CodeCellModel } from '@jupyterlab/cells';
import { ICell } from '@jupyterlab/nbformat';
import {
RenderMimeRegistry,
standardRendererFactories as initialFactories
} from '@jupyterlab/rendermime';
import { Panel } from '@lumino/widgets';
import { suggestionCellStyle } from './style';
import {
CodeMirrorEditorFactory,
EditorExtensionRegistry,
EditorLanguageRegistry,
EditorThemeRegistry,
ybinding
} from '@jupyterlab/codemirror';
import { IYText } from '@jupyter/ydoc';
import { ICell } from '@jupyterlab/nbformat';
import {
RenderMimeRegistry,
standardRendererFactories as initialFactories
} from '@jupyterlab/rendermime';
import { Panel } from '@lumino/widgets';

import { highlightTextExtension } from './cmExtension';
import { suggestionCellStyle } from './style';

export class CellWidget extends Panel {
constructor(options: CellWidget.IOptions) {
super(options);
const { cellModel } = options;
this.addClass(suggestionCellStyle);

this._cellId = cellModel.id as string | undefined;
const editorExtensions = () => {
const themes = new EditorThemeRegistry();
EditorThemeRegistry.getDefaultThemes().forEach(theme => {
Expand All @@ -39,12 +41,19 @@ export class CellWidget extends Panel {
const sharedModel = options.model.sharedModel as IYText;
return EditorExtensionRegistry.createImmutableExtension(
ybinding({
ytext: sharedModel.ysource,
undoManager: sharedModel.undoManager ?? undefined
ytext: sharedModel.ysource
})
);
}
});
registry.addExtension({
name: 'suggestion-view',
factory: options => {
return EditorExtensionRegistry.createImmutableExtension([
highlightTextExtension
]);
}
});
return registry;
};

Expand Down Expand Up @@ -97,6 +106,12 @@ export class CellWidget extends Panel {
}).initializeState();
this.addWidget(cellWidget);
}

get cellId(): string | undefined {
return this._cellId;
}

private _cellId: string | undefined;
}

export namespace CellWidget {
Expand Down
105 changes: 105 additions & 0 deletions packages/base/src/suggestionsPanel/cmExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
ViewPlugin,
Decoration,
DecorationSet,
ViewUpdate,
WidgetType,
EditorView
} from '@codemirror/view';
import * as Diff from 'diff';
class HighlightDiff {
constructor(view: EditorView) {
this._view = view;
this.decorations = Decoration.none;
this._originalText = this._view.state.doc.toString();
this.updateDecorations();
}

update(update: ViewUpdate) {
if (update.docChanged || update.selectionSet) {
this.updateDecorations();
}
}

updateDecorations() {
const currentText = this._view.state.doc.toString();
// Compare words with spaces
const diffs = Diff.diffWordsWithSpace(this._originalText, currentText);

const decorations = [];
let pos = 0;

for (const diff of diffs) {
const length = diff.value.length;

if (diff.added) {
// Highlight added text
decorations.push(
Decoration.mark({
class: 'cm-cell-diff-added'
}).range(pos, pos + length)
);
pos += length; // Move the position for added text
} else if (diff.removed) {
// Keep removed text and display it inline as strikethrough
decorations.push(
Decoration.widget({
widget: new RemovedTextWidget(diff.value),
side: -1
}).range(pos, pos) // Insert before current position
);
} else {
pos += length; // Unchanged text
}
}

this.decorations = Decoration.set(decorations);
}

destroy() {
// TODO
}

decorations: DecorationSet;
private _originalText: string;
private _view: EditorView;
}
/**
* Widget to show removed text
*
* @class RemovedTextWidget
* @extends {WidgetType}
*/
class RemovedTextWidget extends WidgetType {
constructor(text: string) {
super();
this._text = text;
}

get text(): string {
return this._text;
}

toDOM() {
const span = document.createElement('span');
span.textContent = this._text;
span.className = 'cm-cell-diff-removed';
return span;
}

eq(other: WidgetType) {
return other instanceof RemovedTextWidget && other.text === this.text;
}

updateDOM() {
return false;
}

private _text: string;
}

export const highlightTextExtension = [
ViewPlugin.fromClass(HighlightDiff, {
decorations: (v: HighlightDiff) => v.decorations
})
];
54 changes: 53 additions & 1 deletion packages/base/src/suggestionsPanel/model.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { NotebookPanel } from '@jupyterlab/notebook';
import { Notebook, NotebookPanel } from '@jupyterlab/notebook';
import {
IAllSuggestions,
ISuggestionChange,
ISuggestionsManager,
ISuggestionsModel
} from '../types';
import { ISignal, Signal } from '@lumino/signaling';
import { Cell, ICellModel } from '@jupyterlab/cells';
export class SuggestionsModel implements ISuggestionsModel {
constructor(options: SuggestionsModel.IOptions) {
this.switchNotebook(options.panel);
Expand All @@ -21,6 +22,9 @@ export class SuggestionsModel implements ISuggestionsModel {
get notebookSwitched(): ISignal<ISuggestionsModel, void> {
return this._notebookSwitched;
}
get activeCellChanged(): ISignal<ISuggestionsModel, { cellId?: string }> {
return this._activeCellChanged;
}
get suggestionChanged(): ISignal<
ISuggestionsModel,
Omit<ISuggestionChange, 'notebookPath'>
Expand Down Expand Up @@ -65,16 +69,59 @@ export class SuggestionsModel implements ISuggestionsModel {
...options
});
}
getCellIndex(cellId?: string): number {
if (!cellId) {
return -1;
}

const allCells = this._notebookPanel?.content.model?.cells;
if (!allCells) {
return -1;
}
for (let idx = 0; idx < allCells.length; idx++) {
const element = allCells.get(idx);
if (element.id === cellId) {
return idx;
}
}
return -1;
}
async switchNotebook(panel: NotebookPanel | null): Promise<void> {
if (panel) {
await panel.context.ready;
this._allSuggestions = this._suggestionsManager.getAllSuggestions(panel);
}
this._disconnectPanelSignal();
this._notebookPanel = panel;
this._connectPanelSignal();
this._filePath = this._notebookPanel?.context.localPath;
this._notebookSwitched.emit();
}

private _connectPanelSignal() {
if (!this._notebookPanel) {
return;
}
this._notebookPanel.content.activeCellChanged.connect(
this._handleActiveCellChanged,
this
);
}

private _disconnectPanelSignal() {
if (!this._notebookPanel) {
return;
}
this._notebookPanel.content.activeCellChanged.disconnect(
this._handleActiveCellChanged
);
}
private _handleActiveCellChanged(
nb: Notebook,
cell: Cell<ICellModel> | null
) {
this._activeCellChanged.emit({ cellId: cell?.model.id });
}
private _handleSuggestionChanged(
manager: ISuggestionsManager,
changed: ISuggestionChange
Expand All @@ -84,6 +131,7 @@ export class SuggestionsModel implements ISuggestionsModel {
this._suggestionChanged.emit(newChanged);
}
}

private _isDisposed = false;
private _filePath?: string;
private _notebookPanel: NotebookPanel | null = null;
Expand All @@ -94,6 +142,10 @@ export class SuggestionsModel implements ISuggestionsModel {
ISuggestionsModel,
Omit<ISuggestionChange, 'notebookPath'>
>(this);
private _activeCellChanged = new Signal<
ISuggestionsModel,
{ cellId?: string }
>(this);
}

export namespace SuggestionsModel {
Expand Down
13 changes: 10 additions & 3 deletions packages/base/src/suggestionsPanel/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ export const mainPanelStyle = style({});
export const suggestionsWidgetAreaStyle = style({
display: 'flex',
flexDirection: 'column',
gap: '10px',
height: '100%'
gap: '5px',
height: '100%',
overflow: 'auto'
});

export const suggestionCellStyle = style({
background: 'var(--jp-cell-editor-background)'
background: 'var(--jp-cell-editor-background)',
margin: '5px'
});
export const suggestionCellSelectedStyle = style({
border:
'var(--jp-border-width) solid var(--jp-cell-editor-active-border-color)',
boxShadow: 'var(--jp-elevation-z4)'
});
Loading