diff --git a/examples/center.ipynb b/examples/example1/center.ipynb similarity index 100% rename from examples/center.ipynb rename to examples/example1/center.ipynb diff --git a/examples/left.ipynb b/examples/example1/left.ipynb similarity index 100% rename from examples/left.ipynb rename to examples/example1/left.ipynb diff --git a/examples/right.ipynb b/examples/example1/right.ipynb similarity index 100% rename from examples/right.ipynb rename to examples/example1/right.ipynb diff --git a/examples/example2/center.ipynb b/examples/example2/center.ipynb new file mode 100644 index 00000000..717a6db2 --- /dev/null +++ b/examples/example2/center.ipynb @@ -0,0 +1,39 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "eabfe4e0-d646-4d09-95a4-09ee3d030db6", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "import numpy as np\n", + "x = np.arange(0, 2, 0.1)\n", + "y1 = np.exp(x)\n", + "plt.plot(x, y1, 'b')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/example2/left.ipynb b/examples/example2/left.ipynb new file mode 100644 index 00000000..69ad6582 --- /dev/null +++ b/examples/example2/left.ipynb @@ -0,0 +1,40 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "2d47bfe2-d91d-4f72-9426-1885c4edb5a9", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "import numpy as np\n", + "x = np.linspace(0, 2, 10)\n", + "z = np.exp(x + 2)\n", + "plt.plot(x, z, 'c')\n", + "# this is an example" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/example2/right.ipynb b/examples/example2/right.ipynb new file mode 100644 index 00000000..f63e628e --- /dev/null +++ b/examples/example2/right.ipynb @@ -0,0 +1,42 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ff160690-22c8-48f3-9b89-d78a8997baec", + "metadata": {}, + "outputs": [], + "source": [ + "from matplotlib import pyplot as plt\n", + "import numpy as np\n", + "x = np.arange(0, 2, 0.1)\n", + "y2 = np.exp(x)\n", + "plt.plot(x, y2, 'b')\n", + "y1 = y2\n", + "plt.plot(x, y1, 'g')\n", + "# this is easy" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/example3/center.ipynb b/examples/example3/center.ipynb new file mode 100644 index 00000000..34c52067 --- /dev/null +++ b/examples/example3/center.ipynb @@ -0,0 +1,65 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "9e423ee0-5ba3-4792-8b16-b20fe7a775e3", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd # programmers like shortening names for things, so they usually import pandas as \"pd\"\n", + "import altair as alt # again with the shortening of names\n", + "import requests # we use it for downloading the data so we don't have to save it on GitHub (too big!)\n", + "import zipfile # for de-compressing the files we download from the EPA" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08e85e68-1722-4847-a13f-97c921829073", + "metadata": {}, + "outputs": [], + "source": [ + "# Download the data from the EPA website\n", + "data_file_urls = [\n", + " 'https://aqs.epa.gov/aqsweb/airdata/daily_88101_2020.zip',\n", + " 'https://aqs.epa.gov/aqsweb/airdata/daily_88101_2019.zip',\n", + "]\n", + "# copied this example from https://stackoverflow.com/questions/16694907/download-large-file-in-python-with-requests\n", + "for url in data_file_urls:\n", + " local_filename = \"data/{}\".format(url.split('/')[-1])\n", + " with requests.get(url, stream=True) as r:\n", + " r.raise_for_status()\n", + " with open(local_filename, 'wb') as f:\n", + " for chunk in r.iter_content(chunk_size=8192): \n", + " f.write(chunk)\n", + "# and unzip the files\n", + "files_to_unzip = [\"data/{}\".format(url.split('/')[-1]) for url in data_file_urls]\n", + "for f in files_to_unzip:\n", + " with zipfile.ZipFile(f,\"r\") as zip_ref:\n", + " zip_ref.extractall(\"data\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/example3/left.ipynb b/examples/example3/left.ipynb new file mode 100644 index 00000000..7dcf4fe4 --- /dev/null +++ b/examples/example3/left.ipynb @@ -0,0 +1,67 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "9e423ee0-5ba3-4792-8b16-b20fe7a775e3", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd # programmers like shortening names for things, so they usually import pandas as \"pd\"\n", + "import altair as alt # again with the shortening of names\n", + "import requests # we use it for downloading the data so we don't have to save it on GitHub (too big!)\n", + "import zipfile # for de-compressing the files we download from the EPA\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08e85e68-1722-4847-a13f-97c921829073", + "metadata": {}, + "outputs": [], + "source": [ + "# Download the data from the EPA website\n", + "data_file_urls = [\n", + " 'https://aqs.epa.gov/aqsweb/airdata/daily_88101_2020.zip',\n", + " 'https://aqs.epa.gov/aqsweb/airdata/daily_88101_2019.zip',\n", + " 'https://aqs.epa.gov/aqsweb/airdata/daily_88101_2018.zip'\n", + "]\n", + "# copied this example from https://stackoverflow.com/questions/16694907/download-large-file-in-python-with-requests\n", + "for url in data_file_urls:\n", + " local_filename = \"data/{}\".format(url.split('/')[-1])\n", + " with requests.get(url, stream=True) as r:\n", + " r.raise_for_status()\n", + " with open(local_filename, 'wb') as f:\n", + " for chunk in r.iter_content(chunk_size=8192): \n", + " f.write(chunk)\n", + "# and unzip the files\n", + "files_to_unzip = [\"data/{}\".format(url.split('/')[-1]) for url in data_file_urls]\n", + "for f in files_to_unzip:\n", + " with zipfile.ZipFile(f,\"r\") as zip_ref:\n", + " zip_ref.extractall(\"data\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/example3/right.ipynb b/examples/example3/right.ipynb new file mode 100644 index 00000000..e060abb0 --- /dev/null +++ b/examples/example3/right.ipynb @@ -0,0 +1,60 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "9e423ee0-5ba3-4792-8b16-b20fe7a775e3", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd # programmers like shortening names for things, so they usually import pandas as \"pd\"\n", + "import superior as sup # again with the shortening of names\n", + "import requests # we use it for downloading the data so we don't have to save it on GitHub (too big!)\n", + "import tarfile # for de-compressing the files we download from the EPA" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08e85e68-1722-4847-a13f-97c921829073", + "metadata": {}, + "outputs": [], + "source": [ + "# Download the data from the EPA website\n", + "data_file_urls = [\n", + " 'https://aqs.epa.gov/aqsweb/airdata/daily_88101_2017.zip',\n", + " 'https://aqs.epa.gov/aqsweb/airdata/daily_88101_2018.zip',\n", + "]\n", + "# copied this example from https://stackoverflow.com/questions/16694907/download-large-file-in-python-with-requests\n", + "for url in data_file_urls:\n", + " local_filename = \"data/{}\".format(url.split('/')[-1])\n", + " with requests.get(url, stream=True) as r:\n", + " r.raise_for_status()\n", + " with open(local_filename, 'r') as f:\n", + " for chunk in r.iter_content(chunk_size=8192): \n", + " f.write(chunk)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/package-lock.json b/package-lock.json index afbbd907..f08dbf52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1862,13 +1862,13 @@ } }, "node_modules/@codemirror/lang-python": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.2.tgz", - "integrity": "sha512-nbQfifLBZstpt6Oo4XxA2LOzlSp4b/7Bc5cmodG1R+Cs5PLLCTUvsMNWDnziiCfTOG/SW1rVzXq/GbIr6WXlcw==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.3.tgz", + "integrity": "sha512-S9w2Jl74hFlD5nqtUMIaXAq9t5WlM0acCkyuQWUUSvZclk1sV+UfnpFiZzuZSG+hfEaOmxKR5UxY/Uxswn7EhQ==", "dependencies": { "@codemirror/autocomplete": "^6.3.2", - "@codemirror/language": "^6.0.0", - "@lezer/python": "^1.0.0" + "@codemirror/language": "^6.8.0", + "@lezer/python": "^1.1.4" } }, "node_modules/@codemirror/lang-rust": { @@ -1915,9 +1915,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.7.0.tgz", - "integrity": "sha512-4SMwe6Fwn57klCUsVN0y4/h/iWT+XIXFEmop2lIHHuWO0ubjCrF3suqSZLyOQlznxkNnNbOOfKe5HQbQGCAmTg==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.8.0.tgz", + "integrity": "sha512-r1paAyWOZkfY0RaYEZj3Kul+MiQTEbDvYqf8gPGaRvNneHXCmfSaAVFjwRUPlgxS8yflMxw2CTu6uCMp8R8A2g==", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", @@ -21744,6 +21744,7 @@ "version": "6.2.0", "license": "BSD-3-Clause", "dependencies": { + "@codemirror/lang-python": "^6.1.3", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.9.6", "@jupyterlab/codeeditor": "^4.0.0", diff --git a/packages/nbdime/package.json b/packages/nbdime/package.json index fcb45557..957ad8d6 100644 --- a/packages/nbdime/package.json +++ b/packages/nbdime/package.json @@ -21,6 +21,7 @@ "dependencies": { "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.9.6", + "@codemirror/lang-python": "^6.1.3", "@jupyterlab/codeeditor": "^4.0.0", "@jupyterlab/codemirror": "^4.0.0", "@jupyterlab/coreutils": "^6.0.0", diff --git a/packages/nbdime/src/common/mergeview.bak b/packages/nbdime/src/common/mergeview.bak deleted file mode 100644 index 352d5ade..00000000 --- a/packages/nbdime/src/common/mergeview.bak +++ /dev/null @@ -1,1451 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -// This code is based on the CodeMirror mergeview.js source: -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -'use strict'; - -import * as CodeMirror from 'codemirror'; - -import { Widget, Panel } from '@lumino/widgets'; - -import type { IStringDiffModel } from '../diff/model'; - -import { DecisionStringDiffModel } from '../merge/model'; - -import type { DiffRangePos } from '../diff/range'; - -import { ChunkSource, Chunk, lineToNormalChunks } from '../chunking'; - -import { EditorWidget } from './editor'; - -/* import { - valueIn, hasEntries, splitLines, copyObj -} from './util'; */ - -import { valueIn, hasEntries, copyObj } from './util'; - -import { NotifyUserError } from './exceptions'; - -import type { EditorView } from '@codemirror/view'; - -const PICKER_SYMBOL = '\u27ad'; - -const CONFLICT_MARKER = '\u26A0'; // '\u2757' - -export type Marker = CodeMirror.LineHandle | CodeMirror.TextMarker; - -export enum DIFF_OP { - DIFF_DELETE = -1, - DIFF_INSERT = 1, - DIFF_EQUAL = 0 -} - -export enum EventDirection { - INCOMING, - OUTGOING -} - -export type DiffClasses = { - [key: string]: string; - chunk: string; - start: string; - end: string; - insert: string; - del: string; - connect: string; - gutter: string; -}; - -const GUTTER_PICKER_CLASS = 'jp-Merge-gutter-picker'; -const GUTTER_CONFLICT_CLASS = 'jp-Merge-gutter-conflict'; - -const CHUNK_CONFLICT_CLASS = 'jp-Merge-conflict'; - -const leftClasses: DiffClasses = { - chunk: 'CodeMirror-merge-l-chunk', - start: 'CodeMirror-merge-l-chunk-start', - end: 'CodeMirror-merge-l-chunk-end', - insert: 'CodeMirror-merge-l-inserted', - del: 'CodeMirror-merge-l-deleted', - connect: 'CodeMirror-merge-l-connect', - gutter: 'CodeMirror-merge-l-gutter' -}; -const rightClasses: DiffClasses = { - chunk: 'CodeMirror-merge-r-chunk', - start: 'CodeMirror-merge-r-chunk-start', - end: 'CodeMirror-merge-r-chunk-end', - insert: 'CodeMirror-merge-r-inserted', - del: 'CodeMirror-merge-r-deleted', - connect: 'CodeMirror-merge-r-connect', - gutter: 'CodeMirror-merge-r-gutter' -}; - -const mergeClassPrefix: DiffClasses = { - chunk: 'CodeMirror-merge-m-chunk', - start: 'CodeMirror-merge-m-chunk-start', - end: 'CodeMirror-merge-m-chunk-end', - insert: 'CodeMirror-merge-m-inserted', - del: 'CodeMirror-merge-m-deleted', - connect: 'CodeMirror-merge-m-connect', - gutter: 'CodeMirror-merge-m-gutter' -}; - -/** - * A wrapper view for showing StringDiffModels in a MergeView - */ -export function createNbdimeMergeView(remote: IStringDiffModel): MergeView; -export function createNbdimeMergeView( - remote: IStringDiffModel | null, - local: IStringDiffModel | null, - merged: IStringDiffModel, - readOnly?: boolean -): MergeView; -export function createNbdimeMergeView( - remote: IStringDiffModel | null, - local?: IStringDiffModel | null, - merged?: IStringDiffModel, - readOnly?: boolean -): MergeView { - let opts: IMergeViewEditorConfiguration = { - remote, - local, - merged, - readOnly, - orig: null - }; - opts.collapseIdentical = true; - let mergeview = new MergeView(opts); - let editors: DiffView[] = []; - if (mergeview.left) { - editors.push(mergeview.left); - } - if (mergeview.right) { - editors.push(mergeview.right); - } - if (mergeview.merge) { - editors.push(mergeview.merge); - } - - let mimetype = (remote || merged!).mimetype; - if (mimetype) { - // Set the editor mode to the MIME type. - for (let e of editors) { - e.ownWidget.model.mimeType = mimetype; - } - mergeview.base.model.mimeType = mimetype; - } - return mergeview; -} - -/** - * Used by MergeView to show diff in a string diff model - */ -export class DiffView { - constructor( - model: IStringDiffModel, - type: 'left' | 'right' | 'merge', - updateCallback: (force?: boolean) => void, - options: CodeMirror.MergeView.MergeViewEditorConfiguration - ) { - this.model = model; - this.type = type; - this.updateCallback = updateCallback; - this.classes = - type === 'left' ? leftClasses : type === 'right' ? rightClasses : null; - /*let ownValue = this.model.remote || '';*/ - /*this.ownWidget = new EditorWidget(ownValue, copyObj({readOnly: !!options.readOnly}, options)); - */ - this.ownWidget = new EditorWidget(); - this.showDifferences = options.showDifferences !== false; - } - - init(base: CodeMirror.Editor) { - /*init(base: EditorView) {*/ - this.baseEditor = base; - ( - this.baseEditor.state.diffViews || (this.baseEditor.state.diffViews = []) - ).push(this); - this.ownEditor.state.diffViews = [this]; - - this.baseEditor.on('gutterClick', this.onGutterClick.bind(this)); - this.ownEditor.on('gutterClick', this.onGutterClick.bind(this)); - - this.lineChunks = this.model.getLineChunks(); - this.chunks = lineToNormalChunks(this.lineChunks); - this.dealigned = false; - - this.forceUpdate = this.registerUpdate(); - this.setScrollLock(true, false); - this.registerScroll(); - } - - setShowDifferences(val: boolean) { - val = val !== false; - if (val !== this.showDifferences) { - this.showDifferences = val; - this.forceUpdate('full'); - } - } - - syncModel() { - if (this.modelInvalid()) { - let edit = this.ownEditor; - let updatedLineChunks = this.model.getLineChunks(); - let updatedChunks = lineToNormalChunks(updatedLineChunks); - if (this.model.remote === edit.getValue()) { - // Nothing to do except update chunks - this.lineChunks = updatedLineChunks; - this.chunks = updatedChunks; - return; - } - let cursor = edit.state.doc.getCursor(); - /*let newLines = splitLines(this.model.remote!);*/ - let start = 1; - let last = edit.state.doc.lines + 1; - /*let cumulativeOffset = 0;*/ - let end: number; - let updatedEnd: number; - // We want to replace contents of editor, but if we have collapsed regions - // some lines have been optimized away. Carefully replace the relevant bits: - for (let range of this.collapsedRanges) { - let baseLine = range.line; - end = getMatchingEditLine(baseLine, this.chunks); - updatedEnd = getMatchingEditLine(baseLine, updatedChunks); - let offset = updatedEnd - end; - if (end !== start || offset !== 0) { - edit.state.doc.replace( - /*newLines.slice(start + cumulativeOffset, updatedEnd + cumulativeOffset - 1).join(''), - CodeMirror.Pos(start, 0), - CodeMirror.Pos(end - 1, 0), - 'syncModel'*/ - 1, - 10, - edit.state.doc /*REAL VALUES TO BE REPLACED*/ - ); - } - /*cumulativeOffset += offset; - start = end + range.size;*/ - } - if (start < last) { - // Only here if no collapsed ranges, replace full contents - edit.state.doc.replace( - /*newLines.slice(start, newLines.length).join(''), - CodeMirror.Pos(start, 0), - CodeMirror.Pos(last, 0), - 'syncModel'*/ - 1, - 10, - edit.state.doc /*REAL VALUES TO BE REPLACED*/ - ); - } - this.ownEditor.state.doc.setCursor(cursor); - this.lineChunks = updatedLineChunks; - this.chunks = updatedChunks; - } - } - - buildGap(): HTMLElement { - let lock = (this.lockButton = elt( - 'div', - undefined, - 'CodeMirror-merge-scrolllock' - )); - lock.title = 'Toggle locked scrolling'; - let lockWrap = elt('div', [lock], 'CodeMirror-merge-scrolllock-wrap'); - let self: DiffView = this; - CodeMirror.on(lock, 'click', function () { - self.setScrollLock(!self.lockScroll); - }); - return (this.gap = elt('div', [lockWrap], 'CodeMirror-merge-gap')); - } - - setScrollLock(val: boolean, action?: boolean) { - this.lockScroll = val; - if (val && action !== false) { - this.syncScroll(EventDirection.OUTGOING); - } - if (this.lockButton) { - this.lockButton.innerHTML = val - ? '\u21db\u21da' - : '\u21db  \u21da'; - } - } - - protected registerUpdate() { - let editMarkers: Marker[] = []; - let origMarkers: Marker[] = []; - let debounceChange: number; - let self: DiffView = this; - self.updating = false; - self.updatingFast = false; - function update(mode?: 'full') { - self.updating = true; - self.updatingFast = false; - if (mode === 'full') { - self.syncModel(); - if (self.classes === null) { - clearMergeMarks(self.baseEditor, editMarkers); - clearMergeMarks(self.ownEditor, origMarkers); - } else { - clearMarks(self.baseEditor, editMarkers, self.classes); - clearMarks(self.ownEditor, origMarkers, self.classes); - } - } - if (self.showDifferences) { - self.updateMarks( - self.ownEditor, - self.model.additions, - editMarkers, - DIFF_OP.DIFF_INSERT - ); - self.updateMarks( - self.baseEditor, - self.model.deletions, - origMarkers, - DIFF_OP.DIFF_DELETE - ); - } - - self.updateCallback(true); - checkSync(self.ownEditor); - self.updating = false; - } - /*function setDealign(fast: boolean | CodeMirror.Editor) {*/ - function setDealign(fast: boolean | CodeMirror.Editor) { - let upd = false; - for (let dv of self.baseEditor.state.diffViews) { - upd = upd || dv.updating; - } - if (upd) { - return; - } - self.dealigned = true; - set(fast === true); - } - function set(fast: boolean) { - let upd = false; - for (let dv of self.baseEditor.state.diffViews) { - upd = upd || dv.updating || dv.updatingFast; - } - if (upd) { - return; - } - clearTimeout(debounceChange); - if (fast === true) { - self.updatingFast = true; - } - debounceChange = window.setTimeout(update, fast === true ? 20 : 250); - } - function change(_cm: EditorView, change: CodeMirror.EditorChange) { - if (!(self.model instanceof DecisionStringDiffModel)) { - // TODO: Throttle? - self.lineChunks = self.model.getLineChunks(); - self.chunks = lineToNormalChunks(self.lineChunks); - } - // Update faster when a line was added/removed - setDealign(change.text.length - 1 !== change.to.line - change.from.line); - } - function checkSync(cm: EditorView) { - if (self.model.remote !== cm.getValue()) { - throw new NotifyUserError( - 'CRITICAL: Merge editor out of sync with model! ' + - 'Double-check any saved merge output!' - ); - } - } - this.baseEditor.on('change', change); - this.ownEditor.on('change', change); - this.baseEditor.on('markerAdded', setDealign); - this.baseEditor.on('markerCleared', setDealign); - this.ownEditor.on('markerAdded', setDealign); - this.ownEditor.on('markerCleared', setDealign); - this.baseEditor.on('viewportChange', function () { - set(false); - }); - this.ownEditor.on('viewportChange', function () { - set(false); - }); - update(); - return update; - } - - protected modelInvalid(): boolean { - return this.model instanceof DecisionStringDiffModel && this.model.invalid; - } - - protected onGutterClick( - instance: CodeMirror.Editor, - line: number, - gutter: string, - clickEvent: Event - ): void { - if ((clickEvent as MouseEvent).button !== 0) { - // Only care about left clicks - return; - } - let li = instance.lineInfo(line); - if (!li.gutterMarkers || !li.gutterMarkers.hasOwnProperty(gutter)) { - return; - } - let node = li.gutterMarkers[gutter]; - if (node && node.sources) { - let ss = node.sources as ChunkSource[]; - if (gutter === GUTTER_PICKER_CLASS) { - if (instance === this.ownEditor) { - for (let s of ss) { - s.decision.action = s.action; - } - } else if (this.type === 'merge' && instance === this.baseEditor) { - for (let s of ss) { - s.decision.action = 'base'; - } - } - for (let i = ss.length - 1; i >= 0; --i) { - let s = ss[i]; - if (this.type === 'merge' && hasEntries(s.decision.customDiff)) { - // Custom diffs are cleared on pick, - // as there is no way to re-pick them - s.decision.customDiff = []; - } - } - if (ss.length === 0) { - // All decisions empty, remove picker - // In these cases, there should only be one picker, on base - // so simply remove the one we have here - instance.setGutterMarker(line, GUTTER_PICKER_CLASS, null); - } - } else if (gutter === GUTTER_CONFLICT_CLASS) { - for (let s of ss) { - s.decision.conflict = false; - } - } - for (let dv of this.baseEditor.state.diffViews as DiffView[]) { - if (dv.model instanceof DecisionStringDiffModel) { - dv.model.invalidate(); - } - dv.forceUpdate('full'); - } - } - } - - protected registerScroll(): void { - let self = this; - this.baseEditor.on('scroll', function () { - self.syncScroll(EventDirection.OUTGOING); - }); - this.ownEditor.on('scroll', function () { - self.syncScroll(EventDirection.INCOMING); - }); - } - - /** - * Sync scrolling between base and own editors. `type` is used to indicate - * which editor is the source, and which editor is the destination of the sync. - */ - protected syncScroll(type: EventDirection): void { - if (this.modelInvalid()) { - return; - } - if (!this.lockScroll) { - return; - } - // editor: What triggered event, other: What needs to be synced - let editor: CodeMirror.Editor; - let other: CodeMirror.Editor; - if (type === EventDirection.OUTGOING) { - editor = this.baseEditor; - other = this.ownEditor; - } else { - editor = this.ownEditor; - other = this.baseEditor; - } - - if (editor.state.scrollSetBy === this) { - editor.state.scrollSetBy = null; - return; - } - - // Position to update to - other.state.scrollPosition = editor.getScrollInfo(); - - // If ticking, we already have a scroll queued - if (other.state.scrollTicking) { - return; - } - - let sInfo = other.getScrollInfo(); - // Don't queue an event if already synced. - if ( - other.state.scrollPosition.top === sInfo.top && - other.state.scrollPosition.left === sInfo.left - ) { - return; - } - // Throttle by requestAnimationFrame(). - // If event is outgoing, this will lead to a one frame delay of other DiffViews - let self = this; - window.requestAnimationFrame(function () { - other.scrollTo( - other.state.scrollPosition.left, - other.state.scrollPosition.top - ); - other.state.scrollTicking = false; - other.state.scrollSetBy = self; - }); - other.state.scrollTicking = true; - return; - } - - protected updateMarks( - editor: CodeMirror.Editor, - diff: DiffRangePos[], - markers: Marker[], - type: DIFF_OP - ) { - let classes: DiffClasses; - if (this.classes === null) { - // Only store prefixes here, will be completed later - classes = copyObj(mergeClassPrefix); - } else { - classes = this.classes; - } - - let self = this; - function markChunk( - editor: CodeMirror.Editor, - from: number, - to: number, - sources: ChunkSource[] - ) { - if (self.classes === null && sources.length > 0) { - // Complete merge class prefixes here - classes = copyObj(mergeClassPrefix); - // First, figure out 'action' state of chunk - let s: string = sources[0].action; - if (sources.length > 1) { - for (let si of sources.slice(1)) { - if (si.action !== s) { - s = 'mixed'; - break; - } - } - } - for (let k of Object.keys(classes)) { - classes[k] += '-' + s; - } - } - // Next, figure out conflict state - let conflict = false; - if (sources.length > 0) { - for (let s of sources) { - if (s.decision.conflict) { - conflict = true; - break; - } - } - } - - for (let i = from; i < to; ++i) { - let line = editor.addLineClass(i, 'background', classes.chunk); - if (conflict) { - editor.addLineClass(line, 'background', CHUNK_CONFLICT_CLASS); - } - if (i === from) { - editor.addLineClass(line, 'background', classes.start); - if (self.type !== 'merge') { - // For all editors except merge editor, add a picker button - let picker = elt('div', PICKER_SYMBOL, classes.gutter); - (picker as any).sources = sources; - picker.classList.add(GUTTER_PICKER_CLASS); - editor.setGutterMarker(line, GUTTER_PICKER_CLASS, picker); - } else if (editor === self.baseEditor) { - for (let s of sources) { - if ( - s.decision.action === 'custom' && - !hasEntries(s.decision.localDiff) && - !hasEntries(s.decision.remoteDiff) - ) { - // We have a custom decision, add picker on base only! - let picker = elt('div', PICKER_SYMBOL, classes.gutter); - (picker as any).sources = sources; - picker.classList.add(GUTTER_PICKER_CLASS); - editor.setGutterMarker(line, GUTTER_PICKER_CLASS, picker); - } - } - } else if (conflict && editor === self.ownEditor) { - // Add conflict markers on editor, if conflicted - let conflictMarker = elt('div', CONFLICT_MARKER, ''); - (conflictMarker as any).sources = sources; - conflictMarker.classList.add(GUTTER_CONFLICT_CLASS); - editor.setGutterMarker(line, GUTTER_CONFLICT_CLASS, conflictMarker); - } - } - if (i === to - 1) { - editor.addLineClass(line, 'background', classes.end); - } - markers.push(line); - } - // When the chunk is empty, make sure a horizontal line shows up - if (from === to) { - let line = editor.addLineClass(from, 'background', classes.start); - if (self.type !== 'merge') { - let picker = elt('div', PICKER_SYMBOL, classes.gutter); - (picker as any).sources = sources; - picker.classList.add(GUTTER_PICKER_CLASS); - editor.setGutterMarker(line, GUTTER_PICKER_CLASS, picker); - } else if (conflict) { - // Add conflict markers on editor, if conflicted - let conflictMarker = elt('div', CONFLICT_MARKER, ''); - (conflictMarker as any).sources = sources; - conflictMarker.classList.add(GUTTER_CONFLICT_CLASS); - editor.setGutterMarker(line, GUTTER_CONFLICT_CLASS, conflictMarker); - } - editor.addLineClass(line, 'background', classes.end + '-empty'); - markers.push(line); - } - } - let cls = type === DIFF_OP.DIFF_DELETE ? classes.del : classes.insert; - editor.operation(function () { - let edit = editor === self.baseEditor; - if (self.classes) { - clearMarks(editor, markers, classes); - } else { - clearMergeMarks(editor, markers); - } - highlightChars(editor, diff, markers, cls); - for (let c of self.chunks) { - if (edit) { - markChunk(editor, c.baseFrom, c.baseTo, c.sources); - } else { - markChunk(editor, c.remoteFrom, c.remoteTo, c.sources); - } - } - }); - } - - get ownEditor(): EditorView { - return this.ownWidget.cm; - } - - ownWidget: EditorWidget; - model: IStringDiffModel; - type: string; - showDifferences: boolean; - dealigned: boolean; - forceUpdate: Function; - baseEditor: EditorView; - chunks: Chunk[]; - lineChunks: Chunk[]; - gap: HTMLElement; - lockScroll: boolean; - updating: boolean; - updatingFast: boolean; - collapsedRanges: { line: number; size: number }[] = []; - - protected updateCallback: (force?: boolean) => void; - protected copyButtons: HTMLElement; - protected lockButton: HTMLElement; - protected classes: DiffClasses | null; -} - -// Updating the marks for editor content - -function clearMergeMarks(editor: CodeMirror.Editor, arr: Marker[]) { - for (let postfix of ['-local', '-remote', '-either', '-custom']) { - let classes = copyObj(mergeClassPrefix); - for (let k of Object.keys(classes)) { - classes[k] += postfix; - } - clearMarks(editor, arr, classes); - } -} - -function isTextMarker(marker: Marker): marker is CodeMirror.TextMarker { - return 'clear' in marker; -} - -function clearMarks(editor: EditorView, arr: Marker[], classes: DiffClasses) { - for (let i = arr.length - 1; i >= 0; --i) { - let mark = arr[i]; - if (isTextMarker(mark)) { - mark.clear(); - arr.splice(i, 1); - } else if ((mark as any).parent) { - editor.removeLineClass(mark, 'background', classes.chunk); - editor.removeLineClass(mark, 'background', classes.start); - editor.removeLineClass(mark, 'background', classes.end); - editor.removeLineClass(mark, 'background', CHUNK_CONFLICT_CLASS); - // Merge editor does not set a marker currently, so don't clear for it: - - if (valueIn(classes.gutter, [leftClasses.gutter, rightClasses.gutter])) { - editor.setGutterMarker(mark, GUTTER_PICKER_CLASS, null); - } else { - editor.setGutterMarker(mark, GUTTER_CONFLICT_CLASS, null); - } - let line = editor.lineInfo(mark); - if (!line.bgClass || line.bgClass.length === 0) { - arr.splice(i, 1); - } - } - } -} - -function highlightChars( - editor: EditorView, - ranges: DiffRangePos[], - markers: Marker[], - cls: string -) { - let doc = editor.state.doc; - let origCls: string | null = null; - if (valueIn(cls, [mergeClassPrefix.del, mergeClassPrefix.insert])) { - origCls = cls; - } - for (let r of ranges) { - if (origCls !== null) { - cls = origCls + (r.source ? '-' + r.source.action : ''); - } - markers.push(doc.markText(r.from, r.to, { className: cls })); - } -} - -// Updating the gap between editor and original - -/** - * From a line in base, find the matching line in another editor by chunks. - */ -function getMatchingEditLine(baseLine: number, chunks: Chunk[]): number { - let offset = 0; - // Start values correspond to either the start of the chunk, - // or the start of a preceding unmodified part before the chunk. - // It is the difference between these two that is interesting. - for (let i = 0; i < chunks.length; i++) { - let chunk = chunks[i]; - if (chunk.baseTo > baseLine && chunk.baseFrom <= baseLine) { - return 0; - } - if (chunk.baseFrom > baseLine) { - break; - } - offset = chunk.remoteTo - chunk.baseTo; - } - return baseLine + offset; -} - -/** - * 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) { - let chunk = chunks[i]; - if (chunk.baseFrom === editLine) { - return chunk.remoteTo; - } - if (chunk.baseFrom > editLine) { - break; - } - } - return toMatch.baseTo; -} - -/** - * Find which line numbers align with each other, in the - * set of DiffViews. The returned array is of the format: - * - * [ 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 agains 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 alignLines( - cm: EditorView[], - lines: number[], - aligners: CodeMirror.LineWidget[] -): void { - let maxOffset = 0; - let offset: number[] = []; - for (let i = 0; i < cm.length; i++) { - if (lines[i] !== null) { - let off = cm[i].heightAtLine(lines[i], 'local'); - offset[i] = off; - maxOffset = Math.max(maxOffset, off); - } - } - for (let i = 0; i < cm.length; i++) { - if (lines[i] !== null) { - let diff = maxOffset - offset[i]; - if (diff > 1) { - aligners.push(padAbove(cm[i], lines[i], diff)); - } - } - } -} - -function padAbove( - cm: EditorView, - line: number, - size: number -): CodeMirror.LineWidget { - let above = true; - if (line > cm.state.doc.lines) { - 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 }); -} - -export interface IMergeViewEditorConfiguration - extends CodeMirror.EditorConfiguration { - /** - * When true stretches of unchanged text will be collapsed. When a number is given, this indicates the amount - * of lines to leave visible around such stretches (which defaults to 2). Defaults to false. - */ - collapseIdentical?: boolean | number; - - /** - * Original value, not used - */ - orig: any; - - /** - * Provides remote diff of document to be shown on the right of the base. - * To create a diff view, provide only remote. - */ - remote: IStringDiffModel | null; - - /** - * Provides local diff of the document to be shown on the left of the base. - * To create a diff view, omit local. - */ - local?: IStringDiffModel | null; - - /** - * Provides the partial merge input for a three-way merge. - */ - merged?: IStringDiffModel; - - /** - * When true, the base of a three-way merge is shown. Defaults to true. - */ - showBase?: boolean; - - /** - * When true, changed pieces of text are highlighted. Defaults to true. - */ - showDifferences?: boolean; -} - -// Merge view, containing 1 or 2 diff views. -export class MergeView extends Panel { - constructor(options: IMergeViewEditorConfiguration) { - super(); - this.options = options; - let remote = options.remote; - let local = options.local || null; - let merged = options.merged || null; - - let panes: number = 0; - let left: DiffView | null = (this.left = null); - let right: DiffView | null = (this.right = null); - 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!'); - } - options.value = main.base !== null ? main.base : main.remote; - options.lineNumbers = options.lineNumbers !== false; - // Whether merge view should be readonly - let readOnly = options.readOnly; - // For all others: - options.readOnly = true; - - /* - * Different cases possible: - * - Local and merged supplied: Merge: - * - Always use left, right and merge panes - * - Use base if `showBase` not set to false - * - Only remote supplied: Diff: - * - No change: Use ony base editor - * - Entire content added/deleted: Use only base editor, - * but with different classes - * - Partial changes: Use base + right editor - */ - - let dvOptions = - options as CodeMirror.MergeView.MergeViewEditorConfiguration; - - if (merged) { - options.gutters = [GUTTER_CONFLICT_CLASS, GUTTER_PICKER_CLASS]; - if (options.lineWrapping === undefined) { - // Turn off linewrapping for merge view by default, keep for diff - options.lineWrapping = false; - } - } - - /*CHANGED: this.base = new EditorWidget(options.value, copyObj({readOnly: !!options.readOnly}, options));*/ - this.base = new EditorWidget(); - - this.base.addClass('CodeMirror-merge-pane'); - this.base.addClass('CodeMirror-merge-pane-base'); - - if (merged) { - let showBase = options.showBase !== false; - if (!showBase) { - this.base.node.style.display = 'hidden'; - } - - let leftWidget: Widget; - if (!local || local.remote === null) { - // Local value was deleted - left = this.left = null; - leftWidget = new Widget({ - node: elt('div', 'Value missing', 'jp-mod-missing') - }); - } else { - left = this.left = new DiffView( - local, - 'left', - this.alignViews.bind(this), - copyObj(dvOptions) - ); - this.diffViews.push(left); - leftWidget = left.ownWidget; - } - leftWidget.addClass('CodeMirror-merge-pane'); - leftWidget.addClass('CodeMirror-merge-pane-local'); - this.addWidget(leftWidget); - - if (showBase) { - this.addWidget(this.base); - } - - let rightWidget: Widget; - if (!remote || remote.remote === null) { - // Remote value was deleted - right = this.right = null; - rightWidget = new Widget({ - node: elt('div', 'Value missing', 'jp-mod-missing') - }); - } else { - right = this.right = new DiffView( - remote, - 'right', - this.alignViews.bind(this), - copyObj(dvOptions) - ); - this.diffViews.push(right); - rightWidget = right.ownWidget; - } - rightWidget.addClass('CodeMirror-merge-pane'); - rightWidget.addClass('CodeMirror-merge-pane-remote'); - this.addWidget(rightWidget); - - this.addWidget( - new Widget({ - node: elt( - 'div', - null, - 'CodeMirror-merge-clear', - 'height: 0; clear: both;' - ) - }) - ); - - merge = this.merge = new DiffView( - merged, - 'merge', - this.alignViews.bind(this), - copyObj({ readOnly }, copyObj(dvOptions)) - ); - this.diffViews.push(merge); - let mergeWidget = merge.ownWidget; - mergeWidget.addClass('CodeMirror-merge-pane'); - mergeWidget.addClass('CodeMirror-merge-pane-final'); - this.addWidget(mergeWidget); - - panes = 3 + (showBase ? 1 : 0); - } else if (remote) { - // If in place for type guard - this.addWidget(this.base); - if (remote.unchanged || remote.added || remote.deleted) { - if (remote.unchanged) { - this.base.addClass('CodeMirror-merge-pane-unchanged'); - } else if (remote.added) { - this.base.addClass('CodeMirror-merge-pane-added'); - } else if (remote.deleted) { - this.base.addClass('CodeMirror-merge-pane-deleted'); - } - panes = 1; - } else { - right = this.right = new DiffView( - remote, - 'right', - this.alignViews.bind(this), - dvOptions - ); - this.diffViews.push(right); - let rightWidget = right.ownWidget; - rightWidget.addClass('CodeMirror-merge-pane'); - rightWidget.addClass('CodeMirror-merge-pane-remote'); - this.addWidget(new Widget({ node: right.buildGap() })); - this.addWidget(rightWidget); - panes = 2; - } - this.addWidget( - new Widget({ - node: elt( - 'div', - null, - 'CodeMirror-merge-clear', - 'height: 0; clear: both;' - ) - }) - ); - } - - this.addClass('CodeMirror-merge'); - this.addClass('CodeMirror-merge-' + panes + 'pane'); - - for (let dv of [left, right, merge]) { - if (dv) { - dv.init(this.base.cm); - } - } - - if (options.collapseIdentical && panes > 1) { - this.base.cm.operation(function () { - collapseIdenticalStretches(self, options.collapseIdentical); - }); - } - for (let dv of [left, right, merge]) { - if (dv) { - dv.collapsedRanges = this.collapsedRanges; - } - } - this.initialized = true; - if (this.left || this.right || this.merge) { - this.alignViews(true); - } - } - - alignViews(force?: boolean) { - let dealigned = false; - if (!this.initialized) { - return; - } - for (let dv of this.diffViews) { - dv.syncModel(); - if (dv.dealigned) { - dealigned = true; - dv.dealigned = false; - } - } - - if (!dealigned && !force) { - return; // Nothing to do - } - // Find matching lines - let linesToAlign = findAlignedLines(this.diffViews); - - // Function modifying DOM to perform alignment: - let self: MergeView = this; - let f = function () { - // Clear old aligners - let aligners = self.aligners; - for (let i = 0; i < aligners.length; i++) { - aligners[i].clear(); - } - aligners.length = 0; - - // Editors (order is important, so it matches - // format of linesToAlign) - /*let cm: CodeMirror.Editor[] = [self.base.cm];*/ - let cm: EditorView[] = [self.base.cm]; - let scroll: number[] = []; - for (let dv of self.diffViews) { - cm.push(dv.ownEditor); - } - for (let i = 0; i < cm.length; i++) { - scroll.push(cm[i].getScrollInfo().top); - } - - for (let ln = 0; ln < linesToAlign.length; ln++) { - alignLines(cm, linesToAlign[ln], aligners); - } - - for (let i = 0; i < cm.length; i++) { - cm[i].scrollTo(null, scroll[i]); - } - }; - - // All editors should have an operation (simultaneously), - // so set up nested operation calls. - if (!this.base.cm.curOp) { - f = (function (fn) { - return function () { - self.base.cm.operation(fn); - }; - })(f); - } - for (let dv of this.diffViews) { - if (!dv.ownEditor.curOp) { - f = (function (fn) { - return function () { - dv.ownEditor.operation(fn); - }; - })(f); - } - } - // Perform alignment - f(); - } - - setShowDifferences(val: boolean) { - if (this.right) { - this.right.setShowDifferences(val); - } - if (this.left) { - this.left.setShowDifferences(val); - } - } - - getMergedValue(): string { - if (!this.merge) { - throw new Error('No merged value; missing "merged" view'); - } - return this.merge.ownEditor.getValue(); - } - - left: DiffView | null; - right: DiffView | null; - merge: DiffView | null; - base: EditorWidget; - options: any; - diffViews: DiffView[]; - aligners: CodeMirror.LineWidget[]; - initialized: boolean = false; - collapsedRanges: { size: number; line: number }[] = []; -} - -function collapseSingle( - cm: EditorView, - from: number, - to: number -): { mark: CodeMirror.TextMarker; clear: () => void } { - cm.addLineClass(from, 'wrap', 'CodeMirror-merge-collapsed-line'); - let widget = document.createElement('span'); - widget.className = 'CodeMirror-merge-collapsed-widget'; - widget.title = 'Identical text collapsed. Click to expand.'; - let mark = cm - .getDoc() - .markText(CodeMirror.Pos(from, 0), CodeMirror.Pos(to - 1), { - inclusiveLeft: true, - inclusiveRight: true, - replacedWith: widget, - clearOnEnter: true - }); - function clear() { - mark.clear(); - cm.removeLineClass(from, 'wrap', 'CodeMirror-merge-collapsed-line'); - } - CodeMirror.on(widget, 'click', clear); - return { mark: mark, clear: clear }; -} - -function collapseStretch( - size: number, - editors: { line: number; cm: EditorView }[] -): CodeMirror.TextMarker { - let marks: { mark: CodeMirror.TextMarker; clear: () => void }[] = []; - function clear() { - for (let i = 0; i < marks.length; i++) { - marks[i].clear(); - } - } - for (let i = 0; i < editors.length; i++) { - let editor = editors[i]; - let mark = collapseSingle(editor.cm, editor.line, editor.line + size); - marks.push(mark); - // Undocumented, but merge.js used it, so follow their lead: - (mark.mark as any).on('clear', clear); - } - return marks[0].mark; -} - -function unclearNearChunks( - dv: DiffView, - margin: number, - off: number, - clear: boolean[] -): void { - for (let i = 0; i < dv.chunks.length; i++) { - let chunk = dv.chunks[i]; - for (let l = chunk.baseFrom - margin; l < chunk.baseTo + margin; l++) { - let pos = l + off; - if (pos >= 0 && pos < clear.length) { - clear[pos] = false; - } - } - } -} - -function collapseIdenticalStretches( - mv: MergeView, - margin?: boolean | number -): void { - // FIXME: Use all panes - if (typeof margin !== 'number') { - margin = 2; - } - let clear: boolean[] = []; - let edit = mv.base.cm; - let off = 1; - for (let l = off, e = edit.state.doc.lines; l <= e; l++) { - clear.push(true); - } - if (mv.left) { - unclearNearChunks(mv.left, margin, off, clear); - } - if (mv.right) { - unclearNearChunks(mv.right, margin, off, clear); - } - if (mv.merge) { - unclearNearChunks(mv.merge, margin, off, clear); - } - mv.collapsedRanges = []; - for (let i = 0; i < clear.length; i++) { - if (clear[i]) { - let line = i + off; - let size = 1; - for (; i < clear.length - 1 && clear[i + 1]; i++, size++) { - // Just finding size - } - if (size > margin) { - /*let editors: {line: number, cm: CodeMirror.Editor}[] =*/ - let editors: { line: number; cm: EditorView }[] = [ - { line: line, cm: edit } - ]; - if (mv.left) { - editors.push({ - line: getMatchingEditLine(line, mv.left.chunks), - cm: mv.left.ownEditor - }); - } - if (mv.right) { - editors.push({ - line: getMatchingEditLine(line, mv.right.chunks), - cm: mv.right.ownEditor - }); - } - if (mv.merge) { - editors.push({ - line: getMatchingEditLine(line, mv.merge.chunks), - cm: mv.merge.ownEditor - }); - } - let mark = collapseStretch(size, editors); - mv.collapsedRanges.push({ line, size }); - (mark as any).on('clear', () => { - for (let i = 0; i < mv.collapsedRanges.length; ++i) { - let range = mv.collapsedRanges[i]; - if (range.line === line) { - mv.collapsedRanges.splice(i, 1); - return; - } - } - }); - if (mv.options.onCollapse) { - mv.options.onCollapse(mv, line, size, mark); - } - } - } - } -} - -// General utilities - -function elt( - tag: string, - content?: string | HTMLElement[] | null, - className?: string | null, - style?: string | null -): HTMLElement { - let e = document.createElement(tag); - if (className) { - e.className = className; - } - if (style) { - e.style.cssText = style; - } - if (typeof content === 'string') { - e.appendChild(document.createTextNode(content)); - } else if (content) { - for (let i = 0; i < content.length; ++i) { - e.appendChild(content[i]); - } - } - return e; -} - -function findPrevDiff( - chunks: Chunk[], - start: number, - isOrig: boolean -): number | null { - for (let i = chunks.length - 1; i >= 0; i--) { - let chunk = chunks[i]; - let to = (isOrig ? chunk.remoteTo : chunk.baseTo) - 1; - if (to < start) { - return to; - } - } - return null; -} - -function findNextDiff( - chunks: Chunk[], - start: number, - isOrig: boolean -): number | null { - for (let i = 0; i < chunks.length; i++) { - let chunk = chunks[i]; - let from = isOrig ? chunk.remoteFrom : chunk.baseFrom; - if (from > start) { - return from; - } - } - return null; -} - -enum DiffDirection { - Previous = -1, - Next = 1 -} - -function goNearbyDiff(cm: EditorView, dir: DiffDirection): void | any { - let found: number | null = null; - let views = cm.state.diffViews as DiffView[]; - let line = cm.getDoc().getCursor().line; - - if (views) { - for (let i = 0; i < views.length; i++) { - let dv = views[i]; - let isOrig = cm === dv.ownEditor; - let pos = - dir === DiffDirection.Previous - ? findPrevDiff(dv.chunks, line, isOrig) - : findNextDiff(dv.chunks, line, isOrig); - if ( - pos !== null && - (found === null || - (dir === DiffDirection.Previous ? pos > found : pos < found)) - ) { - found = pos; - } - } - } - if (found !== null) { - cm.dispatch({ selection: { anchor: found } }); - } else { - return CodeMirror.Pass; - } -} - -CodeMirror.commands.goNextDiff = function (cm: EditorView) { - return goNearbyDiff(cm, DiffDirection.Next); -}; -CodeMirror.commands.goPrevDiff = function (cm: EditorView) { - return goNearbyDiff(cm, DiffDirection.Previous); -}; diff --git a/packages/nbdime/src/common/mergeview.ts b/packages/nbdime/src/common/mergeview.ts index 6b31f8c6..6db772b8 100644 --- a/packages/nbdime/src/common/mergeview.ts +++ b/packages/nbdime/src/common/mergeview.ts @@ -7,48 +7,33 @@ 'use strict'; -import { - Widget, Panel, /*SplitPanel*/ -} from '@lumino/widgets'; +import { Widget, Panel /*SplitPanel*/ } from '@lumino/widgets'; -import type { - IStringDiffModel -} from '../diff/model'; +import type { IStringDiffModel } from '../diff/model'; /*import { DecisionStringDiffModel } from '../merge/model';*/ -/*import type { - DiffRangePos -} from '../diff/range';*/ - - -import { - /*ChunkSource, Chunk,*/ lineToNormalChunks -} from '../chunking'; - -import type { - /*ChunkSource,*/ Chunk -} from '../chunking'; +import type { DiffRangePos } from '../diff/range'; +import { ChunkSource, Chunk, lineToNormalChunks } from '../chunking'; -import { - EditorWidget -} from './editor'; +import { EditorWidget } from './editor'; -import { - /*valueIn, hasEntries,*/ copyObj -} from './util'; +import { /*valueIn/*, hasEntries*/ copyObj } from './util'; -import { - /*valueIn, hasEntries,*/ /*splitLines,/* copyObj*/ -} from './util'; /* import { NotifyUserError } from './exceptions'; */ +/*import { + python +} from '@codemirror/lang-python'; +*/ + +import { python } from "@codemirror/lang-python"; import { Extension, @@ -56,7 +41,7 @@ import { StateField, ChangeDesc, /*RangeSet,*/ - Line, + /*Line,*/ Text, Range } from '@codemirror/state'; @@ -68,53 +53,46 @@ import { ViewUpdate, Decoration, DecorationSet, - keymap, + /*keymap,*/ WidgetType } from '@codemirror/view'; -import { - LegacyCodeMirror -} from '../legacy_codemirror/cmconfig'; +import { LegacyCodeMirror } from '../legacy_codemirror/cmconfig'; //const PICKER_SYMBOL = '\u27ad'; //const CONFLICT_MARKER = '\u26A0'; // '\u2757' - /*export type Marker = CodeMirror.LineHandle | CodeMirror.TextMarker;*/ -export -enum DIFF_OP { +export enum DIFF_OP { DIFF_DELETE = -1, DIFF_INSERT = 1, DIFF_EQUAL = 0 } -export -enum EventDirection { +export enum EventDirection { INCOMING, OUTGOING } -export -type DiffClasses = { +export type DiffClasses = { [key: string]: string; - chunk: string, - start: string, - end: string, - insert: string, - del: string, - connect: string, - gutter: string + chunk: string; + start: string; + end: string; + insert: string; + del: string; + connect: string; + gutter: string; }; - /*const GUTTER_PICKER_CLASS = 'jp-Merge-gutter-picker'; -const GUTTER_CONFLICT_CLASS = 'jp-Merge-gutter-conflict'; +const GUTTER_CONFLICT_CLASS = 'jp-Merge-gutter-conflict';*/ -const CHUNK_CONFLICT_CLASS = 'jp-Merge-conflict';*/ +const CHUNK_CONFLICT_CLASS = 'jp-Merge-conflict'; -const leftClasses: DiffClasses = { chunk: 'CodeMirror-merge-l-chunk', +/*const leftClasses: DiffClasses = { chunk: 'CodeMirror-merge-l-chunk', start: 'CodeMirror-merge-l-chunk-start', end: 'CodeMirror-merge-l-chunk-end', insert: 'CodeMirror-merge-l-inserted', @@ -138,70 +116,79 @@ const rightClasses: DiffClasses = { chunk: 'CodeMirror-merge-r-chunk', gutter: 'CodeMirror-merge-m-gutter'}; */ - /************************ start decoration lines and marks*****************************/ -export -type SideHighlightDict = { +export type EditorDecorationsDict = { [key: string]: Decoration; - chunk: Decoration, - start: Decoration, - end: Decoration, - insert: Decoration, - delete: Decoration -}; - -export -type GlobalHighlightDict = { - [key: string]: SideHighlightDict; - left: SideHighlightDict, - right: SideHighlightDict, - merge: SideHighlightDict -}; - -const leftHighlightDict: SideHighlightDict = { - chunk: Decoration.line( {class: "cm-merge-l-chunk"}), - start: Decoration.line({class: "cm-merge-l-start"}), - end: Decoration.line({class: "cm-merge-l-end"}), - insert: Decoration.mark({class: "cm-merge-l-inserted"}), - delete: Decoration.mark({class: "cm-merge-l-deleted"}) -}; - -const rightHighlightDict: SideHighlightDict = { - chunk: Decoration.line( {class: "cm-merge-r-chunk"}), - start: Decoration.line({class: "cm-merge-r-start"}), - end: Decoration.line({class: "cm-merge-r-end"}), - insert: Decoration.mark({class: "cm-merge-r-inserted"}), - delete: Decoration.mark({class: "cm-merge-r-deleted"}) + chunk: Decoration; + start: Decoration; + end: Decoration; + conflict: Decoration; + endEmpty: Decoration; + inserted: Decoration; + deleted: Decoration; }; -const mergeHighlightDict: SideHighlightDict = { - chunk: Decoration.line( {class: "cm-merge-m-chunk"}), - start: Decoration.line({class: "cm-merge-m-start"}), - end: Decoration.line({class: "cm-merge-m-end"}), - insert: Decoration.mark({class: "cm-merge-m-inserted"}), - delete: Decoration.mark({class: "cm-merge-m-deleted"}) +export type MergeViewDecorationDict = { + [key: string]: EditorDecorationsDict; + left: EditorDecorationsDict; + right: EditorDecorationsDict; + localMerge: EditorDecorationsDict; + remoteMerge: EditorDecorationsDict; + customMerge: EditorDecorationsDict; + eitherMerge: EditorDecorationsDict; + mixedMerge: EditorDecorationsDict; }; -const highlightDict: GlobalHighlightDict = { - left: leftHighlightDict, - right: rightHighlightDict, - merge: mergeHighlightDict +const conflictDecoration = Decoration.line({ class: CHUNK_CONFLICT_CLASS }); +namespace Private { + export function buildEditorDecorationDict( + editorType: string, + chunkAction?: string + ) { + const suffix: string = chunkAction ? '-' + chunkAction : ''; + const prefix: string = 'cm-merge' + '-' + editorType; + const dict: EditorDecorationsDict = { + chunk: Decoration.line({ class: prefix + '-chunk' + suffix }), + start: Decoration.line({ + class: prefix + '-chunk' + '-' + 'start' + suffix + }), + end: Decoration.line({ class: prefix + '-chunk' + '-' + 'end' + suffix }), + endEmpty: Decoration.line({ + class: prefix + '-chunk' + '-' + 'end' + suffix + '-empty' + }), + conflict: conflictDecoration, + inserted: Decoration.mark({ class: prefix + '-' + 'inserted'}), + deleted: Decoration.mark({ class: prefix + '-' + 'deleted'}), + }; + return dict; + } } -const diffViewPlugin = ViewPlugin.fromClass(class { - addDiffView(dv: DiffView) : void { - this.diffviews.push(dv); - } - update(update: ViewUpdate) { - if (update.docChanged || update.viewportChanged){ - console.log('Update tracker'); - /*for (let dv of this.diffviews ) { +const mergeViewDecorationDict: MergeViewDecorationDict = { + left: Private.buildEditorDecorationDict('l'), + right: Private.buildEditorDecorationDict('r'), + localMerge: Private.buildEditorDecorationDict('m', 'local'), + remoteMerge: Private.buildEditorDecorationDict('m', 'remote'), + customMerge: Private.buildEditorDecorationDict('m', 'custom'), + eitherMerge: Private.buildEditorDecorationDict('m', 'either'), + mixedMerge: Private.buildEditorDecorationDict('m', 'mixed') +}; +const diffViewPlugin = ViewPlugin.fromClass( + class { + addDiffView(dv: DiffView): void { + this.diffviews.push(dv); + } + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + //console.log('Update tracker'); + /*for (let dv of this.diffviews ) { dv.update(); }*/ + } } + private diffviews: DiffView[] = []; } - private diffviews: DiffView[] = []; -}); +); function getCommonEditorExtensions(): Extension { return [ @@ -209,7 +196,8 @@ function getCommonEditorExtensions(): Extension { lineNumbers(), HighlightField, LineWidgetField, - decorationKeymap + python() + /*decorationKeymap*/ ]; } @@ -217,86 +205,98 @@ function applyMapping({ from, to }: any, mapping: ChangeDesc) { const map: any = { from: mapping.mapPos(from), to: mapping.mapPos(to) }; return map; } -const addHighlight = StateEffect.define<{from: number, to: number, highlightType: string, side: string}> ({ +const addHighlight = StateEffect.define<{ + from: number; + to: number; + highlightType: string; + decorationKey: string; +}>({ + map: applyMapping +}); + +const removeHighlight = StateEffect.define<{ + highlightType: string; + decorationKey: string; +}>({ map: applyMapping -}) +}); const HighlightField = StateField.define({ create() { - return Decoration.none + return Decoration.none; }, update(highlightRanges, transaction) { highlightRanges = highlightRanges.map(transaction.changes); for (let e of transaction.effects) { - let markDict: SideHighlightDict; - if (e.is(addHighlight)) { - if (e.value.side == 'left') { - markDict = highlightDict['left']; - highlightRanges = highlightRanges.update({ - add: [markDict[e.value.highlightType].range(e.value.from, e.value.to)] - }); - } - if (e.value.side == 'right') { - markDict = highlightDict['right']; - highlightRanges = highlightRanges.update({ - add: [markDict[e.value.highlightType].range(e.value.from, e.value.to)] - }); - } - if (e.value.side == 'merge') { - markDict = highlightDict['merge']; - highlightRanges = highlightRanges.update({ - add: [markDict[e.value.highlightType].range(e.value.from, e.value.to)] - }); - } - } - } - return highlightRanges; - }, + let decoration: Decoration; + if (e.is(addHighlight)) { + decoration = mergeViewDecorationDict[e.value.decorationKey][e.value.highlightType]; + 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]; + highlightRanges = highlightRanges.update({ + filter: (from: number, to: number, value: Decoration) => {return (decoration.spec.class !== value.spec.class) }, + }); + } + } + return highlightRanges; +}, provide: field => EditorView.decorations.from(field) }); -function leftHighlightChunkSelection(view: EditorView) { +/*function leftHighlightChunkSelection(view: EditorView) { let highlightType = 'chunk' - let side = 'left'; + let decorationKey = 'left'; let effects: StateEffect[] = view.state.selection.ranges .filter(r => !r.empty) - .map(({ from }) => addHighlight.of({ from, to: from, highlightType, side })); + .map(({ from }) => addHighlight.of({ from, to: from, highlightType, decorationKey })); view.dispatch({ effects }); return true; } function rightHighlightChunkSelection(view: EditorView) { let highlightType = 'chunk' - let side = 'right'; + let decorationKey = 'right'; let effects: StateEffect[] = view.state.selection.ranges .filter(r => !r.empty) - .map(({ from }) => addHighlight.of({ from, to: from, highlightType, side })); + .map(({ from }) => addHighlight.of({ from, to: from, highlightType, decorationKey })); view.dispatch({ effects }); return true; } function leftHighlightInsertSelection(view: EditorView) { - let highlightType = 'insert' - let side = 'left'; + 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, side })); + .map(({ from, to }) => addHighlight.of({ from, to, highlightType, decorationKey })); view.dispatch({ effects }); return true; } function rightHighlightInsertSelection(view: EditorView) { - let highlightType = 'insert' - let side = 'right'; + 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, side })); + .map(({ from, to }) => addHighlight.of({ from, to, highlightType, decorationKey })); view.dispatch({ effects }); return true; -} +}*/ /***********************start decoration widget and related statefield***********************************/ -const addLineWidgetEffect = StateEffect.define< {line: number, size: number} >({ - map: ({line, size}, mapping) => ({line: mapping.mapPos(line), size: mapping.mapPos(size)}) +const addLineWidgetEffect = StateEffect.define<{ line: number; size: number }>({ + map: ({ line, size }, mapping) => ({ + line: mapping.mapPos(line), + size: mapping.mapPos(size) + }) }); const LineWidgetField = StateField.define({ @@ -304,23 +304,22 @@ const LineWidgetField = StateField.define({ return Decoration.none; }, update: (LineWidgetRanges, transaction) => { - console.log('StateField updated') LineWidgetRanges = LineWidgetRanges.map(transaction.changes); for (let e of transaction.effects) { if (e.is(addLineWidgetEffect)) LineWidgetRanges = LineWidgetRanges.update({ - add: addLineWidget(transaction.state.doc,e.value.line, e.value.size) - }); + add: addLineWidget(transaction.state.doc, e.value.line, e.value.size) + }); } return LineWidgetRanges; -}, + }, provide: field => EditorView.decorations.from(field) }); class LineWidget extends WidgetType { constructor(block: boolean, line: number, size: number) { - super() + super(); this.block = block; - this.lineNumber = line; + this.lineNumber = line; this.size = size; } toDOM() { @@ -329,34 +328,40 @@ class LineWidget extends WidgetType { elt.style.height = this.size + 'px'; elt.style.minWidth = '1px'; - return elt + return elt; } - block : boolean; + block: boolean; lineNumber: number; size: number; } function posToOffset(doc: any, pos: any) { - return doc.line(pos.line + 1).from + pos.ch + return doc.line(pos.line + 1).from + pos.ch; } -// type DecorationSet = RangeSet -/* class RangeSet { -constructor(Range[]) -//}*/ -function addLineWidget(doc: Text, line: number, size: number): Range[] { - let widgets: Range[] = [] + +/* function offsetToPos(doc, offset) { + let line = doc.lineAt(offset) + return {line: line.number - 1, ch: offset - line.from} +} */ + +function addLineWidget( + doc: Text, + line: number, + size: number +): Range[] { + let widgets: Range[] = []; let block: boolean = true; let deco = Decoration.widget({ widget: new LineWidget(block, line, size) - }) + }); const ch = 0; - let pos: any = {line: line, ch: ch} - let offset = posToOffset(doc, pos) - widgets.push(deco.range(offset)) + let pos: any = { line: line, ch: ch }; + let offset = posToOffset(doc, pos); + widgets.push(deco.range(offset)); return widgets; } -function addLineWidgetFromUI(view: EditorView) { +/*function addLineWidgetFromUI(view: EditorView) { const cursor = view.state.selection.main.head; const line: Line = view.state.doc.lineAt(cursor); let effects: StateEffect[] = []; @@ -364,9 +369,9 @@ function addLineWidgetFromUI(view: EditorView) { view.dispatch({effects}); return true; -} +}*/ /**************************end decoration widget and related statefield************************************************* */ -const decorationKeymap = keymap.of([ +/*const decorationKeymap = keymap.of([ { key: 'Alt-u', preventDefault: true, @@ -392,7 +397,7 @@ const decorationKeymap = keymap.of([ preventDefault: true, run: addLineWidgetFromUI }], -) +)*/ /** * @@ -400,22 +405,24 @@ const decorationKeymap = keymap.of([ */ export function createNbdimeMergeView(remote: IStringDiffModel): MergeView; export function createNbdimeMergeView( - remote: IStringDiffModel | null, - local: IStringDiffModel | null, - merged: IStringDiffModel, - readOnly?: boolean): MergeView; -export -function createNbdimeMergeView( - remote: IStringDiffModel | null, - local?: IStringDiffModel | null, - merged?: IStringDiffModel, - readOnly?: boolean): MergeView { + remote: IStringDiffModel | null, + local: IStringDiffModel | null, + merged: IStringDiffModel, + readOnly?: boolean +): MergeView; +export function createNbdimeMergeView( + remote: IStringDiffModel | null, + local?: IStringDiffModel | null, + merged?: IStringDiffModel, + readOnly?: boolean +): MergeView { let opts: IMergeViewEditorConfiguration = { remote, local, merged, readOnly, - orig: null}; + orig: null + }; opts.collapseIdentical = true; let mergeview = new MergeView(opts); let editors: DiffView[] = []; @@ -433,8 +440,8 @@ function createNbdimeMergeView( if (mimetype) { // Set the editor mode to the MIME type. for (let e of editors) { - e.ownWidget.model.mimeType = mimetype; - } + e.remoteEditorWidget.model.mimeType = mimetype; + } mergeview.base.model.mimeType = mimetype; } return mergeview; @@ -443,43 +450,98 @@ function createNbdimeMergeView( /** * Used by MergeView to show diff in a string diff model */ -export -class DiffView { - constructor(model: IStringDiffModel, - type: 'left' | 'right' | 'merge', - updateCallback: (force?: boolean) => void, - options: IMergeViewEditorConfiguration) { +export class DiffView { + constructor( + model: IStringDiffModel, + type: 'left' | 'right' | 'merge', + updateCallback: (force?: boolean) => void, + options: IMergeViewEditorConfiguration + ) { this.model = model; this.type = type; this.updateCallback = updateCallback; - this.classes = type === 'left' ? - leftClasses : type === 'right' ? rightClasses : null; - let ownValue = this.model.remote || ''; - //this.ownWidget = new EditorWidget(ownValue, copyObj({readOnly: !!options.readOnly}, options)); - this.ownWidget = new EditorWidget(ownValue); // OPTIONS TO BE GIVEN - this.ownWidget.editor.injectExtension(getCommonEditorExtensions()); + //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.showDifferences = options.showDifferences !== false; + } + /* +init(base: CodeMirror.Editor) { + this.baseEditor = base; + (this.baseEditor.state.diffViews || + (this.baseEditor.state.diffViews = [])).push(this); + this.remoteEditor.state.diffViews = [this]; + this.baseEditor.on('gutterClick', this.onGutterClick.bind(this)); + this.remoteEditor.on('gutterClick', this.onGutterClick.bind(this)); + + this.lineChunks = this.model.getLineChunks(); + this.chunks = lineToNormalChunks(this.lineChunks); + this.dealigned = false; + + this.forceUpdate = this.registerUpdate(); + this.setScrollLock(true, false); + this.registerScroll(); } +*/ - init(base: EditorWidget) { - this.baseEditor = base; - const baseEditorPlugin = this.baseEditor.cm.plugin(diffViewPlugin); - const remoteEditorPlugin = this.ownWidget.cm.plugin(diffViewPlugin); + init(baseWidget: EditorWidget) { + this.baseEditorWidget = baseWidget; + const baseEditor = this.baseEditorWidget.cm; + const remoteEditor = this.remoteEditorWidget.cm; + const baseEditorPlugin = baseEditor.plugin(diffViewPlugin); + const remoteEditorPlugin = remoteEditor.plugin(diffViewPlugin); baseEditorPlugin?.addDiffView(this); remoteEditorPlugin?.addDiffView(this); + //this.baseEditor.on('gutterClick', this.onGutterClick.bind(this)); - //this.ownEditor.on('gutterClick', this.onGutterClick.bind(this)); + //this.remoteEditor.on('gutterClick', this.onGutterClick.bind(this)); this.lineChunks = this.model.getLineChunks(); this.chunks = lineToNormalChunks(this.lineChunks); - this.dealigned = false; + this.clearHighlighting( + remoteEditor, + this.model.additions, + this.chunks, + DIFF_OP.DIFF_INSERT + ); + + this.clearHighlighting( + baseEditor, + this.model.deletions, + this.chunks, + DIFF_OP.DIFF_DELETE + ); + + this.updateHighlighting( + remoteEditor, + this.model.additions, + this.chunks, + DIFF_OP.DIFF_INSERT + ); + this.updateHighlighting( + baseEditor, + this.model.deletions, + this.chunks, + DIFF_OP.DIFF_DELETE + ); + + + + /*this.dealigned = false; this.forceUpdate = this.registerUpdate(); + this.setScrollLock(true, false); this.registerScroll(); + */ } + update() {} + setShowDifferences(val: boolean) { /* val = val !== false; @@ -492,7 +554,7 @@ class DiffView { syncModel() { /*if (this.modelInvalid()) { - let edit = this.ownEditor; + let edit = this.remoteEditor; let updatedLineChunks = this.model.getLineChunks(); let updatedChunks = lineToNormalChunks(updatedLineChunks); //if (this.model.remote === edit.getValue()) {// @@ -542,16 +604,15 @@ class DiffView { 'syncModel' ); } - this.ownEditor.getDoc().setCursor(cursor); + this.remoteEditor.getDoc().setCursor(cursor); this.lineChunks = updatedLineChunks; this.chunks = updatedChunks; } */ } - - buildGap(): HTMLElement { - let lock = this.lockButton = elt('div', undefined, 'cm-merge-scrolllock'); + buildGap(): HTMLElement { + let lock = (this.lockButton = elt('div', undefined, 'cm-merge-scrolllock')); lock.title = 'Toggle locked scrolling'; let lockWrap = elt('div', [lock], 'cm-merge-scrolllock-wrap'); // TODO: plug listener @@ -560,7 +621,7 @@ class DiffView { CodeMirror.on(lock, 'click', function() { self.setScrollLock(!self.lockScroll); });*/ - return this.gap = elt('div', [lockWrap], 'cm-merge-gap'); + return (this.gap = elt('div', [lockWrap], 'cm-merge-gap')); //return (document.createElement("div")); } @@ -588,29 +649,29 @@ class DiffView { if (mode === 'full') { self.syncModel(); if (self.classes === null) { - clearMergeMarks(self.baseEditor, editMarkers); - clearMergeMarks(self.ownEditor, origMarkers); + clearMergeMarks(self.baseEditorWidget, editMarkers); + clearMergeMarks(self.remoteEditor, origMarkers); } else { - clearMarks(self.baseEditor, editMarkers, self.classes); - clearMarks(self.ownEditor, origMarkers, self.classes); + clearMarks(self.baseEditorWidget, editMarkers, self.classes); + clearMarks(self.remoteEditor, origMarkers, self.classes); } } if (self.showDifferences) { self.updateMarks( - self.ownEditor, self.model.additions, + self.remoteEditor, self.model.additions, editMarkers, DIFF_OP.DIFF_INSERT); self.updateMarks( - self.baseEditor, self.model.deletions, + self.baseEditorWidget, self.model.deletions, origMarkers, DIFF_OP.DIFF_DELETE); } self.updateCallback(true); - checkSync(self.ownEditor); + checkSync(self.remoteEditor); self.updating = false; } function setDealign(fast: boolean | CodeMirror.Editor, diffViews: diffView[]) { let upd = false; - for (let dv of self.baseEditor.state.diffViews) { + for (let dv of self.baseEditorWidget.state.diffViews) { upd = upd || dv.updating; } if (upd) { @@ -621,7 +682,7 @@ class DiffView { } function set(fast: boolean) { let upd = false; - for (let dv of self.baseEditor.state.diffViews) { + for (let dv of self.baseEditorWidget.state.diffViews) { upd = upd || dv.updating || dv.updatingFast; } if (upd) { @@ -650,20 +711,19 @@ class DiffView { } } */ - /*this.baseEditor.on('change', change); - this.ownEditor.on('change', change); - this.baseEditor.on('markerAdded', setDealign); - this.baseEditor.on('markerCleared', setDealign); - this.ownEditor.on('markerAdded', setDealign); - this.ownEditor.on('markerCleared', setDealign); - this.baseEditor.on('viewportChange', function() { set(false); }); - this.ownEditor.on('viewportChange', function() { set(false); }); + /*this.baseEditorWidget.on('change', change); + this.remoteEditor.on('change', change); + this.baseEditorWidget.on('markerAdded', setDealign); + this.baseEditorWidget.on('markerCleared', setDealign); + this.remoteEditor.on('markerAdded', setDealign); + this.remoteEditor.on('markerCleared', setDealign); + this.baseEditorWidget.on('viewportChange', function() { set(false); }); + this.remoteEditor.on('viewportChange', function() { set(false); }); update();*/ - return ()=>{}; + return () => {}; } - protected modelInvalid(): boolean { /* return this.model instanceof DecisionStringDiffModel && this.model.invalid; */ @@ -683,11 +743,11 @@ class DiffView { if (node && node.sources) { let ss = node.sources as ChunkSource[]; if (gutter === GUTTER_PICKER_CLASS) { - if (instance === this.ownEditor) { + if (instance === this.remoteEditor) { for (let s of ss) { s.decision.action = s.action; } - } else if (this.type === 'merge' && instance === this.baseEditor) { + } else if (this.type === 'merge' && instance === this.baseEditorWidget) { for (let s of ss) { s.decision.action = 'base'; } @@ -711,7 +771,7 @@ class DiffView { s.decision.conflict = false; } } - for (let dv of this.baseEditor.state.diffViews as DiffView[]) { + for (let dv of this.baseEditorWidget.state.diffViews as DiffView[]) { if (dv.model instanceof DecisionStringDiffModel) { dv.model.invalidate(); } @@ -722,10 +782,10 @@ class DiffView { protected registerScroll(): void { /* let self = this; - this.baseEditor.on('scroll', function() { + this.baseEditorWidget.on('scroll', function() { self.syncScroll(EventDirection.OUTGOING); }); - this.ownEditor.on('scroll', function() { + this.remoteEditor.on('scroll', function() { self.syncScroll(EventDirection.INCOMING); }); */ } @@ -745,11 +805,11 @@ class DiffView { let editor: CodeMirror.Editor; let other: CodeMirror.Editor; if (type === EventDirection.OUTGOING) { - editor = this.baseEditor; - other = this.ownEditor; + editor = this.baseEditorWidget; + other = this.remoteEditor; } else { - editor = this.ownEditor; - other = this.baseEditor; + editor = this.remoteEditor; + other = this.baseEditorWidget; } if (editor.state.scrollSetBy === this) { @@ -783,8 +843,7 @@ class DiffView { return; */ } - - /* protected updateMarks(editor: CodeMirror.Editor, diff: DiffRangePos[], + /*protected updateMarks(editor: EditorView, diff: DiffRangePos[], markers: Marker[], type: DIFF_OP) { let classes: DiffClasses; if (this.classes === null) { @@ -838,7 +897,7 @@ class DiffView { (picker as any).sources = sources; picker.classList.add(GUTTER_PICKER_CLASS); editor.setGutterMarker(line, GUTTER_PICKER_CLASS, picker); - } else if (editor === self.baseEditor) { + } else if (editor === self.baseEditorWidget) { for (let s of sources) { if (s.decision.action === 'custom' && !hasEntries(s.decision.localDiff) && @@ -850,7 +909,7 @@ class DiffView { editor.setGutterMarker(line, GUTTER_PICKER_CLASS, picker); } } - } else if (conflict && editor === self.ownEditor) { + } else if (conflict && editor === self.remoteEditor) { // Add conflict markers on editor, if conflicted let conflictMarker = elt('div', CONFLICT_MARKER, ''); (conflictMarker as any).sources = sources; @@ -884,7 +943,7 @@ class DiffView { } let cls = type === DIFF_OP.DIFF_DELETE ? classes.del : classes.insert; editor.operation(function() { - let edit = editor === self.baseEditor; + let edit = editor === self.baseEditorWidget; if (self.classes) { clearMarks(editor, markers, classes); } else { @@ -901,33 +960,248 @@ class DiffView { }); }*/ - get ownEditor(): EditorWidget { - return this.ownWidget; + private getDecorationKey(sources: ChunkSource[]): string { + let s: string = this.type; + let res: string = s; + if (this.type === 'merge') { + s = sources[0].action; + res = s + 'Merge' + if (sources.length > 1) { + for (let si of sources.slice(1)) { + if (si.action !== s) { + res = 'mixedMerge'; + break; + } + } + } + } + return res; + } + + private getConflictState(sources: ChunkSource[]): boolean { + let conflict = false; + if (sources.length > 0) { + for (let s of sources) { + if (s.decision.conflict) { + conflict = true; + break; + } + } + } + return conflict; + } + + private createAddHighlightEffect( + highlightType: string, + decorationKey: string, + startingOffset: number, + endingOffset?: number + ) { + const effect = addHighlight.of({ + from: startingOffset, + to: endingOffset ?? startingOffset, + highlightType: highlightType, + decorationKey: decorationKey + }); + return effect; + } + + private createClearHighlightEffect( + highlightType: string, + decorationKey: string + ) { + const effect = removeHighlight.of({ + highlightType: highlightType, + decorationKey: decorationKey + }); + return effect; + } + + private buildLineHighlighting(editor: EditorView, chunkArray: Chunk[]) { + let effects: StateEffect[] = []; + let isbaseEditor = editor === this.baseEditorWidget.cm; + + for (let chunk of chunkArray) { + let sources: ChunkSource[] = chunk.sources; + let decorationKey = this.getDecorationKey(sources); + let conflict = this.getConflictState(sources); + let chunkFirstLine: number; + let chunkLastLine: number; + + if (isbaseEditor) { + chunkFirstLine = chunk.baseFrom; + chunkLastLine = chunk.baseTo; + } else { + chunkFirstLine = chunk.remoteFrom; + chunkLastLine = chunk.remoteTo; + } + for (let i = chunkFirstLine; i < chunkLastLine; i++) { + const pos: any = { line: i, ch: 0 }; + const startingOffset = posToOffset(editor.state.doc, pos); + effects.push( + this.createAddHighlightEffect('chunk', decorationKey, startingOffset) + ); + if (conflict) { + effects.push( + this.createAddHighlightEffect('conflict', decorationKey, startingOffset) + ); + } + if (i === chunkFirstLine) { + effects.push( + this.createAddHighlightEffect('start', decorationKey, startingOffset) + ); + } + if (i === chunkLastLine - 1) { + effects.push(this.createAddHighlightEffect('end', decorationKey, startingOffset)); + } + } + if (chunkFirstLine === chunkLastLine) { + const startingOffset = posToOffset(editor.state.doc, { line: chunkFirstLine, ch: 0 }); + effects.push( + this.createAddHighlightEffect('endEmpty', decorationKey, startingOffset) + ); + } + } + return effects; + } + + + + private clearLineHighlighting(editor: EditorView, chunkArray: Chunk[]) { + let effects: StateEffect[] = []; + + for (let chunk of chunkArray) { + let sources: ChunkSource[] = chunk.sources; + let decorationKey = this.getDecorationKey(sources); + effects.push(this.createClearHighlightEffect('chunk', decorationKey)); + effects.push(this.createClearHighlightEffect('conflict', decorationKey)); + effects.push(this.createClearHighlightEffect('start', decorationKey)); + effects.push(this.createClearHighlightEffect('end', decorationKey,)); + } + + return effects; + } + + + private buildMarkHighlighting( + editor: EditorView, + diffRanges: DiffRangePos[], + markType: DIFF_OP + ) { + let effects: StateEffect[] = []; + let sources: ChunkSource[] = []; + if (markType === DIFF_OP.DIFF_INSERT || markType === DIFF_OP.DIFF_DELETE) { + let highlightType: string = markType === DIFF_OP.DIFF_DELETE ? 'deleted' : 'inserted'; + for (let r of diffRanges) { + if (r.source !== undefined) { + sources.push(r.source); + } + let decorationKey = this.getDecorationKey(sources); + let startingOffset = posToOffset(editor.state.doc, { + line: r.from.line, + ch: r.from.column + }); + let endingOffset = posToOffset(editor.state.doc, { + line: r.to.line, + ch: r.to.column + }); + effects.push( + this.createAddHighlightEffect( + highlightType, + decorationKey, + startingOffset, + endingOffset + ) + ); + } + } + return effects; + } + + private clearMarkHighlighting( + editor: EditorView, + diffRanges: DiffRangePos[], + markType: DIFF_OP + ) { + let effects: StateEffect[] = []; + let sources: ChunkSource[] = []; + if (markType === DIFF_OP.DIFF_INSERT || markType === DIFF_OP.DIFF_DELETE) { + let highlightType: string = markType === DIFF_OP.DIFF_DELETE ? 'deleted' : 'inserted'; + for (let r of diffRanges) { + if (r.source !== undefined) { + sources.push(r.source); + } + let decorationKey = this.getDecorationKey(sources); + effects.push( + this.createClearHighlightEffect( + highlightType, + decorationKey, + + ) + ); + } + } + return effects; + } + + protected updateHighlighting( + editor: EditorView, + diffRanges: DiffRangePos[], + chunkArray: Chunk[], + type: DIFF_OP + ) { + let self = this; + let LineHighlightEffects: StateEffect[] = + self.buildLineHighlighting(editor, chunkArray); + let MarkHighlightEffects: StateEffect[] = + self.buildMarkHighlighting(editor, diffRanges, type); + let effects: StateEffect[] = + LineHighlightEffects.concat(MarkHighlightEffects); + editor.dispatch({ effects }); + } + + protected clearHighlighting( + editor: EditorView, + diffRanges: DiffRangePos[], + chunkArray: Chunk[], + type: DIFF_OP + ) { + let self = this; + let clearLineHighlightEffects: StateEffect[] = + self.clearLineHighlighting(editor, chunkArray); + let clearMarkHighlightEffects: StateEffect[] = + self.clearMarkHighlighting(editor, diffRanges, type); + let effects: StateEffect[] = + clearLineHighlightEffects.concat(clearMarkHighlightEffects); + editor.dispatch({ effects }); } - ownWidget: EditorWidget; + + get remoteEditorWidget(): EditorWidget { + return this._remoteEditorWidget; + } + + baseEditorWidget: EditorWidget; + private _remoteEditorWidget: EditorWidget; model: IStringDiffModel; type: string; showDifferences: boolean; dealigned: boolean; forceUpdate: Function; - baseEditor: EditorWidget; chunks: Chunk[]; lineChunks: Chunk[]; gap: HTMLElement; lockScroll: boolean; updating: boolean; updatingFast: boolean; - collapsedRanges: {line: number, size: number}[] = []; - + collapsedRanges: { line: number; size: number }[] = []; protected updateCallback: (force?: boolean) => void; protected copyButtons: HTMLElement; protected lockButton: HTMLElement; - protected classes: DiffClasses | null; + //protected classes: DiffClasses | null; } - // Updating the marks for editor content /*function clearMergeMarks(editor: CodeMirror.Editor, arr: Marker[]) { @@ -940,6 +1214,7 @@ class DiffView { } }*/ + /*function isTextMarker(marker: Marker): marker is CodeMirror.TextMarker { return 'clear' in marker; }*/ @@ -969,6 +1244,8 @@ class DiffView { } }*/ + + /*function highlightChars(editor: CodeMirror.Editor, ranges: DiffRangePos[], markers: Marker[], cls: string) { let doc = editor.getDoc(); @@ -984,10 +1261,8 @@ class DiffView { } }*/ - // Updating the gap between editor and original - /** * From a line in base, find the matching line in another editor by chunks. */ @@ -1009,7 +1284,6 @@ class DiffView { return baseLine + offset; }*/ - /** * From a line in base, find the matching line in another editor by line chunks */ @@ -1027,6 +1301,20 @@ class DiffView { return toMatch.baseTo; }*/ +/* CM6 */ +/*function getMatchingEditLineLC(toMatch: Chunk, chunks: Chunk[]): number { + let editLine = toMatch.baseFrom; + for (let i = 0; i < chunks.length; ++i) { + let chunk = chunks[i]; + if (chunk.baseFrom === editLine) { + return chunk.remoteTo; + } + if (chunk.baseFrom > editLine) { + break; + } + } + return toMatch.baseTo; +}*/ /** * Find which line numbers align with each other, in the @@ -1117,6 +1405,89 @@ class DiffView { return linesToAlign; }*/ +/* CM6 Version*/ +/*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 alignLines(cm: CodeMirror.Editor[], lines: number[], aligners: CodeMirror.LineWidget[]): void { let maxOffset = 0; @@ -1151,8 +1522,8 @@ function padAbove(cm: CodeMirror.Editor, line: number, size: number): CodeMirror } */ -export -interface IMergeViewEditorConfiguration extends LegacyCodeMirror.EditorConfiguration { +export interface IMergeViewEditorConfiguration + extends LegacyCodeMirror.EditorConfiguration { /** * When true stretches of unchanged text will be collapsed. When a number is given, this indicates the amount * of lines to leave visible around such stretches (which defaults to 2). Defaults to false. @@ -1193,8 +1564,7 @@ interface IMergeViewEditorConfiguration extends LegacyCodeMirror.EditorConfigura } // Merge view, containing 1 or 2 diff views. -export -class MergeView extends Panel { +export class MergeView extends Panel { constructor(options: IMergeViewEditorConfiguration) { super(); this.options = options; @@ -1202,25 +1572,23 @@ class MergeView extends Panel { let local = options.local || null; let merged = options.merged || null; //let panes: number = 0; - let left: DiffView | null = this.left = null; - let right: DiffView | null = this.right = null; - let merge: DiffView | null = this.merge = null; + let left: DiffView | null = (this.left = null); + let right: DiffView | null = (this.right = null); + let merge: DiffView | null = (this.merge = null); //let self = this; this.diffViews = []; - //this.aligners = []; + this.aligners = []; let main = options.remote || options.merged; if (!main) { throw new Error('Either remote or merged model needs to be specified!'); } - options.value = (main.base !== null ? - main.base : main.remote); + options.value = main.base !== null ? main.base : main.remote; options.lineNumbers = options.lineNumbers !== false; // Whether merge view should be readonly let readOnly = options.readOnly; // For all others: options.readOnly = true; - /* * Different cases possible: * - Local and merged supplied: Merge: @@ -1233,10 +1601,10 @@ class MergeView extends Panel { * - Partial changes: Use base + right editor */ - let dvOptions = options;// as CodeMirror.MergeView.MergeViewEditorConfiguration; + let dvOptions = options; // as CodeMirror.MergeView.MergeViewEditorConfiguration; this.gridPanel = new Panel(); - this.gridPanel.addClass('cm-grid-panel') + this.gridPanel.addClass('cm-grid-panel'); this.addWidget(this.gridPanel); if (merged) { @@ -1249,8 +1617,7 @@ 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(getCommonEditorExtensions()); if (merged) { let showBase = options.showBase !== false; @@ -1263,18 +1630,23 @@ class MergeView extends Panel { if (!local || local.remote === null) { // Local value was deleted left = this.left = null; - leftWidget = new Widget({node: elt('div', 'Value missing', 'jp-mod-missing')}); + leftWidget = new Widget({ + node: elt('div', 'Value missing', 'jp-mod-missing') + }); } else { - left = this.left = new DiffView(local, 'left', this.alignViews.bind(this), - copyObj(dvOptions)); + left = this.left = new DiffView( + local, + 'left', + this.alignViews.bind(this), + copyObj(dvOptions) + ); this.diffViews.push(left); - leftWidget = left.ownWidget; + leftWidget = left.remoteEditorWidget; } this.gridPanel.addWidget(leftWidget); leftWidget.addClass('.cm-left-editor'); //leftWidget.addClass('cm-mergeViewEditor'); - if (showBase) { this.gridPanel.addWidget(this.base); this.base.addClass('.cm-central-editor'); @@ -1285,12 +1657,18 @@ class MergeView extends Panel { if (!remote || remote.remote === null) { // Remote value was deleted right = this.right = null; - rightWidget = new Widget({node: elt('div', 'Value missing', 'jp-mod-missing')}); + rightWidget = new Widget({ + node: elt('div', 'Value missing', 'jp-mod-missing') + }); } else { - right = this.right = new DiffView(remote, 'right', this.alignViews.bind(this), - copyObj(dvOptions)); + right = this.right = new DiffView( + remote, + 'right', + this.alignViews.bind(this), + copyObj(dvOptions) + ); this.diffViews.push(right); - rightWidget = right.ownWidget; + rightWidget = right.remoteEditorWidget; } //rightWidget.addClass('cm-mergeViewEditor'); this.gridPanel.addWidget(rightWidget); @@ -1300,16 +1678,19 @@ class MergeView extends Panel { node: elt('div', null, 'CodeMirror-merge-clear', 'height: 0; clear: both;') }));*/ - merge = this.merge = new DiffView(merged, 'merge', this.alignViews.bind(this), - copyObj({readOnly}, copyObj(dvOptions))); + merge = this.merge = new DiffView( + merged, + 'merge', + this.alignViews.bind(this), + copyObj({ readOnly }, copyObj(dvOptions)) + ); this.diffViews.push(merge); - let mergeWidget = merge.ownWidget; - // mergeWidget.addClass('cm-mergeViewEditor'); + let mergeWidget = merge.remoteEditorWidget; + // mergeWidget.addClass('cm-mergeViewEditor'); this.gridPanel.addWidget(mergeWidget); mergeWidget.addClass('cm-merge-editor'); - - - } else if (remote) { // If in place for type guard + } else if (remote) { + // If in place for type guard this.gridPanel.addWidget(this.base); if (remote.unchanged || remote.added || remote.deleted) { @@ -1321,16 +1702,21 @@ class MergeView extends Panel { //this.base.addClass('CodeMirror-merge-pane-deleted'); } } else { - right = this.right = new DiffView(remote, 'right', this.alignViews.bind(this), dvOptions); + right = this.right = new DiffView( + remote, + 'right', + this.alignViews.bind(this), + dvOptions + ); this.diffViews.push(right); - let rightWidget = right.ownWidget; + let rightWidget = right.remoteEditorWidget; //rightWidget.addClass('CodeMirror-merge-pane-remote'); //this.addWidget(new Widget({node: right.buildGap()})); //rightWidget.addClass('cm-mergeViewEditor'); this.gridPanel.addWidget(rightWidget); - rightWidget.addClass('cm-right-editor') + rightWidget.addClass('cm-right-editor'); //panes = 2; } @@ -1339,14 +1725,13 @@ class MergeView extends Panel { }));*/ } - for (let dv of [left, right, merge]) { if (dv) { dv.init(this.base); } } - /* if (options.collapseIdentical && panes > 1) { + /* if (options.collapseIdentical && panes > 1) { this.base.cm.operation(function() { collapseIdenticalStretches(self, options.collapseIdentical); }); @@ -1400,7 +1785,7 @@ class MergeView extends Panel { let cm: CodeMirror.Editor[] = [self.base.cm]; let scroll: number[] = []; for (let dv of self.diffViews) { - cm.push(dv.ownEditor); + cm.push(dv.remoteEditor); } for (let i = 0; i < cm.length; i++) { scroll.push(cm[i].getScrollInfo().top); @@ -1423,9 +1808,80 @@ class MergeView extends Panel { }(f); } for (let dv of this.diffViews) { - if (!dv.ownEditor.curOp) { + if (!dv.remoteEditor.curOp) { f = function(fn) { - return function() { dv.ownEditor.operation(fn); }; + return function() { dv.remoteEditor.operation(fn); }; + }(f); + } + } + // Perform alignment + f(); + /*} + + + /*CM6 version */ + /*alignViews(force?: boolean) { + let dealigned = false; + if (!this.initialized) { + return; + } + for (let dv of this.diffViews) { + dv.syncModel(); + if (dv.dealigned) { + dealigned = true; + dv.dealigned = false; + } + } + + if (!dealigned && !force) { + return; // Nothing to do + } + // Find matching lines + let linesToAlign = findAlignedLines(this.diffViews); + + // Function modifying DOM to perform alignment: + let self: MergeView = this; + let f = function () { + + // Clear old aligners + 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; + + // Editors (order is important, so it matches + // format of linesToAlign) + let cm: EditorView[] = [self.base.cm]; + let scroll: number[] = []; + for (let dv of self.diffViews) { + cm.push(dv.remoteEditorWidget.cm); + } + for (let i = 0; i < cm.length; i++) { + scroll.push(cm[i].getScrollInfo().top); + } + + for (let ln = 0; ln < linesToAlign.length; ln++) { + alignLines(cm, linesToAlign[ln], aligners); + } + + for (let i = 0; i < cm.length; i++) { + cm[i].scrollTo(null, scroll[i]); + } + }; + + // All editors should have an operation (simultaneously), + // so set up nested operation calls. + if (!this.base.cm.curOp) { + f = function(fn) { + return function() { self.base.cm.operation(fn); }; + }(f); + } + for (let dv of this.diffViews) { + if (!dv.remoteEditorWidget.cm.curOp) { + f = function(fn) { + return function() { dv.remoteEditorWidget.cm.operation(fn); }; }(f); } } @@ -1446,7 +1902,7 @@ class MergeView extends Panel { /*if (!this.merge) { throw new Error('No merged value; missing "merged" view'); } - return this.merge.ownEditor.getValue();*/ + return this.merge.remoteEditor.getValue();*/ return ''; } //splitPanel: SplitPanel; @@ -1459,9 +1915,9 @@ class MergeView extends Panel { base: EditorWidget; options: any; diffViews: DiffView[]; - /*aligners: CodeMirror.LineWidget[];*/ + aligners: LineWidget[]; initialized: boolean = false; - collapsedRanges: {size: number, line: number}[] = []; + collapsedRanges: { size: number; line: number }[] = []; } /*function collapseSingle(cm: CodeMirror.Editor, from: number, to: number): {mark: CodeMirror.TextMarker, clear: () => void} { @@ -1548,15 +2004,15 @@ class MergeView extends Panel { [{line: line, cm: edit}]; if (mv.left) { editors.push({line: getMatchingEditLine(line, mv.left.chunks), - cm: mv.left.ownEditor}); + cm: mv.left.remoteEditor}); } if (mv.right) { editors.push({line: getMatchingEditLine(line, mv.right.chunks), - cm: mv.right.ownEditor}); + cm: mv.right.remoteEditor}); } if (mv.merge) { editors.push({line: getMatchingEditLine(line, mv.merge.chunks), - cm: mv.merge.ownEditor}); + cm: mv.merge.remoteEditor}); } let mark = collapseStretch(size, editors); mv.collapsedRanges.push({line, size}); @@ -1579,7 +2035,12 @@ class MergeView extends Panel { // General utilities -function elt(tag: string, content?: string | HTMLElement[] | null, className?: string | null, style?: string | null): HTMLElement { +function elt( + tag: string, + content?: string | HTMLElement[] | null, + className?: string | null, + style?: string | null +): HTMLElement { let e = document.createElement(tag); if (className) { e.className = className; @@ -1591,11 +2052,11 @@ function elt(tag: string, content?: string | HTMLElement[] | null, className?: s e.appendChild(document.createTextNode(content)); } else if (content) { for (let i = 0; i < content.length; ++i) { - e.appendChild((content)[i]); + e.appendChild(content[i]); } } return e; -}; +} /*function findPrevDiff(chunks: Chunk[], start: number, isOrig: boolean): number | null { for (let i = chunks.length - 1; i >= 0; i--) { @@ -1631,7 +2092,7 @@ function elt(tag: string, content?: string | HTMLElement[] | null, className?: s if (views) { for (let i = 0; i < views.length; i++) { let dv = views[i]; - let isOrig = cm === dv.ownEditor; + let isOrig = cm === dv.remoteEditor; let pos = dir === DiffDirection.Previous ? findPrevDiff(dv.chunks, line, isOrig) : findNextDiff(dv.chunks, line, isOrig); diff --git a/packages/nbdime/src/styles/common.css b/packages/nbdime/src/styles/common.css index 055a0275..83ca34ff 100644 --- a/packages/nbdime/src/styles/common.css +++ b/packages/nbdime/src/styles/common.css @@ -1,5 +1,6 @@ -/*.nbdime-root .CodeMirror-merge { +/* +.nbdime-root .CodeMirror-merge { position: relative; white-space: pre;*/ /* style as jp-InputArea-editor: */ @@ -20,25 +21,31 @@ display: none; } +/* Reintroduced .nbdime-root .CodeMirror-merge-pane-unchanged { width: 100%; } + .nbdime-root .CodeMirror-merge-1pane .CodeMirror-merge-gap { width: 6%; } .nbdime-root .CodeMirror-merge-2pane .CodeMirror-merge-pane { width: 47%; } .nbdime-root .CodeMirror-merge-2pane .CodeMirror-merge-gap { width: 6%; } .nbdime-root .CodeMirror-merge-3pane .CodeMirror-merge-pane { width: 31%; } .nbdime-root .CodeMirror-merge-3pane .CodeMirror-merge-gap { width: 3.5%; } +/* Reintroduced .nbdime-root .CodeMirror-merge-pane { display: inline-block; white-space: normal; vertical-align: top; width: 100%; } + +/* Reintroduced .nbdime-root .CodeMirror-merge-pane-rightmost { position: absolute; right: 0px; z-index: 1; } + .nbdime-root .p-Widget.CodeMirror-merge-gap { z-index: 2; display: inline-block; @@ -64,13 +71,11 @@ line-height: 1; } +/* Reintroduced .nbdime-root .CodeMirror-merge-r-inserted, .CodeMirror-merge-l-inserted { background-color: var(--jp-diff-added-color1); } -.nbdime-root .CodeMirror-merge-r-deleted, .CodeMirror-merge-l-deleted { - background-color: var(--jp-diff-deleted-color1); -} .nbdime-root .CodeMirror-merge-collapsed-widget:before { content: "(...)"; @@ -86,6 +91,8 @@ } .nbdime-root .CodeMirror-merge-collapsed-line .CodeMirror-gutter-elt { display: none; } */ + + .cm-grid-panel { display: grid; grid-template-columns: repeat(3, 1fr); @@ -113,7 +120,6 @@ grid-row: 2; } - .nbdime-root .cm-merge-spacer { background-image: repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(255,255,255,.5) 5px, rgba(255,255,255,.5) 10px); } @@ -135,13 +141,22 @@ line-height: 1; } -.cm-mergeViewEditors { - display: flex; - align-items: stretch; - +.nbdime-root .cm-merge-pane { + display: inline-block; + white-space: normal; + vertical-align: top; + width: 100%; } - -.cm-mergeViewEditor { - flex-grow: 1; +.nbdime-root .cm-merge-pane-rightmost { + position: absolute; + right: 0px; + z-index: 1; } +.nbdime-root .cm-merge-pane-unchanged { width: 100%; } + + +.nbdime-root .cm-merge-r-deleted, .cm-l-deleted, .cm-merge-m-deleted { + background-color: initial; + color: #b00 +} \ No newline at end of file diff --git a/packages/nbdime/src/styles/diff.css b/packages/nbdime/src/styles/diff.css index 4b31500f..a7229157 100644 --- a/packages/nbdime/src/styles/diff.css +++ b/packages/nbdime/src/styles/diff.css @@ -20,7 +20,7 @@ } /* Match input border of unchanged cell source */ -.jp-Notebook-diff .jp-Diff-unchanged .CodeMirror-merge-pane-unchanged { +.jp-Notebook-diff .jp-Diff-unchanged .cm-merge-pane-unchanged { border: var(--jp-border-width) solid var(--jp-private-notebook-cell-editor-border); } @@ -126,13 +126,13 @@ max-width: 100%; } -.jp-Notebook-diff .CodeMirror-merge-pane-remote { +.jp-Notebook-diff .cm-merge-pane-remote { position: relative; left: 6%; } -.jp-Notebook-diff .jp-Cellrow-source .CodeMirror-merge-pane-deleted, -.jp-Notebook-diff .jp-Cellrow-source .CodeMirror-merge-pane-added { +.jp-Notebook-diff .jp-Cellrow-source .cm-merge-pane-deleted, +.jp-Notebook-diff .jp-Cellrow-source .cm-merge-pane-added { width: 75%; } @@ -146,17 +146,17 @@ } /* Color diff highlighting according to style vars */ -.jp-Notebook-diff .CodeMirror-merge-r-chunk { background-color: var(--jp-diff-deleted-color2); } -.jp-Notebook-diff .CodeMirror-merge-r-chunk-start { border-top: 1px solid var(--jp-diff-deleted-color0); } -.jp-Notebook-diff .CodeMirror-merge-r-chunk-end { border-bottom: 1px solid var(--jp-diff-deleted-color0); } -.jp-Notebook-diff .CodeMirror-merge-r-connect { fill: var(--jp-diff-deleted-color2); stroke: var(--jp-diff-deleted-color0); stroke-width: 1px; } -.jp-Notebook-diff .CodeMirror-merge-spacer { background-color: var(--jp-diff-deleted-color2); } +.jp-Notebook-diff .cm-merge-r-chunk { background-color: var(--jp-diff-deleted-color2); } +.jp-Notebook-diff .cm-merge-r-chunk-start { border-top: 1px solid var(--jp-diff-deleted-color0); } +.jp-Notebook-diff .cm-merge-r-chunk-end { border-bottom: 1px solid var(--jp-diff-deleted-color0); } +.jp-Notebook-diff .cm-merge-r-connect { fill: var(--jp-diff-deleted-color2); stroke: var(--jp-diff-deleted-color0); stroke-width: 1px; } +.jp-Notebook-diff .cm-merge-spacer { background-color: var(--jp-diff-deleted-color2); } -.jp-Notebook-diff .CodeMirror-merge-pane-remote .CodeMirror-merge-r-chunk { background-color: var(--jp-diff-added-color2); } -.jp-Notebook-diff .CodeMirror-merge-pane-remote .CodeMirror-merge-r-chunk-start { border-top: 1px solid var(--jp-diff-added-color0); } -.jp-Notebook-diff .CodeMirror-merge-pane-remote .CodeMirror-merge-r-chunk-end { border-bottom: 1px solid var(--jp-diff-added-color0); } -.jp-Notebook-diff .CodeMirror-merge-pane-remote .CodeMirror-merge-r-connect { fill: var(--jp-diff-added-color2); stroke: var(--jp-diff-added-color0); stroke-width: 1px; } -.jp-Notebook-diff .CodeMirror-merge-pane-remote .CodeMirror-merge-spacer { background-color: var(--jp-diff-added-color2); } +.jp-Notebook-diff .cm-merge-pane-remote .cm-merge-r-chunk { background-color: var(--jp-diff-added-color2); } +.jp-Notebook-diff .cm-merge-pane-remote .cm-merge-r-chunk-start { border-top: 1px solid var(--jp-diff-added-color0); } +.jp-Notebook-diff .cm-merge-pane-remote .cm-merge-r-chunk-end { border-bottom: 1px solid var(--jp-diff-added-color0); } +.jp-Notebook-diff .cm-merge-pane-remote .cm-merge-r-connect { fill: var(--jp-diff-added-color2); stroke: var(--jp-diff-added-color0); stroke-width: 1px; } +.jp-Notebook-diff .cm-merge-pane-remote .cm-merge-spacer { background-color: var(--jp-diff-added-color2); } .jp-Notebook-diff .jp-Diff-deleted .CodeMirror, @@ -173,8 +173,8 @@ border: solid 1px var(--jp-diff-added-color0); } -.jp-Notebook-diff .jp-Diff-added .CodeMirror-merge-pane-added, -.jp-Notebook-diff .jp-Diff-deleted .CodeMirror-merge-pane-deleted { +.jp-Notebook-diff .jp-Diff-added .cm-merge-pane-added, +.jp-Notebook-diff .jp-Diff-deleted .cm-merge-pane-deleted { width: 100%; } diff --git a/packages/nbdime/src/styles/merge.css b/packages/nbdime/src/styles/merge.css index 05370777..a90da909 100644 --- a/packages/nbdime/src/styles/merge.css +++ b/packages/nbdime/src/styles/merge.css @@ -23,13 +23,13 @@ margin-top: 20px; } -.jp-Notebook-merge .CodeMirror-merge-pane-local, -.jp-Notebook-merge .CodeMirror-merge-pane-remote, -.jp-Notebook-merge .CodeMirror-merge-pane-base:not(.CodeMirror-merge-pane-unchanged):not(.CodeMirror-merge-pane-deleted):not(.CodeMirror-merge-pane-added) { +.jp-Notebook-merge .cm-merge-pane-local, +.jp-Notebook-merge .cm-merge-pane-remote, +.jp-Notebook-merge .cm-merge-pane-base:not(.cm-merge-pane-unchanged):not(.cm-merge-pane-deleted):not(.cm-merge-pane-added) { width: 33%; } -.jp-Notebook-merge .CodeMirror-merge-pane-final { +.jp-Notebook-merge .cm-merge-pane-final { width: 100%; border-top: var(--codemirror-border); } @@ -81,23 +81,13 @@ text-align: center; } -.jp-Notebook-merge .CodeMirror-merge-pane-deleted, -.jp-Notebook-merge .CodeMirror-merge-pane-added { +.jp-Notebook-merge .cm-merge-pane-deleted, +.jp-Notebook-merge .cm-merge-pane-added { width: 100%; } .jp-Notebook-merge .cm-merge-spacer { background-color: #eee; } -/*.cm-merge-spacer {*/ - /*width: 400px;*/ - /*height: 40px;*/ - /*background: repeating-linear-gradient( - 135deg, - #97979b 10px, - #fff 20px - );*/ - /* background: repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(255,255,255,.5) 5px, rgba(255,255,255,.5) 10px); -}*/ .jp-Notebook-merge .cm-merge-r-chunk { background-color: var(--jp-merge-remote-color2); } .jp-Notebook-merge .cm-merge-r-chunk-start, @@ -107,8 +97,8 @@ .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 + .CodeMirror-linewidget .cm-merge-spacer, -.jp-Notebook-merge .cm-merge-m-chunk-end-remote-empty + .CodeMirror-linewidget .cm-merge-spacer { +.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 { border-bottom: 1px solid var(--jp-merge-remote-color1); background-color: var(--jp-merge-remote-color3); } @@ -121,8 +111,8 @@ .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 + .CodeMirror-linewidget .cm-merge-spacer, -.jp-Notebook-merge .cm-merge-m-chunk-end-local-empty + .CodeMirror-linewidget .cm-merge-spacer { +.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 { border-bottom: 1px solid var(--jp-merge-local-color1); background-color: var(--jp-merge-local-color3); } @@ -133,12 +123,12 @@ -.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 .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-end-empty.cm-merge-r-chunk-end-empty + .CodeMirror-linewidget .cm-merge-spacer, -.jp-Notebook-merge .cm-merge-m-chunk-end-remote-empty.cm-merge-m-chunk-end-local-empty + .CodeMirror-linewidget .cm-merge-spacer { +.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 { border-bottom: 1px solid var(--jp-merge-both-color1); background-color: var(--jp-merge-both-color2); } @@ -149,13 +139,12 @@ .jp-Notebook-merge .cm-merge-m-chunk-end-either-empty, .jp-Notebook-merge .cm-merge-m-chunk-end-remote-empty, .jp-Notebook-merge .cm-merge-m-chunk-end-local-empty, -.jp-Notebook-merge .CodeMirror pre.CodeMirror-line { +.jp-Notebook-merge .CodeMirror pre.cm-line { z-index: 3; } - .jp-Notebook-merge .cm-merge-m-chunk-start-either, -.jp-Notebook-merge .cm-merge-m-chunk-start-mixed:not(.CodeMirror-merge-m-chunk-custom) { +.jp-Notebook-merge .cm-merge-m-chunk-start-mixed:not(.cm-merge-m-chunk-custom) { border-top: 1px solid var(--jp-merge-either-color1); } .jp-Notebook-merge .cm-merge-m-chunk-end-either, @@ -163,14 +152,14 @@ border-bottom: 1px solid var(--jp-merge-either-color1); } -.jp-Notebook-merge .CodeMirror-merge-m-chunk-end-either-empty + .CodeMirror-linewidget .cm-merge-spacer { +.jp-Notebook-merge .cm-merge-m-chunk-end-either-empty + .cm-linewidget .cm-merge-spacer { border-bottom: 1px solid var(--jp-merge-either-color1); background-color: var(--jp-merge-either-color2); } -.jp-Notebook-merge .CodeMirror-merge-pane-base .CodeMirror-line .CodeMirror-merge-m-deleted, -.jp-Notebook-merge .CodeMirror-merge-pane-base .CodeMirror-line .CodeMirror-merge-l-deleted, -.jp-Notebook-merge .CodeMirror-merge-pane-base .CodeMirror-line .CodeMirror-merge-r-deleted { +.jp-Notebook-merge .cm-merge-pane-base .cm-line .cm-merge-m-deleted, +.jp-Notebook-merge .cm-merge-pane-base .cm-line .cm-merge-l-deleted, +.jp-Notebook-merge .cm-merge-pane-base .cm-line .cm-merge-r-deleted { background-color: initial; color: #b00 } @@ -216,11 +205,11 @@ background-color: var(--jp-merge-both-color2); } -.jp-Notebook-merge .CodeMirror-merge-pane.CodeMirror-merge-pane-local.jp-mod-missing { +.jp-Notebook-merge .cm-merge-pane.cm-merge-pane-local.jp-mod-missing { background-color: var(--jp-merge-local-color2); } -.jp-Notebook-merge .CodeMirror-merge-pane.CodeMirror-merge-pane-remote.jp-mod-missing { +.jp-Notebook-merge .cm-merge-pane.cm-merge-pane-remote.jp-mod-missing { background-color: var(--jp-merge-remote-color2); }