Skip to content

Commit 421a574

Browse files
committed
feat: add diff-editor
1 parent 28e8032 commit 421a574

File tree

12 files changed

+290
-4
lines changed

12 files changed

+290
-4
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@angular/platform-browser-dynamic": "^17.3.0",
3737
"@angular/router": "^17.3.0",
3838
"@codemirror/language-data": "^6.5.1",
39+
"@codemirror/merge": "^6.6.2",
3940
"@codemirror/theme-one-dark": "^6.1.2",
4041
"@ng-matero/extensions": "^17.3.0",
4142
"codemirror": "^6.0.1",
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { NgModule } from '@angular/core';
22
import { CodeEditor } from './code-editor';
3+
import { DiffEditor } from './diff-editor';
34

45
@NgModule({
5-
imports: [CodeEditor],
6-
exports: [CodeEditor],
6+
imports: [CodeEditor, DiffEditor],
7+
exports: [CodeEditor, DiffEditor],
78
})
89
export class CodeEditorModule {}

projects/code-editor/code-editor.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { basicSetup, minimalSetup } from 'codemirror';
2323
export type Theme = 'light' | 'dark' | Extension;
2424
export type Setup = 'basic' | 'minimal' | null;
2525

26-
const External = Annotation.define<boolean>();
26+
export const External = Annotation.define<boolean>();
2727

2828
@Component({
2929
selector: 'code-editor',
@@ -54,10 +54,16 @@ const External = Annotation.define<boolean>();
5454
export class CodeEditor implements OnInit, OnDestroy, ControlValueAccessor {
5555
/**
5656
* EditorView's [root](https://codemirror.net/docs/ref/#view.EditorView.root).
57+
*
58+
* Don't support change dynamically!
5759
*/
5860
@Input() root?: Document | ShadowRoot;
5961

60-
/** Whether focus on the editor after init. */
62+
/**
63+
* Whether focus on the editor after init.
64+
*
65+
* Don't support change dynamically!
66+
*/
6167
@Input({ transform: booleanAttribute }) autoFocus = false;
6268

6369
/** Whether the editor is disabled. */
@@ -154,6 +160,8 @@ export class CodeEditor implements OnInit, OnDestroy, ControlValueAccessor {
154160
/**
155161
* An array of language descriptions for known
156162
* [language-data](https://github.com/codemirror/language-data/blob/main/src/language-data.ts).
163+
*
164+
* Don't support change dynamically!
157165
*/
158166
@Input() languages: LanguageDescription[] = [];
159167

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
}

projects/code-editor/ng-package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"entryFile": "public-api.ts"
66
},
77
"allowedNonPeerDependencies": [
8+
"@codemirror/merge",
89
"@codemirror/theme-one-dark",
910
"codemirror"
1011
]

projects/code-editor/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@angular/forms": ">=16.0.0"
2525
},
2626
"dependencies": {
27+
"@codemirror/merge": "^6.0.0",
2728
"@codemirror/theme-one-dark": "^6.0.0",
2829
"codemirror": "^6.0.0",
2930
"tslib": "^2.3.0"

projects/code-editor/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44

55
export * from './code-editor-module';
66
export * from './code-editor';
7+
export * from './diff-editor';

projects/dev-app/src/app/app.routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Routes } from '@angular/router';
22
import { LayoutComponent } from './layout/layout.component';
33
import { HomeComponent } from './home/home.component';
4+
import { DiffComponent } from './diff/diff.component';
45

56
export const routes: Routes = [
67
{
@@ -9,6 +10,7 @@ export const routes: Routes = [
910
children: [
1011
{ path: '', pathMatch: 'full', redirectTo: 'home' },
1112
{ path: 'home', component: HomeComponent },
13+
{ path: 'diff', component: DiffComponent },
1214
],
1315
},
1416
];
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<diff-editor #editor [style.height.px]="200"
2+
[originalValue]="doc" [modifiedValue]="doc2"
3+
(originalValueChange)="log($event)"
4+
(modifiedValueChange)="log($event)"
5+
[orientation]="orientation" [revertControls]="revertControls"
6+
[highlightChanges]="highlightChanges" [gutter]="gutter" />
7+
8+
<button (click)="doc='123'">change original value</button>
9+
<button (click)="doc2='13\n456'">change modified value</button>
10+
<button (click)="orientation==='a-b'?orientation='b-a':orientation='a-b'">orientation</button>
11+
<button (click)="revertControls==='a-to-b'?revertControls='b-to-a':revertControls='a-to-b'">revertControls</button>
12+
<button (click)="highlightChanges=!highlightChanges">highlightChanges</button>
13+
<button (click)="gutter=!gutter">gutter</button>
14+
15+
<code-editor [value]="doc2" [extensions]="unifiedExts" />

projects/dev-app/src/app/diff/diff.component.scss

Whitespace-only changes.

0 commit comments

Comments
 (0)