5

I am working on an extension that uses an external program to format code inside Visual Studio.

If I replace the contents of the file using ITextEdit.Replace(...) the caret will be put at the end of the document, which is wrong.

What I'm trying to do is saving a snapshot of the current caret position in the textbuffer before replacing the contents of the file, and then setting the caret position to where it previously was in the textbuffer before replacing content.

However, ITextEdit.Apply() is generating a new snapshot causing _textView.Caret.MoveTo(point) to throw an exception:

System.ArgumentException: The supplied SnapshotPoint is on an incorrect snapshot.
Parameter name: bufferPosition
at Microsoft.VisualStudio.Text.Editor.Implementation.CaretElement.InternalMoveTo(VirtualSnapshotPoint bufferPosition, PositionAffinity caretAffinity, Boolean captureHorizontalPosition, Boolean captureVerticalPosition, Boolean raiseEvent)
at Microsoft.VisualStudio.Text.Editor.Implementation.CaretElement.MoveTo(SnapshotPoint bufferPosition)

I have also tried creating a new snapshot point instead of using _textView.Caret.Position.BufferPosition, like so:

var point = new SnapshotPoint(_textView.TextSnapshot, 0);

throwing the same "The supplied SnapshotPoint is on an incorrect snapshot." exception.

public class MyCommand
{
    private readonly IWpfTextView _textView;
    private readonly MyFormatter _formatter;
    private readonly ITextDocument _document;

    public MyCommand(IWpfTextView textView, MyFormatter formatter, ITextDocument document)
    {
        _textView = textView;
        _formatter = formatter;
        _document = document;
    }

    public void Format()
    {
        var input = _document.TextBuffer.CurrentSnapshot.GetText();
        var output = _formatter.format(input);

        // get caret snapshot point
        var point = _textView.Caret.Position.BufferPosition;

        using (var edit = _document.TextBuffer.CreateEdit())
        {
            edit.Replace(0, _document.TextBuffer.CurrentSnapshot.Length, output);
            edit.Apply();
        }

        // set caret position
        _textView.Caret.MoveTo(point);
    }
}

I don't want to implement some custom caret "history", I want to do it the way it is meant to be. Also I would like the moving caret to be considered part of the edit, keeping the "ctrl+z" functionality intact.

As always, any help is greatly appreciated!

2 Answers2

1

you can retrieve the position of the point, then create a new SnapshotPoint and move to it.

like this:

var point = _textView.Caret.Position.BufferPosition;
int position = point.Position;

        using (var edit = _document.TextBuffer.CreateEdit())
        {
            edit.Replace(0, _document.TextBuffer.CurrentSnapshot.Length, output);
            edit.Apply();
        }

        // set caret position
        _textView.Caret.MoveTo(new SnapshotPoint(_textView.TextSnapshot, position));

In addition, you can create and use a extension like this: https://github.com/jaredpar/EditorUtils/blob/master/Src/EditorUtils/Extensions.cs

Zhanglong Wu - MSFT
  • 1,652
  • 1
  • 7
  • 8
1

Although Cole Wu - MSFT's answer to my question helped me solve my issue, it didn't do exactly what I wanted:

Using the provided code works to move the caret, however since I replace the whole content of the document it also reset the scroll position to the top even though the caret was further down the document.

Also: this still generated "multiple undos" instead of just one: i.e. replacing the content of the document and moving the caret was two separate "undos", which required the user to hit CTRL + Z two times instead of just one to undo the replace.

To solve this I changed the code to the following:

public class MyCommand
{
    private readonly IWpfTextView _textView;
    private readonly MyFormatter _formatter;
    private readonly ITextDocument _document;
    private readonly ITextBufferUndoManager _undoManager;

    public MyCommand(IWpfTextView textView, MyFormatter formatter, ITextDocument document, ITextBufferUndoManager undoManager)
    {
        _textView = textView;
        _formatter = formatter;
        _document = document;
        _undoManager = undoManager;
    }

    public void Format()
    {
        var input = _textView.TextSnapshot.GetText();
        var output = _formatter.format(input);

        using (var undo = _undoManager.TextBufferUndoHistory.CreateTransaction("Format"))
        using (var edit = _undoManager.TextBuffer.CreateEdit(EditOptions.DefaultMinimalChange, 0, null))
        {
            edit.Replace(0, _textView.TextSnapshot.Length, output);
            edit.Apply();

            undo.Complete();
        }
    }
}

This does not work exactly the way I want because the caret sometimes (when positioned at trailing whitespace) jump down to the start of the next line instead of at the end of the current line.

However, its close enough!