From 6cbdb286621a391ea316e66cbe8b1b16d00f81b0 Mon Sep 17 00:00:00 2001 From: Florence Haudin Date: Tue, 11 Jul 2023 18:49:52 +0200 Subject: [PATCH] Keep on migrating padding (one effect by editor using decoration sets), add a listener, try to reintroduce syntax highlighting (not yet working) and introduce minor css changes. --- package-lock.json | 10 +- packages/nbdime/package.json | 4 +- packages/nbdime/src/common/editor.ts | 103 ++++- packages/nbdime/src/common/mergeview.ts | 482 +++++++++++++++--------- packages/nbdime/src/styles/merge.css | 76 +++- 5 files changed, 470 insertions(+), 205 deletions(-) diff --git a/package-lock.json b/package-lock.json index f08dbf52..c2a128a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1961,9 +1961,9 @@ "integrity": "sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw==" }, "node_modules/@codemirror/view": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.13.0.tgz", - "integrity": "sha512-oXTfJzHJ5Tl7f6T8ZO0HKf981zubxgKohjddLobbntbNZHlOZGMRL+pPZGtclDWFaFJWtGBYRGyNdjQ6Xsx5yA==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.14.0.tgz", + "integrity": "sha512-I263FPs4In42MNmrdwN2DfmYPFMVMXgT7o/mxdGp4jv5LPs8i0FOxzmxF5yeeQdYSTztb2ZhmPIu0ahveInVTg==", "dependencies": { "@codemirror/state": "^6.1.4", "style-mod": "^4.0.0", @@ -21745,8 +21745,10 @@ "license": "BSD-3-Clause", "dependencies": { "@codemirror/lang-python": "^6.1.3", + "@codemirror/language": "^6.6.0", + "@codemirror/legacy-modes": "^6.3.2", "@codemirror/state": "^6.2.0", - "@codemirror/view": "^6.9.6", + "@codemirror/view": "^6.14.0", "@jupyterlab/codeeditor": "^4.0.0", "@jupyterlab/codemirror": "^4.0.0", "@jupyterlab/coreutils": "^6.0.0", diff --git a/packages/nbdime/package.json b/packages/nbdime/package.json index 957ad8d6..d88b2458 100644 --- a/packages/nbdime/package.json +++ b/packages/nbdime/package.json @@ -20,8 +20,10 @@ }, "dependencies": { "@codemirror/state": "^6.2.0", - "@codemirror/view": "^6.9.6", + "@codemirror/view": "^6.14.0", "@codemirror/lang-python": "^6.1.3", + "@codemirror/language": "^6.6.0", + "@codemirror/legacy-modes": "^6.3.2", "@jupyterlab/codeeditor": "^4.0.0", "@jupyterlab/codemirror": "^4.0.0", "@jupyterlab/coreutils": "^6.0.0", diff --git a/packages/nbdime/src/common/editor.ts b/packages/nbdime/src/common/editor.ts index b5e590c6..f5832f14 100644 --- a/packages/nbdime/src/common/editor.ts +++ b/packages/nbdime/src/common/editor.ts @@ -13,37 +13,106 @@ import { } from '@jupyterlab/codeeditor'; import { - CodeMirrorEditorFactory, CodeMirrorEditor + CodeMirrorEditorFactory, + CodeMirrorEditor, + EditorExtensionRegistry, + EditorLanguageRegistry, + EditorThemeRegistry, + ybinding } from '@jupyterlab/codemirror'; +//import { ITranslator, nullTranslator } from '@jupyterlab/translation'; + import type { EditorView } from '@codemirror/view'; import type { Text } from '@codemirror/state'; -import { YFile } from '@jupyter/ydoc'; +import { YFile, IYText } from '@jupyter/ydoc'; + export class EditorWidget extends CodeEditorWrapper { /** * Store all editor instances for operations that * need to loop over all instances. */ - /* Commented line : version before proposed changes for JupyterLab 4.0 migration*/ -/*constructor(options?: CodeMirrorEditor.IOptions | undefined) {*/ + constructor(value?: string, options?: CodeMirrorEditor.IOptions) { - /*if (options && options.readOnly) { - // Prevent readonly editor from trapping tabs - options.extraKeys = {Tab: false, 'Shift-Tab': false}; - }*/ - const sharedModel = new YFile(); - if (value) { - sharedModel.source = value + const sharedModel = new YFile(); + if (value) { + sharedModel.source = value + } + + const extensions = new EditorExtensionRegistry(); + const languages = new EditorLanguageRegistry(); + const registry = new EditorExtensionRegistry(); + const themes = new EditorThemeRegistry(); + + + for (const theme of EditorThemeRegistry.getDefaultThemes( + )) { themes.addTheme(theme);} + + + // Register default languages + for (const language of EditorLanguageRegistry.getDefaultLanguages( + )) {languages.addLanguage(language);} + + // Register default extensions + for (const extensionFactory of EditorExtensionRegistry.getDefaultExtensions( + { + themes } - super({ - model: new CodeEditor.Model({sharedModel}), - factory: function() { - let factory = new CodeMirrorEditorFactory(/*options*/); - return factory.newInlineEditor.bind(factory); - }() + )) {registry.addExtension(extensionFactory);} + + + extensions.addExtension({ + name: 'shared-model-binding', + factory: options => { + const sharedModel = options.model.sharedModel as IYText; + return EditorExtensionRegistry.createImmutableExtension( + ybinding({ + ytext: sharedModel.ysource, + undoManager: sharedModel.undoManager ?? undefined + }) + ); + } }); + + /*console.log('themes:', themes); + console.log('languages:', languages); + console.log('extensions:', extensions);*/ + const model = new CodeEditor.Model({sharedModel}); + model.mimeType = 'text/x-python' + + + super({ + model: model, + factory: function() { + let factory = new CodeMirrorEditorFactory({ + extensions, + languages + }); + + return factory.newInlineEditor.bind(factory); + }() + }); + + /********************WORKING VERSION ****************** */ + /*constructor(value?: string, options?: CodeMirrorEditor.IOptions) { + /*if (options && options.readOnly) { + // Prevent readonly editor from trapping tabs + options.extraKeys = {Tab: false, 'Shift-Tab': false}; + }*/ + /*const sharedModel = new YFile(); + if (value) { + sharedModel.source = value + } + super({ + model: new CodeEditor.Model({sharedModel}), + factory: function() { + let factory = new CodeMirrorEditorFactory(/*options*/ /*);*/ + /*return factory.newInlineEditor.bind(factory); + }() + });*/ + /********************************************************** */ this.staticLoaded = false; //EditorWidget.editors.push(this.cm); } diff --git a/packages/nbdime/src/common/mergeview.ts b/packages/nbdime/src/common/mergeview.ts index a309c1fa..d74bcba0 100644 --- a/packages/nbdime/src/common/mergeview.ts +++ b/packages/nbdime/src/common/mergeview.ts @@ -15,7 +15,7 @@ import type { IStringDiffModel } from '../diff/model'; DecisionStringDiffModel } from '../merge/model';*/ -import type { DiffRangePos } from '../diff/range'; +import { type DiffRangePos } from '../diff/range'; import { ChunkSource, Chunk, lineToNormalChunks } from '../chunking'; @@ -28,11 +28,6 @@ import { valueIn, /*hasEntries*/ copyObj } from './util'; NotifyUserError } from './exceptions'; */ -/*import { - python -} from '@codemirror/lang-python'; -*/ - import { python } from "@codemirror/lang-python"; import { @@ -40,10 +35,9 @@ import { StateEffect, StateField, ChangeDesc, - /*RangeSet,*/ - /*Line,*/ - /*Text,*/ - Range + // Range, + //TransactionSpec, + RangeSetBuilder } from '@codemirror/state'; import { @@ -195,9 +189,9 @@ function getCommonEditorExtensions(): Extension { diffViewPlugin, lineNumbers(), HighlightField, + //oldPaddingWidgetField, PaddingWidgetField, python() - /*decorationKeymap*/ ]; } @@ -234,11 +228,6 @@ const HighlightField = StateField.define({ highlightRanges = highlightRanges.update({ add: [decoration.range(e.value.from, e.value.to)] }); - /*console.log('****************************') - console.log(e.value.decorationKey); - console.log(e.value.highlightType); - console.log('spec:', decoration.spec.class); - console.log('****************************')*/ } if (e.is(removeHighlight)) { decoration = mergeViewDecorationDict[e.value.decorationKey][e.value.highlightType]; @@ -249,73 +238,52 @@ const HighlightField = StateField.define({ } return highlightRanges; }, - provide: field => EditorView.decorations.from(field) + provide: field => EditorView.decorations.from(field); }); -/*function leftHighlightChunkSelection(view: EditorView) { - let highlightType = 'chunk' - let decorationKey = 'left'; - let effects: StateEffect[] = view.state.selection.ranges - .filter(r => !r.empty) - .map(({ from }) => addHighlight.of({ from, to: from, highlightType, decorationKey })); - view.dispatch({ effects }); - return true; -} - -function rightHighlightChunkSelection(view: EditorView) { - let highlightType = 'chunk' - let decorationKey = 'right'; - let effects: StateEffect[] = view.state.selection.ranges - .filter(r => !r.empty) - .map(({ from }) => addHighlight.of({ from, to: from, highlightType, decorationKey })); - view.dispatch({ effects }); - return true; -} +export const addPaddingWidgetEffect= StateEffect.define({ + map: (value, mapping) => value.map(mapping); +}); -function leftHighlightInsertSelection(view: EditorView) { - let highlightType = 'inserted' - let decorationKey = 'left'; - let effects: StateEffect[] = view.state.selection.ranges - .filter(r => !r.empty) - .map(({ from, to }) => addHighlight.of({ from, to, highlightType, decorationKey })); - view.dispatch({ effects }); - return true; -} +export const PaddingWidgetField = StateField.define({ + create: () => Decoration.none, + update: (paddingWidgetRanges, transaction) => { + for (let e of transaction.effects) { + if (e.is(addPaddingWidgetEffect)) { + return e.value; + } + } + return paddingWidgetRanges.map(transaction.changes); + }, + provide: field => EditorView.decorations.from(field) +}); -function rightHighlightInsertSelection(view: EditorView) { - let highlightType = 'inserted' - let decorationKey = 'right'; - let effects: StateEffect[] = view.state.selection.ranges - .filter(r => !r.empty) - .map(({ from, to }) => addHighlight.of({ from, to, highlightType, decorationKey })); - view.dispatch({ effects }); - return true; -}*/ -/***********************start decoration widget and related statefield***********************************/ -const addPaddingWidgetEffect = StateEffect.define<{ offset: number; size: number; above: boolean }>({ +/*const oldAddPaddingWidgetEffect = StateEffect.define<{ offset: number; size: number; above: boolean }>({ map: ({ offset, size, above }, mapping) => ({ offset: mapping.mapPos(offset), size, above }) -}); +});*/ -const PaddingWidgetField = StateField.define({ +/*const oldPaddingWidgetField = StateField.define({ create: () => { return Decoration.none; }, update: (paddingWidgetRanges, transaction) => { paddingWidgetRanges = paddingWidgetRanges.map(transaction.changes); for (let e of transaction.effects) { - if (e.is(addPaddingWidgetEffect)) + if (e.is(oldAddPaddingWidgetEffect)) { paddingWidgetRanges = paddingWidgetRanges.update({ - add: [addPaddingWidget(e.value.offset, e.value.size, e.value.above)] + add: [oldAddPaddingWidget(e.value.offset, e.value.size, e.value.above)] }); } + } return paddingWidgetRanges; }, provide: field => EditorView.decorations.from(field) -}); +});*/ + class PaddingWidget extends WidgetType { constructor(size: number) { super(); @@ -336,12 +304,7 @@ function posToOffset(doc: any, pos: any) { return doc.line(pos.line + 1).from + pos.ch; } -/* function offsetToPos(doc, offset) { - let line = doc.lineAt(offset) - return {line: line.number - 1, ch: offset - line.from} -} */ - -function addPaddingWidget( +/*function oldAddPaddingWidget( pos: number, size: number, above: boolean @@ -352,45 +315,7 @@ function addPaddingWidget( side: above? -1 : 1 }); return deco.range(pos); -} - -/*function addLineWidgetFromUI(view: EditorView) { - const cursor = view.state.selection.main.head; - const line: Line = view.state.doc.lineAt(cursor); - let effects: StateEffect[] = []; - effects.push(addLineWidgetEffect.of({line: line.number, size: 40})); - view.dispatch({effects}); - return true; - }*/ -/**************************end decoration widget and related statefield************************************************* */ -/*const decorationKeymap = keymap.of([ - { - key: 'Alt-u', - preventDefault: true, - run: leftHighlightChunkSelection - }, - { - key: 'Alt-v', - preventDefault: true, - run: leftHighlightInsertSelection - }, - { - key: 'Alt-w', - preventDefault: true, - run: rightHighlightChunkSelection - }, - { - key: 'Alt-x', - preventDefault: true, - run: rightHighlightInsertSelection - }, - { - key: 'Alt-h', - preventDefault: true, - run: addLineWidgetFromUI - }], -)*/ /** * @@ -447,18 +372,17 @@ export class DiffView { constructor( model: IStringDiffModel, type: 'left' | 'right' | 'merge', - updateCallback: (force?: boolean) => void, + listener: Extension, options: IMergeViewEditorConfiguration ) { this.model = model; this.type = type; - this.updateCallback = updateCallback; //this.classes = type === 'left' ? //leftClasses : type === 'right' ? rightClasses : null; let remoteValue = this.model.remote || ''; //this.remoteEditorWidget = new EditorWidget(remoteValue, copyObj({readOnly: !!options.readOnly}, options)); this._remoteEditorWidget = new EditorWidget(remoteValue); // OPTIONS TO BE GIVEN - this._remoteEditorWidget.editor.injectExtension(getCommonEditorExtensions()); + this._remoteEditorWidget.editor.injectExtension([listener, getCommonEditorExtensions()]); this.showDifferences = options.showDifferences !== false; } /* @@ -484,6 +408,7 @@ init(base: CodeMirror.Editor) { init(baseWidget: EditorWidget) { this.baseEditorWidget = baseWidget; const baseEditor = this.baseEditorWidget.cm; + this.lineHeight = baseEditor.defaultLineHeight; const remoteEditor = this.remoteEditorWidget.cm; const baseEditorPlugin = baseEditor.plugin(diffViewPlugin); const remoteEditorPlugin = remoteEditor.plugin(diffViewPlugin); @@ -522,15 +447,12 @@ init(base: CodeMirror.Editor) { DIFF_OP.DIFF_DELETE ); - - - /*this.dealigned = false; + this.dealigned = false; this.forceUpdate = this.registerUpdate(); this.setScrollLock(true, false); this.registerScroll(); - */ } update() {} @@ -1185,6 +1107,7 @@ init(base: CodeMirror.Editor) { updating: boolean; updatingFast: boolean; collapsedRanges: { line: number; size: number }[] = []; + lineHeight: number; protected updateCallback: (force?: boolean) => void; protected copyButtons: HTMLElement; @@ -1276,7 +1199,10 @@ init(base: CodeMirror.Editor) { /** * From a line in base, find the matching line in another editor by line chunks + * */ + + function getMatchingEditLineLC(toMatch: Chunk, chunks: Chunk[]): number { let editLine = toMatch.baseFrom; for (let i = 0; i < chunks.length; ++i) { @@ -1298,6 +1224,171 @@ function getMatchingEditLineLC(toMatch: Chunk, chunks: Chunk[]): number { * [ aligned line #1:[Edit line number, (DiffView#1 line number, DiffView#2 line number,) ...], * algined line #2 ..., etc.] */ +/*function findAlignedLines(dvs: DiffView[]): number[][] { + let linesToAlign: number[][] = []; + let ignored: number[] = []; + + // First fill directly from first DiffView + let dv = dvs[0]; + let others = dvs.slice(1); + for (let i = 0; i < dv.lineChunks.length; i++) { + let chunk = dv.lineChunks[i]; + let lines = [chunk.baseTo, chunk.remoteTo]; + for (let o of others) { + lines.push(getMatchingEditLineLC(chunk, o.lineChunks)); + } + if (linesToAlign.length > 0 && + linesToAlign[linesToAlign.length - 1][0] === lines[0]) { + let last = linesToAlign[linesToAlign.length - 1]; + for (let j = 0; j < lines.length; ++j) { + last[j] = Math.max(last[j], lines[j]); + } + } else { + if (linesToAlign.length > 0) { + let prev = linesToAlign[linesToAlign.length - 1]; + let diff: number | null = lines[0] - prev[0]; + for (let j = 1; j < lines.length; ++j) { + if (diff !== lines[j] - prev[j]) { + diff = null; + break; + } + } + if (diff === null) { + linesToAlign.push(lines); + } else { + ignored.push(lines[0]); + continue; + } + } else { + linesToAlign.push(lines); + } + } + } + // Then fill any chunks from remaining DiffView, which are not already added + for (let o = 0; o < others.length; o++) { + for (let i = 0; i < others[o].lineChunks.length; i++) { + let chunk = others[o].lineChunks[i]; + // Check against existing matches to see if already consumed: + let j = 0; + for (; j < linesToAlign.length; j++) { + let align = linesToAlign[j]; + if (valueIn(chunk.baseTo, ignored)) { + // Chunk already consumed, continue to next chunk + j = -1; + break; + } else if (align[0] >= chunk.baseTo) { + // New chunk, which should be inserted in pos j, + // such that linesToAlign are sorted on edit line + break; + } + } + if (j > -1) { + let lines = [chunk.baseTo, + getMatchingEditLineLC(chunk, dv.lineChunks)]; + for (let k = 0; k < others.length; k++) { + if (k === o) { + lines.push(chunk.remoteTo); + } else { + lines.push(getMatchingEditLineLC(chunk, others[k].lineChunks)); + } + } + if (linesToAlign.length > j && linesToAlign[j][0] === chunk.baseTo) { + let last = linesToAlign[j]; + for (let k = 0; k < lines.length; ++k) { + last[k] = Math.max(last[k], lines[k]); + } + } else { + linesToAlign.splice(j, 0, lines); + } + } + } + } + return linesToAlign; +}*/ + +/* +function findAlignedLines(dvs: DiffView[]): number[][] { + let linesToAlign: number[][] = []; + let ignored: number[] = []; + + // First fill directly from first DiffView + let dv = dvs[0]; + let others = dvs.slice(1); + for (let i = 0; i < dv.lineChunks.length; i++) { + let chunk = dv.lineChunks[i]; + let lines = [chunk.baseTo, chunk.remoteTo]; + for (let o of others) { + lines.push(getMatchingEditLineLC(chunk, o.lineChunks)); + } + if (linesToAlign.length > 0 && + linesToAlign[linesToAlign.length - 1][0] === lines[0]) { + let last = linesToAlign[linesToAlign.length - 1]; + for (let j = 0; j < lines.length; ++j) { + last[j] = Math.max(last[j], lines[j]); + } + } else { + if (linesToAlign.length > 0) { + let prev = linesToAlign[linesToAlign.length - 1]; + let diff: number | null = lines[0] - prev[0]; + for (let j = 1; j < lines.length; ++j) { + if (diff !== lines[j] - prev[j]) { + diff = null; + break; + } + } + if (diff === null) { + linesToAlign.push(lines); + } else { + ignored.push(lines[0]); + continue; + } + } else { + linesToAlign.push(lines); + } + } + } + // Then fill any chunks from remaining DiffView, which are not already added + for (let o = 0; o < others.length; o++) { + for (let i = 0; i < others[o].lineChunks.length; i++) { + let chunk = others[o].lineChunks[i]; + // Check against existing matches to see if already consumed: + let j = 0; + for (; j < linesToAlign.length; j++) { + let align = linesToAlign[j]; + if (valueIn(chunk.baseTo, ignored)) { + // Chunk already consumed, continue to next chunk + j = -1; + break; + } else if (align[0] >= chunk.baseTo) { + // New chunk, which should be inserted in pos j, + // such that linesToAlign are sorted on edit line + break; + } + } + if (j > -1) { + let lines = [chunk.baseTo, + getMatchingEditLineLC(chunk, dv.lineChunks)]; + for (let k = 0; k < others.length; k++) { + if (k === o) { + lines.push(chunk.remoteTo); + } else { + lines.push(getMatchingEditLineLC(chunk, others[k].lineChunks)); + } + } + if (linesToAlign.length > j && linesToAlign[j][0] === chunk.baseTo) { + let last = linesToAlign[j]; + for (let k = 0; k < lines.length; ++k) { + last[k] = Math.max(last[k], lines[k]); + } + } else { + linesToAlign.splice(j, 0, lines); + } + } + } + } + return linesToAlign; +}*/ + function findAlignedLines(dvs: DiffView[]): number[][] { let linesToAlign: number[][] = []; let ignored: number[] = []; @@ -1342,7 +1433,7 @@ function findAlignedLines(dvs: DiffView[]): number[][] { for (let o = 0; o < others.length; o++) { for (let i = 0; i < others[o].lineChunks.length; i++) { let chunk = others[o].lineChunks[i]; - // Check agains existing matches to see if already consumed: + // Check against existing matches to see if already consumed: let j = 0; for (; j < linesToAlign.length; j++) { let align = linesToAlign[j]; @@ -1380,6 +1471,9 @@ function findAlignedLines(dvs: DiffView[]): number[][] { return linesToAlign; } + + + /*function alignLines(cm: CodeMirror.Editor[], lines: number[], aligners: CodeMirror.LineWidget[]): void { let maxOffset = 0; let offset: number[] = []; @@ -1401,68 +1495,51 @@ function findAlignedLines(dvs: DiffView[]): number[][] { } */ -/* CM6 */ -function alignLines(editors: EditorView[], lines: number[]): void { +/* CM6 with as much effects as padding widgets */ +/*function alignLines(editors: EditorView[], lines: number[]): void { let maxPosFromTop = 0; let posFromTop: number[] = []; /*top position of the padding relative to the top of the document */ - let effects: StateEffect[] = []; - let editorNames: string[] = ['base', 'left','right', 'merge']; + //let editorNames: string[] = ['base', 'left', 'right', 'merge']; + //let lineHeight: number; + /*for (let i = 0; i < editors.length; i++) { + - for (let i = 0; i < editors.length; i++) { if (lines[i] !== null) { - let offset = editors[i].state.doc.line(lines[i]).from; - console.log('offset:', offset); + let offset: number = editors[i].state.doc.line(lines[i]).from+1; posFromTop[i] = editors[i].lineBlockAt(offset).top; + //lineHeight = editors[i].lineBlockAt(offset).height; maxPosFromTop = Math.max(maxPosFromTop, posFromTop[i]); - }; + } } for (let i = 0; i < editors.length; i++) { if (lines[i] !== null) { - let height = maxPosFromTop - posFromTop[i]; - console.log('*******************************'); - console.log(editorNames[i]); - console.log('posFromTop:', posFromTop); - console.log('height', height); - console.log('lines[i]:', lines[i]); - if (height > 1) { /* height is in pixels*/ - effects.push(createPaddingEffect(editors[i], lines[i], height)); - editors[i].dispatch({ effects:effects }); + let height = (maxPosFromTop - posFromTop[i]); + if (height > 1) { + const transaction : TransactionSpec = { effects: oldCreatePaddingEffect(editors[i], lines[i], height) } + editors[i].dispatch(transaction); } } + ; } -} - -/* function padAbove(cm: CodeMirror.Editor, line: number, size: number): CodeMirror.LineWidget { - let above = true; - if (line > cm.getDoc().lastLine()) { - line--; - above = false; - } - let elt = document.createElement('div'); - elt.className = 'CodeMirror-merge-spacer'; - elt.style.height = size + 'px'; elt.style.minWidth = '1px'; - return cm.addLineWidget(line, elt, {height: size, above: above}); -} -*/ +}*/ -/* Replaces the CM5 padAbove function */ -function createPaddingEffect(editor: EditorView, line: number, size: number) { +/* Replaces the CM5 padAbove function with one padding widget by effect */ +/*function oldCreatePaddingEffect(editor: EditorView, line: number, size: number) { let above = false; let offset: number = editor.state.doc.length; - if (line <= editor.state.doc.lines) { + if (line < editor.state.doc.lines) { above = true; + offset = editor.state.doc.line(line).from; } - offset = editor.state.doc.line(line).from; - console.log('In createPadding, offset is:', offset); - console.log('In createPadding, line is:', line); - const effect = addPaddingWidgetEffect.of({ + + const effect = oldAddPaddingWidgetEffect.of({ offset: offset, size: size, above: above }); return effect; -} +}*/ export interface IMergeViewEditorConfiguration extends LegacyCodeMirror.EditorConfiguration { @@ -1508,8 +1585,9 @@ export interface IMergeViewEditorConfiguration // Merge view, containing 1 or 2 diff views. export class MergeView extends Panel { constructor(options: IMergeViewEditorConfiguration) { - super(); + super() this.options = options; + this.measuring = -1; let remote = options.remote; let local = options.local || null; let merged = options.merged || null; @@ -1519,7 +1597,6 @@ export class MergeView extends Panel { let merge: DiffView | null = (this.merge = null); //let self = this; this.diffViews = []; - /*this.aligners = [];*/ let main = options.remote || options.merged; if (!main) { throw new Error('Either remote or merged model needs to be specified!'); @@ -1530,6 +1607,13 @@ export class MergeView extends Panel { let readOnly = options.readOnly; // For all others: options.readOnly = true; + this.aligning = true; + const listener = EditorView.updateListener.of(update => { + if (this.measuring < 0 && (/*update.heightChanged || */update.viewportChanged) + && !update.transactions.some(tr => tr.effects.some(e => e.is(addPaddingWidgetEffect)))) { + this.alignViews(); + } + }); /* * Different cases possible: @@ -1559,7 +1643,8 @@ export class MergeView extends Panel { //this.base = new EditorWidget(options.value, copyObj({readOnly: !!options.readOnly}, options)); this.base = new EditorWidget(options.value); - this.base.editor.injectExtension(getCommonEditorExtensions()); + this.base.editor.injectExtension([listener, getCommonEditorExtensions()]); + if (merged) { let showBase = options.showBase !== false; @@ -1579,7 +1664,7 @@ export class MergeView extends Panel { left = this.left = new DiffView( local, 'left', - this.alignViews.bind(this), + listener, copyObj(dvOptions) ); this.diffViews.push(left); @@ -1606,7 +1691,7 @@ export class MergeView extends Panel { right = this.right = new DiffView( remote, 'right', - this.alignViews.bind(this), + listener, copyObj(dvOptions) ); this.diffViews.push(right); @@ -1623,7 +1708,7 @@ export class MergeView extends Panel { merge = this.merge = new DiffView( merged, 'merge', - this.alignViews.bind(this), + listener, copyObj({ readOnly }, copyObj(dvOptions)) ); this.diffViews.push(merge); @@ -1647,7 +1732,7 @@ export class MergeView extends Panel { right = this.right = new DiffView( remote, 'right', - this.alignViews.bind(this), + listener, dvOptions ); this.diffViews.push(right); @@ -1684,10 +1769,11 @@ export class MergeView extends Panel { dv.collapsedRanges = this.collapsedRanges; } } - this.initialized = true; - if (this.left || this.right || this.merge) { + this.aligning = false; + /*if (this.left || this.right || this.merge) { this.alignViews(true); - } + }*/ + this.scheduleAlignViews(); } ////////////////////////////END OF CONSTRUCTOR////////////////////////////////// @@ -1763,25 +1849,28 @@ export class MergeView extends Panel { /*CM6 version */ alignViews(force?: boolean) { - //console.log('Enter alighViews:'); - let dealigned = false; - if (!this.initialized) { + if (this.aligning) { return; } - for (let dv of this.diffViews) { + //let dealigned = false; + this.aligning = true; + + /*for (let dv of this.diffViews) { dv.syncModel(); if (dv.dealigned) { dealigned = true; dv.dealigned = false; } } + */ - if (!dealigned && !force) { + /*if (!dealigned && !force) { return; // Nothing to do } + */ // Find matching lines let linesToAlign = findAlignedLines(this.diffViews); - //console.log('linesToAlign:', linesToAlign); + // Function modifying DOM to perform alignment: let self: MergeView = this; @@ -1791,7 +1880,6 @@ export class MergeView extends Panel { //let aligners = self.aligners; /*for (let i = 0; i < aligners.length; i++) { /*aligners[i].clear();*/ - //console.log('implement a clear method for the aligners') //} //aligners.length = 0; @@ -1803,24 +1891,70 @@ export class MergeView extends Panel { }*/ let editors: EditorView[] = [self.base.cm]; + let builders: RangeSetBuilder[] = []; /*let scroll: number[] = [];*/ for (let dv of self.diffViews) { editors.push(dv.remoteEditorWidget.cm); } + for (let i = 0; i < editors.length; i++) { + builders.push(new RangeSetBuilder()); /*scroll.push(cm[i].getScrollInfo().top);*/ } for (let ln = 0; ln < linesToAlign.length; ln++) { - alignLines(editors, linesToAlign[ln]); + let lines = linesToAlign[ln]; + let maxPosFromTop: number = 0; + let posFromTop: number[] = []; /*top position of the padding relative to the top of the document */ + + for (let i = 0; i < editors.length; i++) { + if (lines[i] !== null) { + let offset: number = editors[i].state.doc.line(lines[i]).from + 1; + posFromTop[i] = editors[i].lineBlockAt(offset).top; + maxPosFromTop = Math.max(maxPosFromTop, posFromTop[i]); + console.log('maxPosFromTop:', maxPosFromTop); + } + } + for (let i = 0; i < editors.length; i++) { + if (lines[i] !== null) { + let height: number = maxPosFromTop - posFromTop[i]; + let side = 1; // padding inserted below + let offset = editors[i].state.doc.length; + if (lines[i] < editors[i].state.doc.lines) { + side = -1; // padding inserted above for all lines excepted the last one + offset = editors[i].state.doc.line(lines[i]).from; + } + if(height>1) { + builders[i].add(offset, offset, Decoration.widget({ + widget: new PaddingWidget(height), + block: true, + side: side + })) + } + } + } + } + + for (let i = 0; i < editors.length; i++) { + let decoSet: DecorationSet = builders[i].finish(); + editors[i].dispatch({ effects: addPaddingWidgetEffect.of(decoSet) }); } - for (let i = 0; i < editors.length; i++) { + for (let i = 0; i < editors.length; i++) { /* editors[i].scrollTo(null, scroll[i]);*/ } + this.aligning = false; }; - + scheduleAlignViews() { + if (this.measuring < 0) { + let win = (this.gridPanel.node.ownerDocument.defaultView || window) + this.measuring = win.requestAnimationFrame(() => { + this.measuring = -1; + this.alignViews(); + }) + } + } setShowDifferences(val: boolean) { /* if (this.right) { @@ -1848,8 +1982,8 @@ export class MergeView extends Panel { base: EditorWidget; options: any; diffViews: DiffView[]; - //aligners: PaddingWidget[]; - initialized: boolean = false; + aligning: boolean; + measuring: number; collapsedRanges: { size: number; line: number }[] = []; } diff --git a/packages/nbdime/src/styles/merge.css b/packages/nbdime/src/styles/merge.css index a90da909..79f2bad3 100644 --- a/packages/nbdime/src/styles/merge.css +++ b/packages/nbdime/src/styles/merge.css @@ -89,50 +89,99 @@ .jp-Notebook-merge .cm-merge-spacer { background-color: #eee; } -.jp-Notebook-merge .cm-merge-r-chunk { background-color: var(--jp-merge-remote-color2); } +.jp-Notebook-merge .cm-merge-r-chunk { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; + background-color: var(--jp-merge-remote-color2); + box-sizing: border-box; } .jp-Notebook-merge .cm-merge-r-chunk-start, -.jp-Notebook-merge .cm-merge-m-chunk-start-remote { border-top: 1px solid var(--jp-merge-remote-color1); } +.jp-Notebook-merge .cm-merge-m-chunk-start-remote { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; + border-top: 1px solid var(--jp-merge-remote-color1); + box-sizing: border-box } .jp-Notebook-merge .cm-merge-r-chunk-end, -.jp-Notebook-merge .cm-merge-m-chunk-end-remote { border-bottom: 1px solid var(--jp-merge-remote-color1); } +.jp-Notebook-merge .cm-merge-m-chunk-end-remote { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; + border-bottom: 1px solid var(--jp-merge-remote-color1); + box-sizing: border-box } .jp-Notebook-merge .cm-merge-r-connect { fill: var(--jp-merge-remote-color2); stroke: var(--jp-merge-remote-color1); stroke-width: 1px; } .jp-Notebook-merge .cm-line .cm-merge-r-inserted { background-color: var(--jp-merge-remote-color1); } .jp-Notebook-merge .cm-merge-r-chunk-end-empty + .cm-linewidget .cm-merge-spacer, .jp-Notebook-merge .cm-merge-m-chunk-end-remote-empty + .cm-linewidget .cm-merge-spacer { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; border-bottom: 1px solid var(--jp-merge-remote-color1); background-color: var(--jp-merge-remote-color3); } .jp-Notebook-merge .cm-merge-l-chunk { background-color: var(--jp-merge-local-color2); } .jp-Notebook-merge .cm-merge-l-chunk-start, -.jp-Notebook-merge .cm-merge-m-chunk-start-local { border-top: 1px solid var(--jp-merge-local-color1); } +.jp-Notebook-merge .cm-merge-m-chunk-start-local { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; + border-top: 1px solid var(--jp-merge-local-color1); + box-sizing: border-box } .jp-Notebook-merge .cm-merge-l-chunk-end, -.jp-Notebook-merge .cm-merge-m-chunk-end-local { border-bottom: 1px solid var(--jp-merge-local-color1); } +.jp-Notebook-merge .cm-merge-m-chunk-end-local { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; + border-bottom: 1px solid var(--jp-merge-local-color1); + box-sizing: border-box } .jp-Notebook-merge .cm-merge-l-connect { fill: var(--jp-merge-local-color2); stroke: var(--jp-merge-local-color1); stroke-width: 1px; } .jp-Notebook-merge .cm-line .cm-merge-l-inserted { background-color: var(--jp-merge-local-color1) } .jp-Notebook-merge .cm-merge-l-chunk-end-empty + .cm-linewidget .cm-merge-spacer, .jp-Notebook-merge .cm-merge-m-chunk-end-local-empty + .cm-linewidget .cm-merge-spacer { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; border-bottom: 1px solid var(--jp-merge-local-color1); background-color: var(--jp-merge-local-color3); + box-sizing: border-box; } .jp-Notebook-merge .cm-merge-m-chunk-custom { background-color: #ffb; } -.jp-Notebook-merge .cm-merge-m-chunk-start-custom { border-top: 1px solid #fe6; } -.jp-Notebook-merge .cm-merge-m-chunk-end-custom { border-bottom: 1px solid #fe6; } +.jp-Notebook-merge .cm-merge-m-chunk-start-custom { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; + border-top: 1px solid #fe6; + box-sizing: border-box; +} + +.jp-Notebook-merge .cm-merge-m-chunk-end-custom { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; + border-bottom: 1px solid #fe6; + box-sizing: border-box; +} .jp-Notebook-merge .cm-merge-l-chunk .cm-merge-r-chunk { background: var(--jp-merge-both-color2); } -.jp-Notebook-merge .cm-merge-l-chunk-start .cm-merge-r-chunk-start { border-top: 1px solid var(--jp-merge-both-color1); } -.jp-Notebook-merge .cm-merge-l-chunk-end .cm-merge-r-chunk-end { border-bottom: 1px solid var(--jp-merge-both-color1); } +.jp-Notebook-merge .cm-merge-l-chunk-start .cm-merge-r-chunk-start { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; + border-top: 1px solid var(--jp-merge-both-color1); + box-sizing: border-box; +} +.jp-Notebook-merge .cm-merge-l-chunk-end .cm-merge-r-chunk-end { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; + border-bottom: 1px solid var(--jp-merge-both-color1); + box-sizing: border-box } .jp-Notebook-merge .cm-merge-l-chunk-end-empty .cm-merge-r-chunk-end-empty + .cm-linewidget .cm-merge-spacer, .jp-Notebook-merge .cm-merge-m-chunk-end-remote-empty .cm-merge-m-chunk-end-local-empty + .cm-linewidget .cm-merge-spacer { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; border-bottom: 1px solid var(--jp-merge-both-color1); background-color: var(--jp-merge-both-color2); + box-sizing: border-box ; } + .jp-Notebook-merge .cm-merge-r-chunk-end-empty, .jp-Notebook-merge .cm-merge-l-chunk-end-empty, .jp-Notebook-merge .cm-merge-m-chunk-end-empty, @@ -145,16 +194,25 @@ .jp-Notebook-merge .cm-merge-m-chunk-start-either, .jp-Notebook-merge .cm-merge-m-chunk-start-mixed:not(.cm-merge-m-chunk-custom) { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; border-top: 1px solid var(--jp-merge-either-color1); + box-sizing: border-box ; } .jp-Notebook-merge .cm-merge-m-chunk-end-either, .jp-Notebook-merge .cm-merge-m-chunk-end-mixed:not(.cm-merge-m-chunk-custom) { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; border-bottom: 1px solid var(--jp-merge-either-color1); + box-sizing: border-box ; } .jp-Notebook-merge .cm-merge-m-chunk-end-either-empty + .cm-linewidget .cm-merge-spacer { + /*height: var(--jp-code-line-height);*/ + height: 1.4 em; border-bottom: 1px solid var(--jp-merge-either-color1); background-color: var(--jp-merge-either-color2); + box-sizing: border-box ; } .jp-Notebook-merge .cm-merge-pane-base .cm-line .cm-merge-m-deleted,