|
| 1 | +import { |
| 2 | + ChangeDetectionStrategy, |
| 3 | + Component, |
| 4 | + ElementRef, |
| 5 | + EventEmitter, |
| 6 | + Input, |
| 7 | + OnChanges, |
| 8 | + OnDestroy, |
| 9 | + OnInit, |
| 10 | + Output, |
| 11 | + SimpleChanges, |
| 12 | + ViewEncapsulation, |
| 13 | + booleanAttribute, |
| 14 | +} from '@angular/core'; |
| 15 | + |
| 16 | +import { External, Setup } from '@acrodata/code-editor'; |
| 17 | +import { DiffConfig, MergeView } from '@codemirror/merge'; |
| 18 | +import { Extension } from '@codemirror/state'; |
| 19 | +import { EditorView } from '@codemirror/view'; |
| 20 | +import { basicSetup, minimalSetup } from 'codemirror'; |
| 21 | + |
| 22 | +export type Orientation = 'a-b' | 'b-a'; |
| 23 | +export type RevertControls = 'a-to-b' | 'b-to-a'; |
| 24 | +export type RenderRevertControl = () => HTMLElement; |
| 25 | + |
| 26 | +@Component({ |
| 27 | + selector: 'diff-editor', |
| 28 | + standalone: true, |
| 29 | + template: ``, |
| 30 | + styles: ` |
| 31 | + .diff-editor { |
| 32 | + display: block; |
| 33 | +
|
| 34 | + .cm-mergeView, |
| 35 | + .cm-mergeViewEditors { |
| 36 | + height: 100%; |
| 37 | + } |
| 38 | +
|
| 39 | + .cm-mergeView .cm-editor, |
| 40 | + .cm-mergeView .cm-scroller { |
| 41 | + height: 100% !important; |
| 42 | + } |
| 43 | + } |
| 44 | + `, |
| 45 | + host: { |
| 46 | + class: 'diff-editor', |
| 47 | + }, |
| 48 | + encapsulation: ViewEncapsulation.None, |
| 49 | + changeDetection: ChangeDetectionStrategy.OnPush, |
| 50 | +}) |
| 51 | +export class DiffEditor implements OnChanges, OnInit, OnDestroy { |
| 52 | + /** |
| 53 | + * The editor's built-in setup. The value can be set to |
| 54 | + * [`basic`](https://codemirror.net/docs/ref/#codemirror.basicSetup), |
| 55 | + * [`minimal`](https://codemirror.net/docs/ref/#codemirror.minimalSetup) or `null`. |
| 56 | + * |
| 57 | + * Don't support change dynamically! |
| 58 | + */ |
| 59 | + @Input() setup: Setup = 'basic'; |
| 60 | + |
| 61 | + /** The diff-editor's original value. */ |
| 62 | + @Input() originalValue = ''; |
| 63 | + |
| 64 | + /** |
| 65 | + * The MergeView original config's |
| 66 | + * [extensions](https://codemirror.net/docs/ref/#state.EditorStateConfig.extensions). |
| 67 | + * |
| 68 | + * Don't support change dynamically! |
| 69 | + */ |
| 70 | + @Input() originalExtensions: Extension[] = []; |
| 71 | + |
| 72 | + /** The diff-editor's modified value. */ |
| 73 | + @Input() modifiedValue = ''; |
| 74 | + |
| 75 | + /** |
| 76 | + * The MergeView modified config's |
| 77 | + * [extensions](https://codemirror.net/docs/ref/#state.EditorStateConfig.extensions). |
| 78 | + * |
| 79 | + * Don't support change dynamically! |
| 80 | + */ |
| 81 | + @Input() modifiedExtensions: Extension[] = []; |
| 82 | + |
| 83 | + /** Controls whether editor A or editor B is shown first. Defaults to `"a-b"`. */ |
| 84 | + @Input() orientation?: Orientation; |
| 85 | + |
| 86 | + /** Controls whether revert controls are shown between changed chunks. */ |
| 87 | + @Input() revertControls?: RevertControls; |
| 88 | + |
| 89 | + /** When given, this function is called to render the button to revert a chunk. */ |
| 90 | + @Input() renderRevertControl?: RenderRevertControl; |
| 91 | + |
| 92 | + /** |
| 93 | + * By default, the merge view will mark inserted and deleted text |
| 94 | + * in changed chunks. Set this to false to turn that off. |
| 95 | + */ |
| 96 | + @Input({ transform: booleanAttribute }) highlightChanges = true; |
| 97 | + |
| 98 | + /** Controls whether a gutter marker is shown next to changed lines. */ |
| 99 | + @Input({ transform: booleanAttribute }) gutter = true; |
| 100 | + |
| 101 | + /** |
| 102 | + * When given, long stretches of unchanged text are collapsed. |
| 103 | + * `margin` gives the number of lines to leave visible after/before |
| 104 | + * a change (default is 3), and `minSize` gives the minimum amount |
| 105 | + * of collapsible lines that need to be present (defaults to 4). |
| 106 | + */ |
| 107 | + @Input() collapseUnchanged?: { margin?: number; minSize?: number }; |
| 108 | + |
| 109 | + /** Pass options to the diff algorithm. */ |
| 110 | + @Input() diffConfig?: DiffConfig; |
| 111 | + |
| 112 | + /** Event emitted when the editor's original value changes. */ |
| 113 | + @Output() originalValueChange = new EventEmitter<string>(); |
| 114 | + |
| 115 | + /** Event emitted when the editor's modified value changes. */ |
| 116 | + @Output() modifiedValueChange = new EventEmitter<string>(); |
| 117 | + |
| 118 | + constructor(private _elementRef: ElementRef<Element>) {} |
| 119 | + |
| 120 | + /** The merge view instance. */ |
| 121 | + mergeView?: MergeView; |
| 122 | + |
| 123 | + private _updateListener = (valueChange: EventEmitter<string>) => { |
| 124 | + return EditorView.updateListener.of(vu => { |
| 125 | + if (vu.docChanged && !vu.transactions.some(tr => tr.annotation(External))) { |
| 126 | + const value = vu.state.doc.toString(); |
| 127 | + valueChange.emit(value); |
| 128 | + } |
| 129 | + }); |
| 130 | + }; |
| 131 | + |
| 132 | + ngOnChanges(changes: SimpleChanges): void { |
| 133 | + if (changes['originalValue']) { |
| 134 | + this.setOriginalValue(this.originalValue); |
| 135 | + } |
| 136 | + if (changes['modifiedValue']) { |
| 137 | + this.setModifiedValue(this.modifiedValue); |
| 138 | + } |
| 139 | + if (changes['orientation']) { |
| 140 | + this.mergeView?.reconfigure({ orientation: this.orientation }); |
| 141 | + } |
| 142 | + if (changes['revertControls']) { |
| 143 | + this.mergeView?.reconfigure({ revertControls: this.revertControls }); |
| 144 | + } |
| 145 | + if (changes['renderRevertControl']) { |
| 146 | + this.mergeView?.reconfigure({ renderRevertControl: this.renderRevertControl }); |
| 147 | + } |
| 148 | + if (changes['highlightChanges']) { |
| 149 | + this.mergeView?.reconfigure({ highlightChanges: this.highlightChanges }); |
| 150 | + } |
| 151 | + if (changes['gutter']) { |
| 152 | + this.mergeView?.reconfigure({ gutter: this.gutter }); |
| 153 | + } |
| 154 | + if (changes['collapseUnchanged']) { |
| 155 | + this.mergeView?.reconfigure({ collapseUnchanged: this.collapseUnchanged }); |
| 156 | + } |
| 157 | + if (changes['diffConfig']) { |
| 158 | + this.mergeView?.reconfigure({ diffConfig: this.diffConfig }); |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + ngOnInit(): void { |
| 163 | + this.mergeView = new MergeView({ |
| 164 | + parent: this._elementRef.nativeElement, |
| 165 | + a: { |
| 166 | + doc: this.originalValue, |
| 167 | + extensions: [ |
| 168 | + this._updateListener(this.originalValueChange), |
| 169 | + this.setup === 'basic' ? basicSetup : this.setup === 'minimal' ? minimalSetup : [], |
| 170 | + ...this.originalExtensions, |
| 171 | + ], |
| 172 | + }, |
| 173 | + b: { |
| 174 | + doc: this.modifiedValue, |
| 175 | + extensions: [ |
| 176 | + this._updateListener(this.modifiedValueChange), |
| 177 | + this.setup === 'basic' ? basicSetup : this.setup === 'minimal' ? minimalSetup : [], |
| 178 | + ...this.modifiedExtensions, |
| 179 | + ], |
| 180 | + }, |
| 181 | + orientation: this.orientation, |
| 182 | + revertControls: this.revertControls, |
| 183 | + renderRevertControl: this.renderRevertControl, |
| 184 | + highlightChanges: this.highlightChanges, |
| 185 | + gutter: this.gutter, |
| 186 | + collapseUnchanged: this.collapseUnchanged, |
| 187 | + diffConfig: this.diffConfig, |
| 188 | + }); |
| 189 | + } |
| 190 | + |
| 191 | + ngOnDestroy(): void { |
| 192 | + this.mergeView?.destroy(); |
| 193 | + } |
| 194 | + |
| 195 | + /** Sets diff-editor's original value. */ |
| 196 | + setOriginalValue(value: string) { |
| 197 | + this.mergeView?.a.dispatch({ |
| 198 | + changes: { from: 0, to: this.mergeView.a.state.doc.length, insert: value }, |
| 199 | + }); |
| 200 | + } |
| 201 | + |
| 202 | + /** Sets diff-editor's modified value. */ |
| 203 | + setModifiedValue(value: string) { |
| 204 | + this.mergeView?.b.dispatch({ |
| 205 | + changes: { from: 0, to: this.mergeView.b.state.doc.length, insert: value }, |
| 206 | + }); |
| 207 | + } |
| 208 | +} |
0 commit comments