@@ -12,6 +12,7 @@ import { xml } from '@codemirror/lang-xml';
1212import { yaml } from '@codemirror/lang-yaml' ;
1313import { csharp } from '@codemirror/legacy-modes/mode/clike' ;
1414import { powerShell } from '@codemirror/legacy-modes/mode/powershell' ;
15+ import { MergeView } from '@codemirror/merge' ;
1516import { useTheme } from '@fluentui/react' ;
1617import { EditorLanguage } from '@microsoft/logic-apps-shared' ;
1718import { 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