Skip to content

Commit 4900760

Browse files
authored
feat(DesignerV2): Added merge/diff view to code editor component (#8819)
* Added merge view to code editor component * Added tests * Fixed a few issues
1 parent 2379db4 commit 4900760

File tree

6 files changed

+250
-48
lines changed

6 files changed

+250
-48
lines changed

apps/Standalone/src/designer/app/AzureLogicAppsDesigner/CodeViewV2.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface CodeViewProps {
1313
isConsumption?: boolean;
1414
}
1515

16+
// eslint-disable-next-line react/display-name
1617
const CodeViewEditor = forwardRef(({ workflowKind, isConsumption }: CodeViewProps, ref) => {
1718
const dispatch = useDispatch<AppDispatch>();
1819
const isWorkflowIsDirty = useIsWorkflowDirty();

libs/designer-ui/package.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@codemirror/language": "^6.10.0",
1313
"@codemirror/legacy-modes": "^6.4.0",
1414
"@codemirror/lint": "^6.8.0",
15+
"@codemirror/merge": "^6.12.0",
1516
"@codemirror/search": "^6.5.0",
1617
"@codemirror/state": "^6.4.0",
1718
"@codemirror/view": "^6.28.0",
@@ -21,9 +22,6 @@
2122
"@fluentui/react-icons": "2.0.224",
2223
"@fluentui/theme": "2.6.25",
2324
"@fluentui/utilities": "8.15.0",
24-
"@lezer/common": "^1.2.0",
25-
"@lezer/highlight": "^1.2.0",
26-
"@lezer/lr": "^1.4.0",
2725
"@lexical/html": "0.33.1",
2826
"@lexical/link": "0.33.1",
2927
"@lexical/list": "0.33.1",
@@ -32,6 +30,9 @@
3230
"@lexical/selection": "0.33.1",
3331
"@lexical/table": "0.33.1",
3432
"@lexical/utils": "0.33.1",
33+
"@lezer/common": "^1.2.0",
34+
"@lezer/highlight": "^1.2.0",
35+
"@lezer/lr": "^1.4.0",
3536
"@microsoft/logic-apps-shared": "workspace:*",
3637
"@react-hookz/web": "22.0.0",
3738
"@xyflow/react": "^12.3.5",
@@ -60,7 +61,10 @@
6061
},
6162
"./package.json": "./package.json"
6263
},
63-
"files": ["build/lib/**/*", "src"],
64+
"files": [
65+
"build/lib/**/*",
66+
"src"
67+
],
6468
"license": "MIT",
6569
"main": "src/index.ts",
6670
"module": "src/index.ts",

libs/designer-ui/src/lib/editor/codemirror/CodeMirrorEditor.tsx

Lines changed: 125 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { xml } from '@codemirror/lang-xml';
1212
import { yaml } from '@codemirror/lang-yaml';
1313
import { csharp } from '@codemirror/legacy-modes/mode/clike';
1414
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
15+
import { MergeView } from '@codemirror/merge';
1516
import { useTheme } from '@fluentui/react';
1617
import { EditorLanguage } from '@microsoft/logic-apps-shared';
1718
import { createFluentTheme } from './themes/fluent';
@@ -59,6 +60,8 @@ export const CodeMirrorEditor = forwardRef<CodeMirrorEditorRef, CodeMirrorEditor
5960
className,
6061
defaultValue = '',
6162
value,
63+
originalValue,
64+
showMerge,
6265
language,
6366
height,
6467
width,
@@ -87,6 +90,7 @@ export const CodeMirrorEditor = forwardRef<CodeMirrorEditorRef, CodeMirrorEditor
8790
const { isInverted } = useTheme();
8891
const containerRef = useRef<HTMLDivElement>(null);
8992
const viewRef = useRef<EditorView | null>(null);
93+
const mergeViewRef = useRef<MergeView | null>(null);
9094
const isInitializedRef = useRef(false);
9195

9296
// Create ref methods
@@ -149,6 +153,54 @@ export const CodeMirrorEditor = forwardRef<CodeMirrorEditorRef, CodeMirrorEditor
149153
}
150154
isInitializedRef.current = true;
151155

156+
const baseThemeSpec = {
157+
'&': {
158+
fontSize: `${fontSize}px`,
159+
height: '100%',
160+
minHeight: '100px',
161+
boxSizing: 'border-box',
162+
},
163+
'.cm-scroller': {
164+
overflow: 'auto',
165+
fontFamily: '"SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
166+
fontWeight: '500',
167+
letterSpacing: '0.5px',
168+
lineHeight: '1.4',
169+
},
170+
'.cm-content': {
171+
textAlign: 'left',
172+
padding: '4px 0',
173+
fontVariantLigatures: 'none',
174+
},
175+
'.cm-line': {
176+
padding: '0 4px',
177+
},
178+
'.cm-gutterElement': {
179+
fontFamily: '"SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
180+
fontWeight: '500',
181+
},
182+
'.cm-changedLine': {
183+
backgroundColor: 'rgba(100, 255, 128, .12) !important',
184+
},
185+
};
186+
187+
const editorTheme = EditorView.theme({
188+
...baseThemeSpec,
189+
'&': {
190+
...baseThemeSpec['&'],
191+
border: `1px solid ${isInverted ? '#605e5c' : '#8a8886'}`,
192+
borderRadius: '2px',
193+
},
194+
'&.cm-focused': {
195+
outline: 'none',
196+
borderColor: '#0078d4',
197+
},
198+
'.cm-gutters': {
199+
borderRight: `1px solid ${isInverted ? '#3b3a39' : '#e1e1e1'}`,
200+
backgroundColor: isInverted ? '#252423' : '#f3f3f3',
201+
},
202+
});
203+
152204
const extensions = [
153205
history(),
154206
bracketMatching(),
@@ -173,43 +225,7 @@ export const CodeMirrorEditor = forwardRef<CodeMirrorEditorRef, CodeMirrorEditor
173225
onMouseDown,
174226
}),
175227
keybindingsCompartment.of(createKeybindingExtensions({ openTokenPicker, indentWithTab })),
176-
EditorView.theme({
177-
'&': {
178-
fontSize: `${fontSize}px`,
179-
height: '100%',
180-
minHeight: '100px',
181-
border: `1px solid ${isInverted ? '#605e5c' : '#8a8886'}`,
182-
borderRadius: '2px',
183-
boxSizing: 'border-box',
184-
},
185-
'&.cm-focused': {
186-
outline: 'none',
187-
borderColor: '#0078d4',
188-
},
189-
'.cm-scroller': {
190-
overflow: 'auto',
191-
fontFamily: '"SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
192-
fontWeight: '500',
193-
letterSpacing: '0.5px',
194-
lineHeight: '1.4',
195-
},
196-
'.cm-content': {
197-
textAlign: 'left',
198-
padding: '4px 0',
199-
fontVariantLigatures: 'none',
200-
},
201-
'.cm-line': {
202-
padding: '0 4px',
203-
},
204-
'.cm-gutterElement': {
205-
fontFamily: '"SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
206-
fontWeight: '500',
207-
},
208-
'.cm-gutters': {
209-
borderRight: `1px solid ${isInverted ? '#3b3a39' : '#e1e1e1'}`,
210-
backgroundColor: isInverted ? '#252423' : '#f3f3f3',
211-
},
212-
}),
228+
editorTheme,
213229
];
214230

215231
if (lineNumbers === 'on') {
@@ -224,6 +240,47 @@ export const CodeMirrorEditor = forwardRef<CodeMirrorEditorRef, CodeMirrorEditor
224240
extensions.push(EditorView.lineWrapping);
225241
}
226242

243+
if (showMerge) {
244+
const mergeView = new MergeView({
245+
a: {
246+
doc: originalValue ?? '',
247+
extensions: [
248+
EditorView.editable.of(false),
249+
EditorState.readOnly.of(true),
250+
themeCompartment.of(createFluentTheme(isInverted)),
251+
languageCompartment.of(getLanguageExtension(language)),
252+
EditorView.theme({
253+
...baseThemeSpec,
254+
'.cm-changedLine': {
255+
backgroundColor: 'rgba(255, 128, 100, .12) !important',
256+
},
257+
'.cm-content': {
258+
backgroundColor: isInverted ? '#252423' : '#f3f3f3',
259+
},
260+
}),
261+
...(lineNumbers === 'on' ? [lineNumbersExtension()] : []),
262+
],
263+
},
264+
b: {
265+
doc: value ?? defaultValue,
266+
extensions,
267+
},
268+
parent: containerRef.current,
269+
});
270+
271+
mergeViewRef.current = mergeView;
272+
viewRef.current = mergeView.b;
273+
onEditorRef?.(editorRef);
274+
onEditorLoaded?.();
275+
276+
return () => {
277+
mergeView.destroy();
278+
mergeViewRef.current = null;
279+
viewRef.current = null;
280+
isInitializedRef.current = false;
281+
};
282+
}
283+
227284
const state = EditorState.create({
228285
doc: value ?? defaultValue,
229286
extensions,
@@ -243,26 +300,53 @@ export const CodeMirrorEditor = forwardRef<CodeMirrorEditorRef, CodeMirrorEditor
243300
viewRef.current = null;
244301
isInitializedRef.current = false;
245302
};
246-
}, []); // Only run once on mount
303+
}, [showMerge, originalValue]); // Recreate editor when merge values change
247304

248305
// Update theme when inverted changes
249306
useEffect(() => {
307+
const newTheme = createFluentTheme(isInverted);
250308
if (viewRef.current) {
251309
viewRef.current.dispatch({
252-
effects: themeCompartment.reconfigure(createFluentTheme(isInverted)),
310+
effects: themeCompartment.reconfigure(newTheme),
311+
});
312+
}
313+
// Also update panel A in merge view
314+
if (mergeViewRef.current) {
315+
mergeViewRef.current.a.dispatch({
316+
effects: themeCompartment.reconfigure(newTheme),
253317
});
254318
}
255319
}, [isInverted]);
256320

257321
// Update language when it changes
258322
useEffect(() => {
323+
const newLang = getLanguageExtension(language);
259324
if (viewRef.current) {
260325
viewRef.current.dispatch({
261-
effects: languageCompartment.reconfigure(getLanguageExtension(language)),
326+
effects: languageCompartment.reconfigure(newLang),
327+
});
328+
}
329+
// Also update panel A in merge view
330+
if (mergeViewRef.current) {
331+
mergeViewRef.current.a.dispatch({
332+
effects: languageCompartment.reconfigure(newLang),
262333
});
263334
}
264335
}, [language]);
265336

337+
// Update originalValue in merge view panel A when it changes
338+
useEffect(() => {
339+
if (mergeViewRef.current && originalValue !== undefined) {
340+
const panelA = mergeViewRef.current.a;
341+
const currentValue = panelA.state.doc.toString();
342+
if (originalValue !== currentValue) {
343+
panelA.dispatch({
344+
changes: { from: 0, to: panelA.state.doc.length, insert: originalValue },
345+
});
346+
}
347+
}
348+
}, [originalValue]);
349+
266350
// Update readOnly when it changes
267351
useEffect(() => {
268352
if (viewRef.current) {
@@ -296,6 +380,7 @@ export const CodeMirrorEditor = forwardRef<CodeMirrorEditorRef, CodeMirrorEditor
296380
const containerStyle: React.CSSProperties = {
297381
height: height ?? '100%',
298382
width: width ?? '100%',
383+
overflow: 'auto',
299384
...monacoContainerStyle,
300385
};
301386

0 commit comments

Comments
 (0)