1111using EnvDTE ;
1212using EnvDTE80 ;
1313using Microsoft . VisualStudio ;
14+ using Microsoft . VisualStudio . ComponentModelHost ;
1415using Microsoft . VisualStudio . Editor ;
15- using Microsoft . VisualStudio . Package ;
1616using Microsoft . VisualStudio . Shell ;
1717using Microsoft . VisualStudio . Shell . Interop ;
1818using Microsoft . VisualStudio . Shell . TableManager ;
1919using Microsoft . VisualStudio . Shell . TableControl ;
20- using Microsoft . VisualStudio . Text . Editor ;
20+ using Microsoft . VisualStudio . TextManager . Interop ;
2121
2222namespace CodingWithCalvin . MCPServer . Services ;
2323
@@ -43,6 +43,73 @@ private static string NormalizePath(string path)
4343 return Path . GetFullPath ( path . Replace ( '/' , '\\ ' ) ) ;
4444 }
4545
46+ private static string DetectLineEnding ( string content )
47+ {
48+ if ( content . Contains ( "\r \n " ) ) return "\r \n " ;
49+ if ( content . Contains ( "\r " ) ) return "\r " ;
50+ if ( content . Contains ( "\n " ) ) return "\n " ;
51+ return Environment . NewLine ;
52+ }
53+
54+ private static string NormalizeToLineEnding ( string content , string lineEnding )
55+ {
56+ return content . Replace ( "\r \n " , "\n " ) . Replace ( "\r " , "\n " ) . Replace ( "\n " , lineEnding ) ;
57+ }
58+
59+ private string ? TryGetVsDocumentLineEnding ( string documentPath )
60+ {
61+ ThreadHelper . ThrowIfNotOnUIThread ( ) ;
62+
63+ try
64+ {
65+ var componentModel = ServiceProvider . GetService ( typeof ( SComponentModel ) ) as IComponentModel ;
66+ if ( componentModel == null ) return null ;
67+
68+ var editorAdapters = componentModel . GetService < IVsEditorAdaptersFactoryService > ( ) ;
69+ if ( editorAdapters == null ) return null ;
70+
71+ var rdt = ServiceProvider . GetService ( typeof ( SVsRunningDocumentTable ) ) as IVsRunningDocumentTable ;
72+ if ( rdt == null ) return null ;
73+
74+ rdt . FindAndLockDocument (
75+ ( uint ) _VSRDTFLAGS . RDT_NoLock ,
76+ NormalizePath ( documentPath ) ,
77+ out _ ,
78+ out _ ,
79+ out IntPtr punkDocData ,
80+ out _ ) ;
81+
82+ if ( punkDocData == IntPtr . Zero ) return null ;
83+
84+ try
85+ {
86+ var vsTextBuffer = Marshal . GetObjectForIUnknown ( punkDocData ) as IVsTextBuffer ;
87+ if ( vsTextBuffer == null ) return null ;
88+
89+ var textBuffer = editorAdapters . GetDataBuffer ( vsTextBuffer ) ;
90+ if ( textBuffer == null ) return null ;
91+
92+ var snapshot = textBuffer . CurrentSnapshot ;
93+ if ( snapshot . LineCount > 0 )
94+ {
95+ var lineBreak = snapshot . GetLineFromLineNumber ( 0 ) . GetLineBreakText ( ) ;
96+ if ( ! string . IsNullOrEmpty ( lineBreak ) )
97+ return lineBreak ;
98+ }
99+ }
100+ finally
101+ {
102+ Marshal . Release ( punkDocData ) ;
103+ }
104+ }
105+ catch
106+ {
107+ // ignored — fall back to content scan
108+ }
109+
110+ return null ;
111+ }
112+
46113 private static bool PathsEqual ( string path1 , string path2 )
47114 {
48115 return NormalizePath ( path1 ) . Equals ( NormalizePath ( path2 ) , StringComparison . OrdinalIgnoreCase ) ;
@@ -333,8 +400,12 @@ public async Task<bool> WriteDocumentAsync(string path, string content)
333400 if ( textDoc != null )
334401 {
335402 var editPoint = textDoc . StartPoint . CreateEditPoint ( ) ;
403+ var existingContent = editPoint . GetText ( textDoc . EndPoint ) ;
404+ var lineEnding = TryGetVsDocumentLineEnding ( doc . FullName ) ?? DetectLineEnding ( existingContent ) ;
405+ var normalizedContent = NormalizeToLineEnding ( content , lineEnding ) ;
406+ editPoint = textDoc . StartPoint . CreateEditPoint ( ) ;
336407 editPoint . Delete ( textDoc . EndPoint ) ;
337- editPoint . Insert ( content ) ;
408+ editPoint . Insert ( normalizedContent ) ;
338409 return true ;
339410 }
340411 }
@@ -423,7 +494,15 @@ public async Task<bool> InsertTextAsync(string text)
423494 return false ;
424495 }
425496
426- textDoc . Selection . Insert ( text ) ;
497+ var lineEnding = TryGetVsDocumentLineEnding ( doc . FullName ) ;
498+ if ( lineEnding == null )
499+ {
500+ var samplePoint = textDoc . StartPoint . CreateEditPoint ( ) ;
501+ var sample = samplePoint . GetLines ( 1 , Math . Min ( textDoc . EndPoint . Line + 1 , 3 ) ) ;
502+ lineEnding = DetectLineEnding ( sample ) ;
503+ }
504+
505+ textDoc . Selection . Insert ( NormalizeToLineEnding ( text , lineEnding ) ) ;
427506 return true ;
428507 }
429508
@@ -444,11 +523,17 @@ public async Task<int> ReplaceTextAsync(string oldText, string newText)
444523 return 0 ;
445524 }
446525
526+ var contentPoint = textDoc . StartPoint . CreateEditPoint ( ) ;
527+ var existingContent = contentPoint . GetText ( textDoc . EndPoint ) ;
528+ var lineEnding = TryGetVsDocumentLineEnding ( doc . FullName ) ?? DetectLineEnding ( existingContent ) ;
529+ var normalizedOldText = NormalizeToLineEnding ( oldText , lineEnding ) ;
530+ var normalizedNewText = NormalizeToLineEnding ( newText , lineEnding ) ;
531+
447532 var count = 0 ;
448533 var searchPoint = textDoc . StartPoint . CreateEditPoint ( ) ;
449534 EditPoint ? matchEnd = null ;
450535
451- while ( searchPoint . FindPattern ( oldText , ( int ) vsFindOptions . vsFindOptionsMatchCase , ref matchEnd ) )
536+ while ( searchPoint . FindPattern ( normalizedOldText , ( int ) vsFindOptions . vsFindOptionsMatchCase , ref matchEnd ) )
452537 {
453538 count ++ ;
454539 searchPoint = matchEnd ;
@@ -457,7 +542,7 @@ public async Task<int> ReplaceTextAsync(string oldText, string newText)
457542 if ( count > 0 )
458543 {
459544 TextRanges ? tags = null ;
460- textDoc . ReplacePattern ( oldText , newText , ( int ) vsFindOptions . vsFindOptionsMatchCase , ref tags ) ;
545+ textDoc . ReplacePattern ( normalizedOldText , normalizedNewText , ( int ) vsFindOptions . vsFindOptionsMatchCase , ref tags ) ;
461546 }
462547
463548 return count ;
0 commit comments