diff --git a/CodeiumVS/CodeiumVS.csproj b/CodeiumVS/CodeiumVS.csproj index b89c00d..11466da 100644 --- a/CodeiumVS/CodeiumVS.csproj +++ b/CodeiumVS/CodeiumVS.csproj @@ -1,7 +1,8 @@ - + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(MSBuildExtensionsPath32)\..\Common7\IDE latest @@ -46,6 +47,11 @@ 4 + + + InlineDiffControl.xaml + + @@ -151,6 +157,9 @@ + + MSBuild:Compile + MSBuild:Compile diff --git a/CodeiumVS/Commands.cs b/CodeiumVS/Commands.cs index d2b12a3..45a2a0b 100644 --- a/CodeiumVS/Commands.cs +++ b/CodeiumVS/Commands.cs @@ -91,8 +91,19 @@ protected override void BeforeQueryStatus(EventArgs e) is_visible = Command.Visible = ThreadHelper.JoinableTaskFactory.Run(async delegate { is_function = false; - docView = await VS.Documents.GetActiveDocumentViewAsync(); - if (docView?.TextView == null) return false; + + // any interactions with the `IVsTextView` should be done on the main thread + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + try + { + docView = await VS.Documents.GetActiveDocumentViewAsync(); + if (docView?.TextView == null) return false; + }catch (Exception ex) + { + await CodeiumVSPackage.Instance.LogAsync($"BaseCommandContextMenu: Failed to get the active document view; Exception: {ex}"); + return false; + } languageInfo = Languages.Mapper.GetLanguage(docView); ITextSelection selection = docView.TextView.Selection; diff --git a/CodeiumVS/InlineDiff/InlineDiffAdornment.cs b/CodeiumVS/InlineDiff/InlineDiffAdornment.cs new file mode 100644 index 0000000..8829e4b --- /dev/null +++ b/CodeiumVS/InlineDiff/InlineDiffAdornment.cs @@ -0,0 +1,721 @@ +using CodeiumVS; +using CodeiumVS.Utilities; +using EnvDTE; +using EnvDTE80; +using Microsoft; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Imaging; +using Microsoft.VisualStudio.OLE.Interop; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Formatting; +using Microsoft.VisualStudio.Text.Projection; +using Microsoft.VisualStudio.Text.Tagging; +using Microsoft.VisualStudio.TextManager.Interop; +using Microsoft.VisualStudio.Utilities; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows; +using System.Windows.Controls; + +namespace CodeiumVs.InlineDiff; + +internal class InlineDiffAdornment : TextViewExtension, ILineTransformSource, IOleCommandTarget +{ + [Export(typeof(AdornmentLayerDefinition))] + [Name("CodeiumInlineDiffAdornment")] + [Order(After = "Text")] + private static readonly AdornmentLayerDefinition _codeiumInlineDiffAdornment; + + private static readonly LineTransform _defaultTransform = new(1.0); + private static MethodInfo? _fnSetInterceptsAggregateFocus = null; + + private readonly IAdornmentLayer _layer; + private readonly IVsTextView _vsHostView; + private readonly IOleCommandTarget _nextCommand; + + private InlineDiffView? _adornment = null; + + private ITrackingSpan? _leftTrackingSpan; + private ITrackingSpan? _rightTrackingSpan; + private ITrackingSpan? _trackingSpanExtended; + + private IProjectionBuffer? _leftProjectionBuffer; + private IProjectionBuffer? _rightProjectionBuffer; + + private IReadOnlyRegion? _leftReadOnlyRegion; + + private ITextBuffer? _rightSourceBuffer; + private IVsWindowFrame? _rightWindowFrame; + private ITextDocument? _rightTextDocument; + + private double _adornmentTop = 0; + private double _codeBlockHeight = 0; + + private LineTransform _lineTransform = _defaultTransform; + private ITagAggregator? _tagAggregator = null; + + public bool HasAdornment => _adornment != null; + public bool IsAdornmentFocused => HasAdornment && (_adornment.ActiveView != null) && _adornment.ActiveView.VisualElement.IsKeyboardFocused; + + public InlineDiffAdornment(IWpfTextView view) : base(view) + { + _vsHostView = _hostView.ToIVsTextView(); + _layer = _hostView.GetAdornmentLayer("CodeiumInlineDiffAdornment"); + + _hostView.Closed += HostView_OnClosed; + _hostView.LayoutChanged += HostView_OnLayoutChanged; + _hostView.ViewportLeftChanged += HostView_OnLayoutChanged; + _hostView.ViewportWidthChanged += HostView_OnLayoutChanged; + _hostView.ZoomLevelChanged += HostView_OnZoomLevelChanged; + _hostView.Caret.PositionChanged += HostView_OnCaretPositionChanged; + + _vsHostView.AddCommandFilter(this, out _nextCommand); + + // attempt to get the `AggregateFocusInterceptor.GetInterceptsAggregateFocus` + // method in the Microsoft.VisualStudio.Text.Internal.dll + if (_fnSetInterceptsAggregateFocus == null) + { + try + { + string name = "Microsoft.VisualStudio.Text.Internal"; + Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().SingleOrDefault(assembly => assembly.GetName().Name == name); + + Type type = assembly?.GetType("Microsoft.VisualStudio.Text.Editor.AggregateFocusInterceptor"); + _fnSetInterceptsAggregateFocus = type?.GetMethod("SetInterceptsAggregateFocus", BindingFlags.Static | BindingFlags.Public); + } + catch (Exception ex) + { + CodeiumVSPackage.Instance?.Log($"InlineDiffAdornment: Failed to get the SetInterceptsAggregateFocus method; Exception: {ex}"); + } + } + } + + /// + /// Create a difference view at a given position, any current diff will be disposed + /// + /// + /// The index on the text buffer that we want to replace + /// + /// + /// The length of the code that needs to be replaced + /// + /// + /// The replacement code + /// + public async Task CreateDiffAsync(int position, int length, string replacement) + { + await DisposeDiffAsync(); + + // for the OpenDocumentViaProject and IsPeekOnAdornment + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + Assumes.True(position > 0 && length > 0 && (position + length) <= _hostView.TextSnapshot.Length, + "InlineDiffAdornment.CreateDiff: Invalid position and length parameter" + ); + Assumes.True( + MefProvider.Instance.TextDocumentFactoryService.TryGetTextDocument(_hostView.TextDataModel.DocumentBuffer, out var textDocument), + "InlineDiffAdornment.CreateDiff: Could not get text document for the current host view" + ); + + // create a temporary file to store the diff + string rightFileName = Path.GetTempFileName() + Path.GetExtension(textDocument.FilePath); + try + { + // create the projection buffers, left projects onto host view, right projects onto a temp file + CreateLeftProjectionBuffer(position, length); + CreateRightProjectionBuffer(rightFileName, position, length, replacement); + + _adornment = new InlineDiffView(_hostView, _leftProjectionBuffer, _hostView.TextDataModel.DocumentBuffer, _rightProjectionBuffer, _rightSourceBuffer); + } + catch (Exception ex) + { + await CodeiumVSPackage.Instance?.LogAsync($"InlineDiffAdornment.CreateDiffAsync: Exception: {ex}"); + await DisposeDiffAsync(); + return; + } + + _adornment.VisualElement.GotFocus += Adornment_OnGotFocus; + _adornment.VisualElement.LostFocus += Adornment_OnLostFocus; + _adornment.VisualElement.SizeChanged += Adornment_OnSizeChanged; + _adornment.VisualElement.OnAccepted = Adornment_OnAccepted; + _adornment.VisualElement.OnRejected = Adornment_OnRejected; + + // set the scale factor for CrispImage, without this, it'll be blurry + _adornment.VisualElement.SetValue(CrispImage.ScaleFactorProperty, _hostView.ZoomLevel * 0.01); + + // close the peek view if it's open + if (!_hostView.Roles.Contains(PredefinedTextViewRoles.EmbeddedPeekTextView) && IsPeekOnAdornment()) + MefProvider.Instance.PeekBroker.DismissPeekSession(_hostView); + + // close any auto completion windows that's open + if (MefProvider.Instance.CompletionBroker.IsCompletionActive(_hostView)) + MefProvider.Instance.CompletionBroker.DismissAllSessions(_hostView); + + // the same for the async completions + if (MefProvider.Instance.AsyncCompletionBroker.IsCompletionActive(_hostView)) + MefProvider.Instance.AsyncCompletionBroker.GetSession(_hostView)?.Dismiss(); + + CalculateExtendedTrackingSpan(position, length); + CalculateCodeBlockHeight(); + RefreshLineTransform(); + UpdateAdornment(); + } + + public void CreateDiff(int position, int length, string replacement) + { + ThreadHelper.JoinableTaskFactory.RunAsync(async delegate + { + await CreateDiffAsync(position, length, replacement); + }).FireAndForget(true); + } + + /// + /// Dispose the current diff, reject any changes made + /// + public async Task DisposeDiffAsync() + { + _adornment?.Dispose(); + + if (_rightWindowFrame != null) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + _rightWindowFrame.CloseFrame((uint)__FRAMECLOSE.FRAMECLOSE_NoSave); + + File.Delete(_rightTextDocument.FilePath); + } + + RemoveLeftReadOnlyRegion(); + + _adornment = null; + + _leftTrackingSpan = null; + _rightTrackingSpan = null; + _trackingSpanExtended = null; + + _leftProjectionBuffer = null; + _rightProjectionBuffer = null; + + _rightSourceBuffer = null; + _rightWindowFrame = null; + _codeBlockHeight = 0; + + UpdateAdornment(); + RefreshLineTransform(); + } + + private void RemoveLeftReadOnlyRegion() + { + if (_leftReadOnlyRegion != null) + { + using IReadOnlyRegionEdit readOnlyRegionEdit = _hostView.TextBuffer.CreateReadOnlyRegionEdit(); + readOnlyRegionEdit.RemoveReadOnlyRegion(_leftReadOnlyRegion); + readOnlyRegionEdit.Apply(); + _leftReadOnlyRegion = null; + } + } + + /// + /// Dispose the current diff, reject any changes made. + /// + public void DisposeDiff() + { + ThreadHelper.JoinableTaskFactory.RunAsync(DisposeDiffAsync).FireAndForget(true); + } + + /// + /// Create a tracking span, tracking span will reposition itself + /// when the user did something that offset it position, like adding new line before it. + /// + /// + /// + /// + /// + private static ITrackingSpan CreateTrackingSpan(ITextSnapshot snapshot, int position, int length) + { + // don't use _hostView.TextViewLines.GetTextViewLineContainingBufferPosition here + // because if the line is not currently on the screen, it won't be in the TextViewLines + SnapshotPoint start = new(snapshot, position); + SnapshotPoint end = new(snapshot, position + length); + SnapshotSpan span = new(start.GetContainingLine().Start, end.GetContainingLine().End); + return snapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeInclusive); + } + + /// + /// Create a projection buffer for the snapshot. + /// + /// + /// + /// + /// + /// + private static IProjectionBuffer CreateProjectionBuffer(ITextSnapshot snapshot, int position, int length, out ITrackingSpan trackingSpan) + { + trackingSpan = CreateTrackingSpan(snapshot, position, length); + + List sourceSpans = [trackingSpan]; + var options = ProjectionBufferOptions.PermissiveEdgeInclusiveSourceSpans; + + return MefProvider.Instance.ProjectionBufferFactoryService.CreateProjectionBuffer(null, sourceSpans, options); + } + + /// + /// Create a projection buffer for the host view, mark the span as read only. + /// + /// + /// + private void CreateLeftProjectionBuffer(int position, int length) + { + _leftProjectionBuffer = CreateProjectionBuffer(_hostView.TextSnapshot, position, length, out _leftTrackingSpan); + + // make sure that the user cannot edit the left buffer while we're showing the diff + using IReadOnlyRegionEdit readOnlyRegionEdit = _hostView.TextBuffer.CreateReadOnlyRegionEdit(); + + _leftReadOnlyRegion = readOnlyRegionEdit.CreateReadOnlyRegion( + _leftTrackingSpan.GetSpan(_hostView.TextSnapshot), + SpanTrackingMode.EdgeInclusive, EdgeInsertionMode.Deny + ); + + readOnlyRegionEdit.Apply(); + } + + /// + /// Create a temporary projection buffer for the right side, + /// it will be disposed when the diff is disposed. + /// + /// + /// + /// + /// + private void CreateRightProjectionBuffer(string tempFileName, int position, int length, string replacement) + { + ThreadHelper.ThrowIfNotOnUIThread("CreateRightBuffer"); + + // copy the snapshot into the temporary file + using (StreamWriter writer = new(tempFileName, append: false, Encoding.UTF8)) + { + _hostView.TextSnapshot.Write(writer); + } + + // open the temporary file + int openingResult = MefProvider.Instance.DocumentOpeningService.OpenDocumentViaProject( + tempFileName, Guid.Empty, out var _, out var _, out var _, out _rightWindowFrame + ); + + Assumes.True(ErrorHandler.Succeeded(openingResult), + "InlineDiffAdornment.CreateRightProjectionBuffer: Could not open the document for temporary file" + ); + + VsShellUtilities.GetTextView(_rightWindowFrame).GetBuffer(out var sourceTextLines); + Assumes.True(sourceTextLines != null, + "InlineDiffAdornment.CreateRightProjectionBuffer: Could not get source text lines" + ); + + _rightSourceBuffer = MefProvider.Instance.EditorAdaptersFactoryService.GetDocumentBuffer(sourceTextLines); + + Assumes.True(_rightSourceBuffer != null, + "InlineDiffAdornment.CreateRightProjectionBuffer: Could not create source buffer" + ); + + Assumes.True( + MefProvider.Instance.TextDocumentFactoryService.TryGetTextDocument(_rightSourceBuffer, out _rightTextDocument), + "InlineDiffAdornment.CreateRightProjectionBuffer: Could not get text document for the temp file" + ); + + // apply the diff + using ITextEdit textEdit = _rightSourceBuffer.CreateEdit(); + textEdit.Replace(position, length, replacement); + ITextSnapshot snapshot = textEdit.Apply(); + + // tell visual studio this document has not changed, although it is + _rightTextDocument.UpdateDirtyState(false, DateTime.UtcNow); + + // create the right projection buffer that projects onto the temporary file + _rightProjectionBuffer = CreateProjectionBuffer(snapshot, position, replacement.Length, out _rightTrackingSpan); + } + + /// + /// Get the extended tracking span of the diff, meaning up and down one line from the actual diff. + /// + /// + /// + private void CalculateExtendedTrackingSpan(int position, int length) + { + ITextSnapshotLine startLine = _hostView.TextSnapshot.GetLineFromPosition(position); + ITextSnapshotLine endLine = _hostView.TextSnapshot.GetLineFromPosition(position + length); + + // move up one line, only if it's possible to do so + if (startLine.LineNumber > 0) + startLine = _hostView.TextSnapshot.GetLineFromLineNumber(startLine.LineNumber - 1); + + // move down one line, you know the deal + if (endLine.LineNumber <= _hostView.TextSnapshot.LineCount - 1) + endLine = _hostView.TextSnapshot.GetLineFromLineNumber(endLine.LineNumber + 1); + + _trackingSpanExtended = CreateTrackingSpan( + _hostView.TextSnapshot, startLine.Start.Position, endLine.End.Position - startLine.Start.Position + ); + } + + /// + /// Calculate the height, in pixel, of the left code block. + /// + private void CalculateCodeBlockHeight() + { + SnapshotPoint pointStart = _leftTrackingSpan.GetStartPoint(_hostView.TextSnapshot); + SnapshotPoint pointEnd = _leftTrackingSpan.GetEndPoint(_hostView.TextSnapshot); + ITextViewLine lineStart = _hostView.TextViewLines.GetTextViewLineContainingBufferPosition(pointStart); + ITextViewLine lineEnd = _hostView.TextViewLines.GetTextViewLineContainingBufferPosition(pointEnd); + + // the lines are out of view, so they're null + if (lineStart != null && lineEnd != null) + { + _codeBlockHeight = lineEnd.TextBottom - lineStart.TextTop; + return; + } + + // caculate the height from the line count + int lineNoStart = _hostView.TextSnapshot.GetLineNumberFromPosition(pointStart.Position); + int lineNoEnd = _hostView.TextSnapshot.GetLineNumberFromPosition(pointEnd.Position); + _codeBlockHeight = (lineNoEnd - lineNoStart) * _hostView.LineHeight; + } + + /// + /// Refresh the adornment when the host view changes its layout. + /// + /// + /// + private void HostView_OnLayoutChanged(object sender, EventArgs e) + { + if (!HasAdornment) return; + + CalculateCodeBlockHeight(); + UpdateAdornment(); + } + + /// + /// Set the scale factor for CrispImage, without this, it'll be blurry. + /// + /// + /// + private void HostView_OnZoomLevelChanged(object sender, ZoomLevelChangedEventArgs e) + { + if (!HasAdornment) return; + + _adornment.VisualElement.SetValue(CrispImage.ScaleFactorProperty, e.NewZoomLevel * 0.01); + CalculateCodeBlockHeight(); + UpdateAdornment(); + } + + /// + /// Attemp to make a smooth caret transistion into the diff view. + /// + /// + /// + private void HostView_OnCaretPositionChanged(object sender, CaretPositionChangedEventArgs e) + { + if (!HasAdornment || IsAdornmentFocused || (_adornment.ActiveView == null)) return; + + SnapshotSpan span = _leftTrackingSpan.GetSpan(_hostView.TextSnapshot); + int position = e.NewPosition.BufferPosition.Position; + + if (position >= span.Start.Position && position <= span.End.Position) + { + try + { + SnapshotPoint point = new(_adornment.ActiveView.TextSnapshot, position - span.Start.Position); + _adornment.ActiveView.Caret.MoveTo(point); + _adornment.ActiveVsView?.SendExplicitFocus(); + } + catch (ArgumentOutOfRangeException) { } + } + } + + private void HostView_OnClosed(object sender, EventArgs e) + { + DisposeDiff(); + _hostView.Closed -= HostView_OnClosed; + _hostView.LayoutChanged -= HostView_OnLayoutChanged; + _hostView.ViewportLeftChanged -= HostView_OnLayoutChanged; + _hostView.ViewportWidthChanged -= HostView_OnLayoutChanged; + _hostView.ZoomLevelChanged -= HostView_OnZoomLevelChanged; + _hostView.Caret.PositionChanged -= HostView_OnCaretPositionChanged; + } + + /// + /// The proposed diff has been accepted by the user. + /// + private void Adornment_OnAccepted() + { + ThreadHelper.ThrowIfNotOnUIThread("Adornment_OnAccepted"); + + RemoveLeftReadOnlyRegion(); + + // apply the replacement + using (ITextEdit textEdit = _leftProjectionBuffer.CreateEdit()) + { + textEdit.Replace(0, _leftProjectionBuffer.CurrentSnapshot.Length, _rightProjectionBuffer.CurrentSnapshot.GetText()); + textEdit.Apply(); + } + + // focus and select the new replacement + _hostView.VisualElement.Focus(); + _hostView.Selection.Select(_leftTrackingSpan.GetSpan(_hostView.TextSnapshot), false); + + // format it + try + { + DTE2 dte = (DTE2)ServiceProvider.GlobalProvider.GetService(typeof(DTE)); + dte.ExecuteCommand("Edit.FormatSelection"); + + // this doesn't work + //var guid = VSConstants.CMDSETID.StandardCommandSet2K_guid; + //Exec(ref guid, (uint)VSConstants.VSStd2KCmdID.FORMATSELECTION, 0, IntPtr.Zero, IntPtr.Zero); + } + catch (Exception) { } // COMException + + DisposeDiff(); + } + + /// + /// The proposed diff has been rejected by the user. + /// + private void Adornment_OnRejected() + { + DisposeDiff(); + } + + private void Adornment_OnSizeChanged(object sender, SizeChangedEventArgs e) + { + _lineTransform = new LineTransform(0.0, e.NewSize.Height - _codeBlockHeight, 1.0); + RefreshLineTransform(); + } + + private void Adornment_OnLostFocus(object sender, RoutedEventArgs e) + { + SetHostViewInterceptsAggregateFocus(false); + } + + private void Adornment_OnGotFocus(object sender, RoutedEventArgs e) + { + SetHostViewInterceptsAggregateFocus(true); + _adornment.ActiveVsView?.SendExplicitFocus(); + } + + /// + /// Intercept the focus of the host view. + /// + /// + private void SetHostViewInterceptsAggregateFocus(bool intercept) + { + if (_fnSetInterceptsAggregateFocus == null) return; + try + { + _fnSetInterceptsAggregateFocus.Invoke(null, [_hostView as DependencyObject, intercept]); + } + catch (Exception ex) + { + CodeiumVSPackage.Instance?.Log($"InlineDiffAdornment: SetHostViewInterceptsAggregateFocus({intercept}) failed; Exception: {ex}"); + } + } + + + /// + /// Update the position and width of the adornment then show it on the screen. + /// + private void UpdateAdornment() + { + if (!HasAdornment) + { + _layer.RemoveAllAdornments(); + return; + } + + SnapshotPoint point = _leftTrackingSpan.GetStartPoint(_hostView.TextSnapshot); + IWpfTextViewLine containningLine = _hostView.TextViewLines.GetTextViewLineContainingBufferPosition(point); + if (containningLine != null) + _adornmentTop = containningLine.TextTop; + + // set the buttons position to be on top of the diff view if it's visible + bool btnsShouldOnTop = _adornmentTop > _hostView.ViewportTop; + if (_adornment.VisualElement.AreButtonsOnTop != btnsShouldOnTop) + { + _adornment.VisualElement.AreButtonsOnTop = btnsShouldOnTop; + + // make sure it updates + _hostView.QueuePostLayoutAction(RefreshLineTransform); + } + + //double glyphWidth = _hostView.FormattedLineSource.ColumnWidth; + Canvas.SetLeft(_adornment.VisualElement, 0); + Canvas.SetTop(_adornment.VisualElement, _adornmentTop - _adornment.VisualElement.TopOffset); + + // `_hostView.ViewportLeft` is the horizontal scroll position + _adornment.VisualElement.Width = _hostView.ViewportLeft + _hostView.ViewportWidth; + + // Note that if we remove the adornments before calling `GetTextViewLineContainingBufferPosition` + // it will return null, because the text view line got removed from TextViewLines + + // I don't even know why we have to remove the adornment before adding it again + // no documentation mentioned ANYTHING about this. All code snippets and even + // the official overview do this. But here's the thing: it works just fine even + // if we removed these two lines. Why, microsoft, why? + _layer.RemoveAllAdornments(); + _layer.AddAdornment(AdornmentPositioningBehavior.OwnerControlled, null, null, _adornment.VisualElement, null); + } + + /// + /// Make the UI update the new line transform. + /// + private void RefreshLineTransform() + { + // this triggers the GetLineTransform so it refreshes, we had to use some magic once again + // `(ViewRelativePosition)4` is some undocumented enum i found on an internal source + // that doesn't actually "display" the position, but only refreshes stuffs + _hostView.DisplayTextLineContainingBufferPosition(default, 0.0, (ViewRelativePosition)4); + } + + LineTransform ILineTransformSource.GetLineTransform(ITextViewLine line, double yPosition, ViewRelativePosition placement) + { + if (_leftTrackingSpan != null) + { + SnapshotPoint point = _leftTrackingSpan.GetEndPoint(_hostView.TextSnapshot); + + if (point.Position >= line.Extent.Start && point.Position <= line.Extent.End) + { + return new LineTransform(0.0, _adornment.VisualElement.ActualHeight - _adornment.VisualElement.TopOffset - _codeBlockHeight, 1.0); + } + else if (_adornment.VisualElement.TopOffset > 0) + { + point = _leftTrackingSpan.GetStartPoint(_hostView.TextSnapshot); + + if (point.Position >= line.Extent.Start && point.Position <= line.Extent.End) + { + return new LineTransform(_adornment.VisualElement.TopOffset, 0.0, 1.0); + } + } + } + return _defaultTransform; + } + + /// + /// Whether or not there is a "peek definition" window intersect with the diff view. + /// + /// + private bool IsPeekOnAdornment() + { + ThreadHelper.ThrowIfNotOnUIThread("IsPeekOnAnchor"); + _tagAggregator ??= IntellisenseUtilities.GetTagAggregator(_hostView); + + SnapshotSpan extent = _leftTrackingSpan.GetSpan(_hostView.TextSnapshot); + + foreach (IMappingTagSpan tag in _tagAggregator.GetTags(extent)) + { + if (!tag.Tag.IsAboveLine && tag.Span.GetSpans(_hostView.TextSnapshot).IntersectsWith(extent)) + { + return true; + } + } + return false; + } + +#pragma warning disable VSTHRD010 // Invoke single-threaded types on Main thread + + // By default, the adornments doesn't received the keyboard inputs it deserved, sadly. + // We have to "hook" the host view commands filter list and check if our adornments + // are focused, and if so, we pass the command to them. + // + // I had spent an embarrassing amount of time to come up with this solution. + // + // A good reference: https://joshvarty.com/2014/08/01/ripping-the-visual-studio-editor-apart-with-projection-buffers/ + + public int QueryStatus(ref Guid pguidCmdGroup, uint cCmds, OLECMD[] prgCmds, IntPtr pCmdText) + { + IOleCommandTarget _commandTarget = IsAdornmentFocused ? + _adornment.ActiveVsView as IOleCommandTarget : _nextCommand; + + return _commandTarget.QueryStatus(ref pguidCmdGroup, cCmds, prgCmds, pCmdText); + } + + public int Exec(ref Guid pguidCmdGroup, uint nCmdID, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) + { + IOleCommandTarget _commandTarget = IsAdornmentFocused ? + _adornment.ActiveVsView as IOleCommandTarget : _nextCommand; + + // handle caret transistion from the diff view to host view + if (pguidCmdGroup == VSConstants.CMDSETID.StandardCommandSet2K_guid && IsAdornmentFocused) + { + var view = _adornment.ActiveView; + int position = view.Caret.Position.BufferPosition.Position; + int lineNo = view.TextSnapshot.GetLineNumberFromPosition(position); + + bool isUp = (nCmdID == (uint)VSConstants.VSStd2KCmdID.UP); + bool isDown = (nCmdID == (uint)VSConstants.VSStd2KCmdID.DOWN); + bool isRight = (nCmdID == (uint)VSConstants.VSStd2KCmdID.RIGHT); + bool isLeft = (nCmdID == (uint)VSConstants.VSStd2KCmdID.LEFT); + + SnapshotPoint? point = null; + + if (isUp && lineNo == 0) + { + point = _trackingSpanExtended.GetStartPoint(_hostView.TextSnapshot); + } + else if (isDown && lineNo == view.TextSnapshot.LineCount - 1) + { + point = _trackingSpanExtended.GetEndPoint(_hostView.TextSnapshot); + } + else if (isLeft && position == 0) + { + point = _trackingSpanExtended.GetStartPoint(_hostView.TextSnapshot); + var viewLine = _hostView.TextViewLines.GetTextViewLineContainingBufferPosition(point.Value); + point = viewLine.End; + } + else if (isRight && position == view.TextSnapshot.Length - 1) + { + point = _trackingSpanExtended.GetEndPoint(_hostView.TextSnapshot); + var viewLine = _hostView.TextViewLines.GetTextViewLineContainingBufferPosition(point.Value); + point = viewLine.Start; + } + + if (point.HasValue) + { + if ((isUp || isDown)) + { + ITextViewLine viewLine = _hostView.TextViewLines.GetTextViewLineContainingBufferPosition(point.Value); + if (viewLine != null) _hostView.Caret.MoveTo(viewLine); + } + else + { + _hostView.Caret.MoveTo(point.Value); + } + + _hostView.Caret.EnsureVisible(); + _hostView.VisualElement.Focus(); + } + } + + return _commandTarget.Exec(ref pguidCmdGroup, nCmdID, nCmdexecopt, pvaIn, pvaOut); + } +#pragma warning restore VSTHRD010 // Invoke single-threaded types on Main thread +} + +[Export(typeof(ILineTransformSourceProvider))] +[Name("CodeiumInlineDiffViewProvider")] +[ContentType("code")] +[TextViewRole(PredefinedTextViewRoles.Document)] +internal class CodeiumInlineDiffViewProvider : ILineTransformSourceProvider +{ + public ILineTransformSource Create(IWpfTextView textView) + { + // we don't want to create a nested diff view, don't we? + if (textView.Roles.Contains(InlineDiffView.Role)) return null; + + return InlineDiffAdornment.GetOrCreate(textView, () => new InlineDiffAdornment(textView)); + } +} diff --git a/CodeiumVS/InlineDiff/InlineDiffControl.xaml b/CodeiumVS/InlineDiff/InlineDiffControl.xaml new file mode 100644 index 0000000..0351afa --- /dev/null +++ b/CodeiumVS/InlineDiff/InlineDiffControl.xaml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CodeiumVS/InlineDiff/InlineDiffControl.xaml.cs b/CodeiumVS/InlineDiff/InlineDiffControl.xaml.cs new file mode 100644 index 0000000..ff95e66 --- /dev/null +++ b/CodeiumVS/InlineDiff/InlineDiffControl.xaml.cs @@ -0,0 +1,74 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; + +namespace CodeiumVs.InlineDiff; + +public partial class InlineDiffControl : UserControl +{ + private bool _areButtonsOnTop = true; + + public Action? OnRejected; + public Action? OnAccepted; + internal readonly InlineDiffView _inlineDiffView; + + public double ButtonsGridHeight => ButtonsGrid.ActualHeight; + public double TopOffset => _areButtonsOnTop ? ButtonsGridHeight : 0; + + public bool AreButtonsOnTop + { + get => _areButtonsOnTop; + set + { + if (_areButtonsOnTop == value) return; + + MainStackPanel.Children.Clear(); + + if (_areButtonsOnTop = value) + { + MainStackPanel.Children.Add(ButtonsGrid); + MainStackPanel.Children.Add(DiffContent); + } + else + { + MainStackPanel.Children.Add(DiffContent); + MainStackPanel.Children.Add(ButtonsGrid); + } + } + } + + internal InlineDiffControl(InlineDiffView inlineDiffView) + { + InitializeComponent(); + _inlineDiffView = inlineDiffView; + + DiffContent.Children.Insert(0, _inlineDiffView.Viewer.VisualElement); + _inlineDiffView.LeftView.ViewportWidthChanged += LeftView_ViewportWidthChanged; + } + + public void SetContentBorderLeftMargin(double pixels) + { + ContentBorder.Margin = new Thickness(pixels, 0, 0, 0); + } + + private void LeftView_ViewportWidthChanged(object sender, EventArgs e) + { + ButtonColumn1.Width = new GridLength(ContentBorder.Margin.Left + _inlineDiffView.LeftView.ViewportWidth); + } + + private void ButtonReject_Click(object sender, RoutedEventArgs e) + { + OnRejected?.Invoke(); + } + + private void ButtonAccept_Click(object sender, RoutedEventArgs e) + { + OnAccepted?.Invoke(); + } + + private void UserControl_PreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Escape) + OnRejected?.Invoke(); + } +} \ No newline at end of file diff --git a/CodeiumVS/InlineDiff/InlineDiffView.cs b/CodeiumVS/InlineDiff/InlineDiffView.cs new file mode 100644 index 0000000..a543c20 --- /dev/null +++ b/CodeiumVS/InlineDiff/InlineDiffView.cs @@ -0,0 +1,651 @@ +using CodeiumVS.Utilities; +using Microsoft; +using Microsoft.VisualStudio; +using Microsoft.VisualStudio.Editor; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Differencing; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Projection; +using Microsoft.VisualStudio.TextManager.Interop; +using Microsoft.VisualStudio.Utilities; +using System.Collections.Generic; +using System.Linq; +using System.Windows; + +namespace CodeiumVs.InlineDiff; + +internal class InlineDiffView +{ + private class InlineDiffTextDataModel : ITextDataModel, IDisposable + { + public ITextBuffer DocumentBuffer { get; } + public ITextBuffer DataBuffer { get; } + public IContentType ContentType => DocumentBuffer.ContentType; + + public event EventHandler? ContentTypeChanged; + + // `dataBuffer` and `documentBuffer` can be the same + internal InlineDiffTextDataModel(ITextBuffer dataBuffer, ITextBuffer documentBuffer) + { + DataBuffer = Requires.NotNull(dataBuffer, "dataBuffer"); + DocumentBuffer = Requires.NotNull(documentBuffer, "documentBuffer"); + DocumentBuffer.ContentTypeChanged += OnContentTypeChanged; + } + + private void OnContentTypeChanged(object sender, ContentTypeChangedEventArgs e) + { + ContentTypeChanged?.Invoke(this, new TextDataModelContentTypeChangedEventArgs(e.BeforeContentType, e.AfterContentType)); + } + + public void Dispose() + { + DocumentBuffer.ContentTypeChanged -= OnContentTypeChanged; + } + } + + private readonly IWpfTextView _hostView; + private readonly IWpfDifferenceViewer _viewer; + private readonly IDifferenceBuffer _diffBuffer; + + private readonly InlineDiffTextDataModel _leftDataModel; + private readonly InlineDiffTextDataModel _rightDataModel; + + // Default roles for the diff views + private static readonly IEnumerable _defaultRoles = new string[] { + PredefinedTextViewRoles.PrimaryDocument, + PredefinedTextViewRoles.Analyzable, + Role + }; + + // options for the difference buffer + private static readonly StringDifferenceOptions _diffBufferOptions = new() + { + DifferenceType = StringDifferenceTypes.Line | StringDifferenceTypes.Word, + IgnoreTrimWhiteSpace = true + }; + + public const string Role = "CODEIUM_INLINE_DIFFERENCE_VIEW"; + + public IWpfDifferenceViewer? Viewer => _viewer; + + public IWpfTextView? LeftView => _viewer?.LeftView; + public IWpfTextView? RightView => _viewer?.RightView; + + public IWpfTextViewHost? LeftHost => _viewer?.LeftHost; + public IWpfTextViewHost? RightHost => _viewer?.RightHost; + + public IVsTextView? LeftVsView { get; private set; } + public IVsTextView? RightVsView { get; private set; } + + public IVsTextLines LeftTextLines { get; private set; } + public IVsTextLines RightTextLines { get; private set; } + + public IWpfTextView? ActiveView { get => _viewer?.ActiveViewType == DifferenceViewType.LeftView ? LeftView : RightView; } + public IVsTextView? ActiveVsView { get => _viewer?.ActiveViewType == DifferenceViewType.LeftView ? LeftVsView : RightVsView; } + + public readonly InlineDiffControl VisualElement; + + /// + /// Create an inline difference view for the given host view. + /// + /// + /// The host view that will be used to display the difference + /// + /// + /// The buffer that projects onto the 's text buffer + /// + /// + /// The text buffer of the + /// + /// + /// The buffer that projects onto the + /// + /// + /// The text buffer for the proposed replacement + /// + public InlineDiffView(IWpfTextView hostView, IProjectionBuffer leftProjection, ITextBuffer leftBuffer, IProjectionBuffer rightProjection, ITextBuffer rightBuffer) + { + ThreadHelper.ThrowIfNotOnUIThread("InlineDiffView"); + _hostView = hostView; + + // create data model and text lines + _leftDataModel = new(leftProjection, leftBuffer); + _rightDataModel = new(rightProjection, rightBuffer); + + // disable undo on the left view + //if (ErrorHandler.Succeeded(_leftTextLines.GetUndoManager(out var ppUndoManager))) + //{ + // ppUndoManager?.Enable(0); + //} + + // disable editting for the left view, enable for the right view + _diffBuffer = MefProvider.Instance.DifferenceBufferFactory.CreateDifferenceBuffer( + _leftDataModel, _rightDataModel, _diffBufferOptions, false, false, false, true + ); + + // create the difference viewer, and intialize it manually + _viewer = MefProvider.Instance.DifferenceViewerFactory.CreateUninitializedDifferenceView(); + _viewer.Initialize(_diffBuffer, CreateTextViewHostCallback); + + // this make it doesn't auto scroll to the first difference + _viewer.Options.SetOptionValue(DifferenceViewerOptions.ScrollToFirstDiffId, value: false); + + VisualElement = new InlineDiffControl(this); + + _diffBuffer.SnapshotDifferenceChanged += DiffBuffer_SnapshotDifferenceChanged; + _viewer.Closed += DifferenceViewer_OnClosed; + } + + /// + /// Dispose and release all resources + /// + public void Dispose() + { + ThreadHelper.ThrowIfNotOnUIThread("Dispose"); + _viewer?.Close(); + LeftVsView?.CloseView(); + RightVsView?.CloseView(); + + if (_diffBuffer != null) + _diffBuffer.SnapshotDifferenceChanged -= DiffBuffer_SnapshotDifferenceChanged; + + // We should not close this as this belongs to the original file + //if (LeftTextLines is IVsPersistDocData vsPersistDocData) + // vsPersistDocData.Close(); + + if (RightTextLines is IVsPersistDocData vsPersistDocData2) + vsPersistDocData2.Close(); + + _leftDataModel?.Dispose(); + _rightDataModel?.Dispose(); + } + + /// + /// Callback to manually intialize the views for IWpfDifferenceViewer + /// + /// + /// + /// + /// + /// + /// + private void CreateTextViewHostCallback(IDifferenceTextViewModel textViewModel, ITextViewRoleSet roles, IEditorOptions options, out FrameworkElement visualElement, out IWpfTextViewHost textViewHost) + { + // create the VS text view + IVsTextView vsTextView = MefProvider.Instance.EditorAdaptersFactoryService.CreateVsTextViewAdapter(MefProvider.Instance.OleServiceProvider); + + // should not happen + if (vsTextView is not IVsUserData codeWindowData) + throw new InvalidOperationException("Creating DifferenceViewerWithAdapters failed: Unable to cast IVsTextView to IVsUserData."); + + // set the roles and text view model for it + SetRolesAndModel(codeWindowData, textViewModel, roles); + + // manually set the default properties for the text view + if (vsTextView is IVsTextEditorPropertyCategoryContainer vsTextEditorProps) + { + Guid rguidCategory = DefGuidList.guidEditPropCategoryViewMasterSettings; + if (ErrorHandler.Succeeded(vsTextEditorProps.GetPropertyCategory(ref rguidCategory, out var ppProp))) + { + ppProp.SetProperty(VSEDITPROPID.VSEDITPROPID_ViewComposite_AllCodeWindowDefaults, true); + } + } + + IVsTextLines vsTextLines = (IVsTextLines) + MefProvider.Instance.EditorAdaptersFactoryService.GetBufferAdapter(textViewModel.DataModel.DocumentBuffer); + + Assumes.NotNull(vsTextLines); + + // initialize the vs text view + INITVIEW initOptions = new() + { + fSelectionMargin = 0u, + fWidgetMargin = 0u, + fVirtualSpace = 0u, + fDragDropMove = 1u + }; + + uint initFlags = (uint)TextViewInitFlags3.VIF_NO_HWND_SUPPORT | (uint)TextViewInitFlags.VIF_HSCROLL; + vsTextView.Initialize(vsTextLines, IntPtr.Zero, initFlags, [initOptions]); + + // get the text view host of the vs text view + textViewHost = MefProvider.Instance.EditorAdaptersFactoryService.GetWpfTextViewHost(vsTextView); + visualElement = textViewHost.HostControl; + + IWpfTextView textView = textViewHost.TextView; + InitializeView(textView, textViewHost); + + // disable line number, only for the left view + if (textViewModel.ViewType == DifferenceViewType.LeftView) + { + LeftVsView = vsTextView; + LeftTextLines = vsTextLines; + textView.Options.SetOptionValue(DefaultTextViewHostOptions.LineNumberMarginId, false); + textView.Options.SetOptionValue(DefaultTextViewHostOptions.SuggestionMarginId, false); + + textView.VisualElement.GotFocus += LeftView_OnGotFocus; + textView.VisualElement.LostFocus += LeftView_OnLostFocus; + textView.Caret.PositionChanged += LeftView_OnCaretPositionChanged; + textView.Closed += LeftView_OnClosed; + } + else if (textViewModel.ViewType == DifferenceViewType.RightView) + { + RightVsView = vsTextView; + RightTextLines = vsTextLines; + textView.VisualElement.GotFocus += RightView_OnGotFocus; + textView.VisualElement.LostFocus += RightView_OnLostFocus; + textView.Caret.PositionChanged += RightView_OnCaretPositionChanged; + textView.Closed += RightView_OnClosed; + } + else + { + throw new InvalidOperationException("Unknow difference viewer mode"); + } + + textView.Closed += DiffView_OnClosed; + textView.LayoutChanged += DiffView_OnLayoutChanged; + textView.ViewportHeightChanged += DiffView_OnViewportHeightChanged; + textView.VisualElement.PreviewMouseWheel += DiffView_OnPreviewMouseWheel; + } + + /// + /// Set the role and text view model for the code window created by . + /// + /// + /// + /// + private void SetRolesAndModel(IVsUserData codeWindowData, IDifferenceTextViewModel textViewModel, ITextViewRoleSet diffRoles) + { + // set the text view model for this code window + // unfortunately, we had to use magic string yet again + Guid riidKey = new("756E1D18-1976-40BE-AA45-916B02F7B809"); ; + codeWindowData.SetData(ref riidKey, textViewModel); + + // create the default roles and add roles from the diffview + IEnumerable enumerable = _defaultRoles.Concat(MefProvider.Instance.TextEditorFactoryService.DefaultRoles); + if (diffRoles != null) + enumerable = enumerable.Concat(MefProvider.Instance.TextEditorFactoryService.CreateTextViewRoleSet(diffRoles)); + + // set the roles for this code window + string vtData = MefProvider.Instance.TextEditorFactoryService.CreateTextViewRoleSet(enumerable).ToString(); + riidKey = VSConstants.VsTextBufferUserDataGuid.VsTextViewRoles_guid; + codeWindowData.SetData(ref riidKey, vtData); + } + + /// + /// Set the options for the text views that is hosted by + /// + /// + /// + private void InitializeView(IWpfTextView view, IWpfTextViewHost host) + { + // some references: + // - https://github.com/dotnet/roslyn/blob/376b78a73ab5c612ea23abca3cd6efd044935d0e/src/EditorFeatures/Core.Wpf/Preview/PreviewFactoryService.cs#L68 + // - https://github.com/microsoft/PTVS/blob/b72355d62889900e963f5a70a99c5ffc9fe8e50d/Python/Product/PythonTools/PythonTools/Intellisense/PreviewChangesService.cs#L92 + + // the zoom level is already managed by the host view, so this is ok + view.ZoomLevel = 100; + + // disable scrollbars + //view.Options.SetOptionValue(DefaultTextViewHostOptions.HorizontalScrollBarId , false); + view.Options.SetOptionValue(DefaultTextViewHostOptions.VerticalScrollBarId , false); + + // enable caret rendering + view.Options.SetOptionValue(DefaultTextViewOptions.ShouldCaretsBeRenderedId , true); + + // disable all the unwanted margins + view.Options.SetOptionValue(DefaultTextViewHostOptions.GlyphMarginId , false); + view.Options.SetOptionValue(DefaultTextViewHostOptions.SelectionMarginId , false); + //view.Options.SetOptionValue(DefaultTextViewHostOptions.LineEndingMarginOptionId , false); + + // enable this will alow ctrl+c and ctrl+x on blank lines + view.Options.SetOptionValue(DefaultTextViewOptions.CutOrCopyBlankLineIfNoSelectionId, true); + + // show url in the code + view.Options.SetOptionValue(DefaultTextViewOptions.DisplayUrlsAsHyperlinksId , true); + + // enable drag and drop selected code + view.Options.SetOptionValue(DefaultTextViewOptions.DragDropEditingId , true); + + // when ctrl+a, the caret will move to the end + view.Options.SetOptionValue(DefaultTextViewOptions.ShouldMoveCaretToEndOnSelectAllId, true); + + // common stuffs + view.Options.SetOptionValue(DefaultTextViewOptions.ShouldSelectionsBeRenderedId , true); + view.Options.SetOptionValue(DefaultTextViewOptions.ShowBlockStructureId , true); + view.Options.SetOptionValue(DefaultTextViewOptions.ShowErrorSquigglesId , true); + + // disable read-only + view.Options.SetOptionValue(DefaultTextViewOptions.ViewProhibitUserInputId , false); + + // not sure what this is + view.Options.SetOptionValue(DefaultTextViewOptions.IsViewportLeftClippedId , false); + + // disable zooming by ctrl+mouse_wheel + view.Options.SetOptionValue(DefaultWpfViewOptions.EnableMouseWheelZoomId , false); + + // these don't work, i have no idea why + view.Options.SetOptionValue(DefaultWpfViewOptions.ClickGoToDefEnabledId , true); + view.Options.SetOptionValue(DefaultWpfViewOptions.ClickGoToDefOpensPeekId , false); + + + // disable the controls in the horizontal scroll bar area (the PredefinedMarginNames.Bottom margin) + view.Options.SetOptionValue(DefaultTextViewHostOptions.EnableFileHealthIndicatorOptionId , false); + view.Options.SetOptionValue(DefaultTextViewHostOptions.EditingStateMarginOptionId , false); + view.Options.SetOptionValue(DefaultTextViewHostOptions.IndentationCharacterMarginOptionId, false); + view.Options.SetOptionValue(DefaultTextViewHostOptions.ZoomControlId , false); + + // How to find these: + // - Open the XAML Live Preview in VS + // - Toggle the "Show element info..." on top left + // - Hover over any elements and see its class + + // Dll: Microsoft.VisualStudio.Platform.VSEditor + // Namespace: Microsoft.VisualStudio.Text.Differencing.Implementation + // String: DifferenceOverviewMargin.MarginName + //host.GetTextViewMargin("deltadifferenceViewerOverview").VisualElement.Visibility = Visibility.Collapsed; + + // Dll: Microsoft.VisualStudio.Platform.VSEditor + // Namespace: Microsoft.VisualStudio.Text.Differencing.Implementation + // String: DifferenceAdornmentMargin.MarginName + // Desc: Show the delta difference, i.e. the '-' or '+' + host.GetTextViewMargin("deltaDifferenceAdornmentMargin").VisualElement.Visibility = Visibility.Collapsed; + + // intellicode Microsoft.VisualStudio.IntelliCode.WholeLineCompletion.UI.LineCompletionMenuEditorMargin.MarginName + //host.GetTextViewMargin("LineCompletionMenuEditorMargin").VisualElement.Visibility = Visibility.Collapsed; + //host.GetTextViewMargin(PredefinedMarginNames.LineEndingMargin).VisualElement.Visibility = Visibility.Collapsed; + //host.GetTextViewMargin(PredefinedMarginNames.Bottom).VisualElement.Visibility = Visibility.Collapsed; + + // enable focus for the diff viewer + view.VisualElement.Focusable = true; + + // leave here for future debugging + + //var edges = host.GetType().GetField("_edges", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(host) as List; + //foreach (var edge in edges) + //{ + // Type edgeType = edges.GetType(); + // Debug.WriteLine("edge: " + edgeType.Name); + //} + //foreach (EditorOptionDefinition i in view.Options.SupportedOptions) + //{ + // Debug.WriteLine(i.Name); + //} + } + + /// + /// Get the max height of both text views, not including its adornments like "peek definition" window and the horizontal scroll bar. + /// + /// + public double ContentHeight() + { + ITextView textView = _viewer.RightView; + ITextView textView2 = _viewer.LeftView; + Difference difference = _viewer.DifferenceBuffer.CurrentSnapshotDifference?.LineDifferences.Differences.FirstOrDefault(); + if (difference != null && difference.Before == null && difference.Right.Length == 0) + { + textView = _viewer.LeftView; + textView2 = _viewer.RightView; + } + textView.DisplayTextLineContainingBufferPosition(new SnapshotPoint(textView.TextSnapshot, 0), 0.0, ViewRelativePosition.Top, 10000.0, 10000.0); + + double leftHeight = textView.TextViewLines[textView.TextViewLines.Count - 1].Bottom - textView.ViewportTop; + double rightHeight = textView2.TextViewLines[textView2.TextViewLines.Count - 1].Bottom - textView2.ViewportTop; + + return Math.Max(leftHeight, rightHeight); + } + + /// + /// We disabled the scroll bar, this makes sure the top is always at line 1. + /// + private void EnsureContentsVisible() + { + _viewer.RightView.QueuePostLayoutAction(delegate + { + if (_viewer.RightView.TextViewLines[0].Start.Position != 0 || _viewer.RightView.TextViewLines[0].Top < _viewer.RightView.ViewportTop) + _viewer.RightView.DisplayTextLineContainingBufferPosition(new SnapshotPoint(_viewer.RightView.TextSnapshot, 0), 0.0, ViewRelativePosition.Top); + }); + + _viewer.LeftView.QueuePostLayoutAction(delegate + { + if (_viewer.LeftView.TextViewLines[0].Start.Position != 0 || _viewer.LeftView.TextViewLines[0].Top < _viewer.LeftView.ViewportTop) + _viewer.LeftView.DisplayTextLineContainingBufferPosition(new SnapshotPoint(_viewer.LeftView.TextSnapshot, 0), 0.0, ViewRelativePosition.Top); + }); + } + + /// + /// Fit the containing window size to the code size. + /// + private void RecalculateSize() + { + var size = ContentHeight(); + + // the scroll bar area + double? leftBottomHeight = LeftHost.GetTextViewMargin(PredefinedMarginNames.Bottom)?.VisualElement.ActualHeight; + double? rightBottomHeight = RightHost.GetTextViewMargin(PredefinedMarginNames.Bottom).VisualElement.ActualHeight; + if (leftBottomHeight != null && rightBottomHeight != null) + { + double bottomMaxHeight = Math.Max(leftBottomHeight.Value, rightBottomHeight.Value); + size += bottomMaxHeight; + + // try to make them the same height, because the right one will have intellicode button and it can't be disabled + var leftScroll = LeftHost.GetTextViewMargin(PredefinedMarginNames.HorizontalScrollBarContainer); + var rightScroll = RightHost.GetTextViewMargin(PredefinedMarginNames.HorizontalScrollBarContainer); + if (leftScroll != null) leftScroll.VisualElement.Height = bottomMaxHeight; + if (rightScroll != null) rightScroll.VisualElement.Height = bottomMaxHeight; + } + + LeftView.ZoomLevel = 100; + RightView.ZoomLevel = 100; + LeftHost.HostControl.Height = size * LeftView.ZoomLevel * 0.01; + RightHost.HostControl.Height = size * RightView.ZoomLevel * 0.01; + } + + /// + /// On closed event for the two inner text view. Applied for both inner text views. + /// + /// + /// + private void DiffView_OnClosed(object sender, EventArgs e) + { + IWpfTextView view = sender as IWpfTextView; + view.Closed -= DiffView_OnClosed; + view.LayoutChanged -= DiffView_OnLayoutChanged; + view.ViewportHeightChanged -= DiffView_OnViewportHeightChanged; + view.VisualElement.PreviewMouseWheel -= DiffView_OnPreviewMouseWheel; + } + + private void LeftView_OnClosed(object sender, EventArgs e) + { + LeftView.VisualElement.GotFocus -= LeftView_OnGotFocus; + LeftView.VisualElement.LostFocus -= LeftView_OnLostFocus; + LeftView.Caret.PositionChanged -= LeftView_OnCaretPositionChanged; + LeftView.Closed -= LeftView_OnClosed; + } + + private void RightView_OnClosed(object sender, EventArgs e) + { + RightView.VisualElement.GotFocus -= RightView_OnGotFocus; + RightView.VisualElement.LostFocus -= RightView_OnLostFocus; + RightView.Caret.PositionChanged -= RightView_OnCaretPositionChanged; + RightView.Closed -= RightView_OnClosed; + } + + /// + /// On closed event for the difference viewer. + /// + /// + /// + private void DifferenceViewer_OnClosed(object sender, EventArgs e) + { + _diffBuffer.SnapshotDifferenceChanged -= DiffBuffer_SnapshotDifferenceChanged; + _viewer.Closed -= DifferenceViewer_OnClosed; + } + + /// + /// Block the mouse wheel input to the diff view and send it to the hostview instead. Applied for both inner text views. + /// + /// + /// + private void DiffView_OnPreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e) + { + if (e.Delta == 0) return; + + // TODO: get the Vertical scrolling sensitivity setting instead of hardcoded + // or just find a way to send mouse scroll to host view, RaiseEvent doesn't work + _hostView.ViewScroller.ScrollViewportVerticallyByLines(e.Delta > 0 ? ScrollDirection.Up : ScrollDirection.Down, 3); + e.Handled = true; + } + + /// + /// Called when the layout of one of the inner text views changes meaning that
+ /// the user scrolled the text view or did anything to make it offscreen. + ///
+ /// + /// + private void DiffView_OnLayoutChanged(object sender, TextViewLayoutChangedEventArgs e) + { + EnsureContentsVisible(); + } + + /// + /// Applied for both inner text views. + /// + /// + /// + private void DiffView_OnViewportHeightChanged(object sender, EventArgs e) + { + RecalculateSize(); + } + + /// + /// Show the "selected line highlight" (i.e. the grey rectangle surrounding the line) for this view and disable for others. + /// + /// + private void ShowSelectedLineForView(IWpfTextView view) + { + LeftView.Options.SetOptionValue(DefaultWpfViewOptions.EnableHighlightCurrentLineId, (view == LeftView)); + RightView.Options.SetOptionValue(DefaultWpfViewOptions.EnableHighlightCurrentLineId, (view == RightView)); + _hostView.Options.SetOptionValue(DefaultWpfViewOptions.EnableHighlightCurrentLineId, (view == _hostView)); + } + + private void RightView_OnLostFocus(object sender, RoutedEventArgs e) + { + if (!LeftView.VisualElement.IsFocused) + ShowSelectedLineForView(_hostView); + } + + private void LeftView_OnLostFocus(object sender, RoutedEventArgs e) + { + if (!RightView.VisualElement.IsFocused) + ShowSelectedLineForView(_hostView); + } + + private void LeftView_OnGotFocus(object sender, RoutedEventArgs e) + { + ShowSelectedLineForView(LeftView); + } + + private void RightView_OnGotFocus(object sender, RoutedEventArgs e) + { + ShowSelectedLineForView(RightView); + } + + /// + /// Called when the difference buffer changes, likely on intialization. + /// + /// + /// + private void DiffBuffer_SnapshotDifferenceChanged(object sender, SnapshotDifferenceChangeEventArgs e) + { + RecalculateSize(); + } + + // Attemp to sync the caret selected line between both views + private void RightView_OnCaretPositionChanged(object sender, CaretPositionChangedEventArgs e) + { + //ITextSnapshotLine snapshotLine = e.NewPosition.BufferPosition.GetContainingLine(); + //if (snapshotLine.LineNumber >= LeftView.TextSnapshot.LineCount) return; + + //snapshotLine = LeftView.TextSnapshot.GetLineFromLineNumber(snapshotLine.LineNumber); + //SnapshotPoint point = new(LeftView.TextSnapshot, snapshotLine.Start); + + //ITextViewLine line = LeftView.TextViewLines.GetTextViewLineContainingBufferPosition(point); + //if (line != null) LeftView.Caret.MoveTo(line); + } + + // Attemp to sync the caret selected line between both views + private void LeftView_OnCaretPositionChanged(object sender, CaretPositionChangedEventArgs e) + { + //ITextSnapshotLine snapshotLine = e.NewPosition.BufferPosition.GetContainingLine(); + //if (snapshotLine.LineNumber >= RightView.TextSnapshot.LineCount) return; + + //snapshotLine = RightView.TextSnapshot.GetLineFromLineNumber(snapshotLine.LineNumber); + //SnapshotPoint point = new(RightView.TextSnapshot, snapshotLine.Start); + + //ITextViewLine line = RightView.TextViewLines.GetTextViewLineContainingBufferPosition(point); + //if (line != null) RightView.Caret.MoveTo(line); + } + + /* + * ======================================================= + * | | + * | This should be left here | + * | for future reference | + * | | + * ======================================================| + * + private static ConstructorInfo? _vsTextBufferAdapterCtor = null; + private static readonly object?[] _twoNulls = new object[2]; + + // Create the IVsTextLines from the InlineDiffTextDataModel. + // This is the implementation of `EditorAdaptersFactoryService.CreateVsTextBufferAdapter`. + // We must do this instead of using `CreateVsTextBufferAdapter` because in the + // `TextDocData.SetSite` method, it check if `_serviceProvider` was set or not. + // If not, it will create its own `_documentTextBuffer` and `_dataTextBuffer`. + // We don't want that, as we have our own buffers. + // + // Using internal components sucks but this is unavoidable. + private IVsTextLines CreateVsTextLines(InlineDiffTextDataModel dataModel) + { + ThreadHelper.ThrowIfNotOnUIThread("CreateTextLines"); + + if (_vsTextBufferAdapterCtor == null) + { + // Get the "Microsoft.VisualStudio.Editor.Implementation" assembly + Assembly assembly = MefProvider.Instance.EditorAdaptersFactoryService.GetType().Assembly; + + // Get the `VsTextBufferAdapter` constructor + Type typeVsTextBufferAdapter = assembly?.GetType("Microsoft.VisualStudio.Editor.Implementation.VsTextBufferAdapter"); + _vsTextBufferAdapterCtor = typeVsTextBufferAdapter?.GetConstructor([]); + + Assumes.True(_vsTextBufferAdapterCtor != null, + $"InlineDiffView.CreateTextLines: Failed to get the constructor for VsTextBufferAdapter. " + + $"Assembly: {assembly?.FullName}; Type: {typeVsTextBufferAdapter?.FullName}" + ); + } + + IVsTextLines vsTextLines = (IVsTextLines)_vsTextBufferAdapterCtor.Invoke([]); + Type type = vsTextLines.GetType(); + Type baseType = type.BaseType; + + BindingFlags privateFlag = BindingFlags.Instance | BindingFlags.NonPublic; + BindingFlags publicFlag = BindingFlags.Instance | BindingFlags.Public; + + // these should be the same, it's not a bug + type.GetField("_documentTextBuffer", privateFlag).SetValue(vsTextLines, dataModel.DataBuffer); + type.GetField("_dataTextBuffer" , privateFlag).SetValue(vsTextLines, dataModel.DataBuffer); + + // we MUST set the _serviceProvider before calling SetSite + type.GetField("_serviceProvider" , privateFlag).SetValue(vsTextLines, MefProvider.Instance.OleServiceProvider); + type.GetMethod("SetSite" , publicFlag).Invoke(vsTextLines, [MefProvider.Instance.OleServiceProvider]); + + baseType.GetMethod("InitializeUndoManager" , privateFlag).Invoke(vsTextLines, []); + baseType.GetMethod("InitializeDocumentTextBuffer", privateFlag).Invoke(vsTextLines, []); + baseType.GetMethod("OnTextBufferInitialized" , privateFlag).Invoke(vsTextLines, _twoNulls); + + return vsTextLines; + } + */ +} diff --git a/CodeiumVS/LanguageServer/LanguageServerController.cs b/CodeiumVS/LanguageServer/LanguageServerController.cs index 5784577..2612c22 100644 --- a/CodeiumVS/LanguageServer/LanguageServerController.cs +++ b/CodeiumVS/LanguageServer/LanguageServerController.cs @@ -1,4 +1,5 @@ -using CodeiumVS.Packets; +using CodeiumVs.InlineDiff; +using CodeiumVS.Packets; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; using ProtoBuf; @@ -51,6 +52,23 @@ void OnMessage(object sender, MessageEventArgs msg) var data = request.insert_at_cursor; InsertText(data.text); } + else if (request.ShouldSerializeapply_diff()) + { + var data = request.apply_diff; + string replacement = ""; + + // i'd rather not using .Join here because it looks too scary + foreach (var line in data.diff.unified_diff.lines) + { + if (line.type == UnifiedDiffLineType.UNIFIED_DIFF_LINE_TYPE_INSERT || + line.type == UnifiedDiffLineType.UNIFIED_DIFF_LINE_TYPE_UNCHANGED) + { + replacement += line.text + "\n"; + } + } + + ApplyDiff(data.file_path, data.diff.start_line, data.diff.end_line, replacement); + } } void OnError(object sender, WebSocketSharp.ErrorEventArgs error) @@ -94,6 +112,36 @@ private void InsertText(string text) }).FireAndForget(true); } + private void ApplyDiff(string filePath, int start_line, int end_line, string replacement) + { + ThreadHelper.JoinableTaskFactory.RunAsync(async delegate + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + // some how OpenViaProjectAsync doesn't work... at least for me + DocumentView? docView = await VS.Documents.OpenAsync(filePath); + if (docView?.TextView == null) return; + + // FIXME: if the file is closed, or it's a new file, GetInstance will failed + // we'd have to wait for the `CodeiumInlineDiffViewProvider` to run + InlineDiffAdornment? adornment = InlineDiffAdornment.GetInstance(docView.TextView); + if (adornment == null) return; + + var snapshot = docView.TextView.TextSnapshot; + ITextSelection selection = docView.TextView.Selection; + + var lineStart = docView.TextBuffer.CurrentSnapshot.GetLineFromLineNumber(start_line - 1); + var lineEnd = docView.TextBuffer.CurrentSnapshot.GetLineFromLineNumber(end_line - 1); + + int position = lineStart.Start.Position; + int length = lineEnd.End.Position - position; + + docView.TextView.DisplayTextLineContainingBufferPosition(new SnapshotPoint(snapshot, position), docView.TextView.ViewportHeight / 2, ViewRelativePosition.Top); + await adornment.CreateDiffAsync(position, length, replacement); + + }).FireAndForget(); + } + private void OpenSelection(string filePath, int start_line, int start_col, int end_line, int end_col) { ThreadHelper.JoinableTaskFactory.RunAsync(async delegate diff --git a/CodeiumVS/Utilities/MefProvider.cs b/CodeiumVS/Utilities/MefProvider.cs index 407cec3..8a51502 100644 --- a/CodeiumVS/Utilities/MefProvider.cs +++ b/CodeiumVS/Utilities/MefProvider.cs @@ -1,6 +1,16 @@ -using Microsoft; +global using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider; + +using Microsoft; using Microsoft.VisualStudio.ComponentModelHost; +using Microsoft.VisualStudio.Editor; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; using Microsoft.VisualStudio.Language.Intellisense; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Text.Differencing; +using Microsoft.VisualStudio.Text.Editor.Commanding; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Projection; +using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Tagging; using System.ComponentModel.Composition; @@ -14,22 +24,22 @@ internal class MefProvider internal static MefProvider Instance { get { return _instance ??= new MefProvider(); } } // disabled because not needed right now - //[Import] internal IPeekBroker PeekBroker { get; private set; } + [Import] internal IPeekBroker PeekBroker { get; private set; } + [Import] internal ICompletionBroker CompletionBroker { get; private set; } [Import] internal IAsyncQuickInfoBroker AsyncQuickInfoBroker { get; set; } - //[Import] internal ICompletionBroker CompletionBroker { get; private set; } - //[Import] internal IAsyncCompletionBroker AsyncCompletionBroker { get; private set; } - //[Import] internal ITextEditorFactoryService TextEditorFactoryService { get; private set; } - //[Import] internal ITextDocumentFactoryService TextDocumentFactoryService { get; private set; } - //[Import] internal IDifferenceBufferFactoryService2 DifferenceBufferFactory { get; private set; } - //[Import] internal IWpfDifferenceViewerFactoryService DifferenceViewerFactory { get; private set; } + [Import] internal IAsyncCompletionBroker AsyncCompletionBroker { get; private set; } + [Import] internal ITextEditorFactoryService TextEditorFactoryService { get; private set; } + [Import] internal ITextDocumentFactoryService TextDocumentFactoryService { get; private set; } + [Import] internal IDifferenceBufferFactoryService2 DifferenceBufferFactory { get; private set; } + [Import] internal IWpfDifferenceViewerFactoryService DifferenceViewerFactory { get; private set; } [Import] internal IViewTagAggregatorFactoryService TagAggregatorFactoryService { get; private set; } - //[Import] internal IVsEditorAdaptersFactoryService EditorAdaptersFactoryService { get; private set; } - //[Import] internal IProjectionBufferFactoryService ProjectionBufferFactoryService { get; private set; } - //[Import] internal IEditorCommandHandlerServiceFactory EditorCommandHandlerService { get; private set; } + [Import] internal IVsEditorAdaptersFactoryService EditorAdaptersFactoryService { get; private set; } + [Import] internal IProjectionBufferFactoryService ProjectionBufferFactoryService { get; private set; } + [Import] internal IEditorCommandHandlerServiceFactory EditorCommandHandlerService { get; private set; } - //internal IOleServiceProvider OleServiceProvider { get; private set; } - //internal IVsRunningDocumentTable RunningDocumentTable { get; private set; } - //internal IVsUIShellOpenDocument DocumentOpeningService { get; private set; } + internal IOleServiceProvider OleServiceProvider { get; private set; } + internal IVsRunningDocumentTable RunningDocumentTable { get; private set; } + internal IVsUIShellOpenDocument DocumentOpeningService { get; private set; } private MefProvider() { @@ -44,13 +54,13 @@ private MefProvider() _compositionService.DefaultCompositionService.SatisfyImportsOnce(this); // disabled because not needed right now - //OleServiceProvider = await ServiceProvider.GetGlobalServiceAsync(typeof(IOleServiceProvider)) as IOleServiceProvider; - //RunningDocumentTable = await ServiceProvider.GetGlobalServiceAsync(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable; - //DocumentOpeningService = await ServiceProvider.GetGlobalServiceAsync(typeof(SVsUIShellOpenDocument)) as IVsUIShellOpenDocument; + OleServiceProvider = await ServiceProvider.GetGlobalServiceAsync(typeof(IOleServiceProvider)) as IOleServiceProvider; + RunningDocumentTable = await ServiceProvider.GetGlobalServiceAsync(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable; + DocumentOpeningService = await ServiceProvider.GetGlobalServiceAsync(typeof(SVsUIShellOpenDocument)) as IVsUIShellOpenDocument; - //Assumes.NotNull(OleServiceProvider); - //Assumes.NotNull(RunningDocumentTable); - //Assumes.NotNull(DocumentOpeningService); + Assumes.NotNull(OleServiceProvider); + Assumes.NotNull(RunningDocumentTable); + Assumes.NotNull(DocumentOpeningService); }); } } \ No newline at end of file