After updating to version 21.1.2, the cursor position is wrong.

SyntaxEditor for WPF Forum

Posted 3 years ago by Sunshine - Appeon
Version: 21.1.2
Platform: .NET 5.0
Environment: Windows 10 (64-bit)
Avatar

After I did a text replacement, I repositioned the cursor position, but the cursor position I saw was still at the end of the replacement text.

For example:

I inserted a commented snippet in the code:

( '|' Represents the position of the cursor)

/// <summary>
/// |
/// </summary>

After replacing the text once, I repositioned the cursor on the second line. But the new version will position him at the end of the replacement text

like this:

/// <summary>
/// 
/// </summary>|

My code runs completely normal in the v2021.1.1 version, and the above problem occurs when I upgrade to v2021.1.2

Comments (17)

Posted 3 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hello,

Can you post exact code to place in one of our samples to see this scenario occur so that we can debug the scenario?  Thanks!


Actipro Software Support

Posted 3 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hello,

Thank you for the sample via a support ticket.  In that sample, you were handling the SyntaxEditor.DocumentTextChanged event and were applying an AutoComplete text change in response to it.  Right after calling the ITextChange.Apply() method, you set the editor.ActiveView.Selection.CaretOffset to a value.

In v21.1.2, we had to make a change to ensure that SyntaxEditor.DocumentTextChanged events fire in proper version order.  The issue in prior versions was this kind of scenario:

  • Some text change occurs, causing snapshot version 2.
  • The SyntaxEditor.DocumentTextChanged event is raised for version 2 and some watchers (like for selection changes, etc.) start handling it.
  • Code that provides auto-completion (or other things like auto-correct, auto-indent) also handles the SyntaxEditor.DocumentTextChanged event and applies another text change to further alter the original text change's text results.  This causes snapshot version 3.
  • The SyntaxEditor.DocumentTextChanged event is immediately raised for version 3 and all watchers handle version 3's event completely.
  • Now that the auto-completion logic's Apply() method returns, any remaining watchers of the DocumentTextChanged event that have not yet handled the event that was raised for version 2 get that event.
  • At this point, some event watchers will have received version 3's event before version 2 and things like outlining or selection could get out of whack.

The change we made in v21.1.2 was to ensure that SyntaxEditor.DocumentTextChanged always fires events in version order.  If it's in the middle of raising snapshot version 2's event, it will queue up snapshot version 3's event to fire only after ALL handlers of the event have first seen version 2's event.

In your scenario, this means that the view is not yet aware of the new snapshot created by your auto-complete, even though it returned from that text change's Apply() method.  Thus trying to set the view's selection with editor.ActiveView.Selection.CaretOffset based on the results of the auto-complete snapshot (which the view doesn't yet know about) will fail to work properly.

Instead of setting the CaretOffset property, you can do this kind of thing before your auto-completion's Apply() call, and it will work:

yourAutoCompleteChange.PostSelectionPositionRanges = TextPositionRange.CreateCollection(new TextPositionRange(0, 5, 0, 5), isBlock: false);

That allows you to specify what the resulting selection should be after the text change is applied.  There it's saying it should be a zero-length selection at position (0, 5).

I hope that helps explain why the event logic change was necessary and how to alter your selection code appropriately.


Actipro Software Support

Posted 3 years ago by Sunshine - Appeon
Avatar

According to the solution you provided, I seem to have encountered a new problem:

Still this scene:

I want to insert a comment snippet and then reposition the cursor.

When I enter the third'/', I need to insert this snippet.

/// <summary>
/// |
/// </summary>

In the data I obtained, I only know that the position of the cursor needs to be positioned at offset 19.

So I need to convert the offset to position,but at this time I cannot get the snapshot after inserting the snippet.

Even if I call myAutoCompleteChange.Replace().

The content of myAutoCompleteChange.Snapshot still does not change, which prevents me from getting the correct position after inserting the snippet.

///

How to deal with this situation?

Posted 3 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Yes, snapshots cannot be immediately updated in the view in auto-complete scenarios for the reason previously described where changed event handlers for the original primary text change were not able to fully complete prior to the auto-complete text change triggering another snapshot and its changed event handlers were all executed.  This would lead to some view and language services getting changed events firing out of snapshot version order, which could lead to problems, some of which could be bad.

Due to the recent change, while the view hasn't yet updated itself to the latest document snapshot (since the event handler where it updates hasn't executed yet in this scenario), the document itself will have the latest snapshot on its CurrentSnapshot property.  Thus instead of using editor.ActiveView.CurrentSnapshot, you should be able to look at editor.Document.CurrentSnapshot instead.


Actipro Software Support

Posted 3 years ago by Sunshine - Appeon
Avatar

Thanks for your hints!

After myChange.Apply(), I set the value of myChange.PostSelectionPositionRanges to reposition the cursor position. Achieved what I wanted, is this correct?

Posted 3 years ago by Sunshine - Appeon
Avatar

Is it only SyntaxEditor.DocumentTextChanged processing that has changed?

Others, such as whether the triggering of IEditorDocumentTextChangeEventSink.NotifyDocumentTextChanged will affect.

We have a lot of code to replace text and then position the cursor. But it seems that only part of it is affected

Posted 3 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Yes, setting ITextChange.PostSelectionPositionRanges is the best way to ensure the selection is at a specific place after any text change.  Setting that property will work both before and after the change made in the prior version since PostSelectionPositionRanges is used after the text change is applied to the view.

On that note, I would recommend setting it before you call ITextChange.Apply() though.  Setting it after may work in this particular scenario since the changed event is getting buffered, but in normal circumstances when there isn't already a text change being processed, setting PostSelectionPositionRanges after Apply() would be too late.

Right now only the view's DocumentTextChange events have been affected by the recent change, but looking into the core text model to possibly do something similar is something else we're considering, as the same core issue may still exist there where events could fire out of snapshot version order.  That is marked for investigation here.  Even if that does change in the future, if you set PostSelectionPositionRanges prior to calling Apply(), it should work fine in any scenario.

If you have any other suggestions for how we can make some of this easier for you, while still handling the fundamental problem described in previous replies, feel free to post here and we can discuss further.


Actipro Software Support

Posted 3 years ago by Sunshine - Appeon
Avatar

1.On that note, I would recommend setting it before you call ITextChange.Apply() though.  Setting it after may work in this particular scenario since the changed event is getting buffered, but in normal circumstances when there isn't already a text change being processed, setting PostSelectionPositionRanges after Apply() would be too late.

According to you say, I need to set the PostSelectionPositionRanges property before calling ITextChange.Apply().

But as I mentioned earlier, the data for cursor repositioning may be an offset. I need to convert him to position. I need the latest snapshot to convert the offset to the correct position.

So in this case, I can only call ITextChange.Apply() first. After getting the latest snapshot, the converted position can be obtained to reposition the cursor.

2.About the scope of influence

After this update, I temporarily found that two functions were abnormal.

(1)One is the formatting function, which I inherited from TextFormatter. In the implementation of the Format method, there is a method to reposition the cursor (reposition the cursor by setting Active.Selection.CaretOffset).

(2)The other is to insert comment snippet. This is achieved by subscribing to the SyntaxEditor.DocumentTextChanged event, and the cursor positioning fails.

In the method implemented by IEditorDocumentTextChangeEventSink. Is working as expected.

Posted 3 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hello,

1) You really want to set PostSelectionPositionRanges before Apply() is called for consistent results in any scenario.  Going back to your sample above where you were inserting this in response to a "/" being typed:

/// <summary>
/// |
/// </summary>

You know the position (line/char) of the third "/" after "///" is typed since that's where the caret is when your auto-correct logic starts to execute.  Say it's on line index 9.  And you know after inserting this XML doc comment that the caret needs to end up at character 4 on the next line.  Therefore you want to set the PostSelectionPositionRanges to a zero-length selection at position (10, 4).

You may need to scan characters on the inserted text to determine the final position if you only had predetermined offset data of where the caret should end up relative to the start of this snippet.  So you scan each character and 1-up the character index after each character.  If you reach a "\n" character, 1-up the line index and reset the character index to 0.  Then you have a line/char relative to the start of the text snippet.  If you think it would be helpful, we could even provide a string helper method to do this.  Then you can use that relative line/char to help determine the position for PostSelectionPositionRanges.  

2) While IEditorDocumentTextChangeEventSink works in the latest version how you were doing things, keep in mind that may have to change in the future since those events may fire out of order as well in certain scenarios.  We are still pending investigation on that.  But as mentioned before, setting PostSelectionPositionRanges before Apply() is the best thing to do, in old versions and new versions, and it will work consistently in both.  


Actipro Software Support

Posted 3 years ago by Sunshine - Appeon
Avatar

This is equivalent to needing me to create a newest snapshot myself, and then convert the coordinates myself. This approach seems unreasonable.

The principle of PostSelectionPositionRanges is to reposition the cursor position after applying changes. Before that, he had the latest snapshot. Can we provide the property of PostSelectionRanges, we can set the range of the offset of the selection area.

Posted 3 years ago by Sunshine - Appeon
Avatar

Hello!

Is there a response to the last statement?

[Modified 3 years ago]

Posted 3 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

I know it's not ideal to not have the next snapshot immediately available, however the change is needed because of the reasons described above.  Selection is all tracked internally with positions (line/char) and not offsets, which is why PostSelectionPositionRanges is there.  This is done to properly support features like selection within virtual space.

You mentioned the idea of possibly adding a PostSelectionRanges property, which I assume would be the same concept as the current PostSelectionPositionRanges but TextRange (and thus offset)-based?  The concern with adding another property that would do effectively the same thing as PostSelectionPositionRanges is that it might create more confusion.  It might be a better idea if you have offsets to build in some helper method that would help translate those text ranges to position ranges that could be set with PostSelectionPositionRanges.


Actipro Software Support

Posted 3 years ago by Sunshine - Appeon
Avatar

Hello!

For your reply, I hold a different view:

(1)PostSelectionPositionRanges and PostSelectionRanges are used for cursor positioning after text changes. And PostSelectionPositionRanges is just an additional function of virtual cursor positioning, because only the Position property can be positioned to the virtual position.So it seems that PostSelectionPositionRanges only has one more function, and it is impossible for users to use PostSelectionRanges to reposition the virtual cursor.

(2)There are few scenes for virtual cursor positioning, and the basic cursor positioning function is mainly used.

(3)I do not agree with the helper method to convert the position because:

My original purpose was to apply the changes to the current text and then reposition the cursor, but I cannot get the latest snapshot before the changes are applied, and the cursor repositioning needs to be set before the changes are applied. I need to do the repetitive things before the changes are applied to get the latest snapshot text, and then use it to convert the position.This approach is very unreasonable to users.

Posted 3 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

I wanted to bring up one alternative to having to set PostSelectionPositionRanges at all for this particular scenario where you are inserting a bit of text and you want the caret somewhere in the middle of it.  What you could do in that case is split the inserted text in two at the offset that the caret should be at.  Then create one operation for the text after the caret and a second operation for the text before where the caret should end up.  When that second operation in the text change occurs, the caret will naturally be positioned after it.

Here's an example that could be put in the project you sent us via the ticket, where if you type "/" it will replace that with a XML comment insertion and the caret will be at the right spot once the text change is applied in the view.

private void Editor_DocumentTextChanged(object sender, ActiproSoftware.Windows.Controls.SyntaxEditor.EditorSnapshotChangedEventArgs e) {
	if (!e.TextChange.IsUndo && !e.TextChange.IsRedo && e.TextChange.Type != TextChangeTypes.AutoComplete) {
		var change = editor.Document.CreateTextChange(TextChangeTypes.AutoComplete);

		var textToInsert = "/// <summary>\n/// |\n/// </summary>\n";
		var caretSplitIndex = textToInsert.IndexOf('|');
		change.ReplaceText(new TextRange(e.TextChange.Operations[0].StartOffset, e.TextChange.Operations[0].InsertionEndOffset), textToInsert.Substring(caretSplitIndex + 1));
		change.InsertText(e.TextChange.Operations[0].StartOffset, textToInsert.Substring(0, caretSplitIndex));
		// Without PostSelectionPositionRanges specified, the caret will move to the end of the last operation, which in this case is the InsertText operation
		change.Apply();
	}
}


Actipro Software Support

Posted 3 years ago by Sunshine - Appeon
Avatar

Hello!

I still want to know if it is possible to support this property PostSelectionRanges, or what is the reason why it cannot be supported.This should be the most ideal solution.

We have a lot of code, and such scenarios are very common. We cannot accept alternative solutions to such problems. This will increase the complexity of our code, and need to re-verify every function.

The currently missing function is only to reposition the cursor by offset after changing the text. This should be a very basic function and should not be solved by various alternatives.

Answer - Posted 3 years ago by Actipro Software Support - Cleveland, OH, USA
Avatar

Hello,

If you want to use offsets, we actually already have an ITextChange.Apply method overload where you can pass in offset-based TextRanges to drive the selection.  The final overload to this method indicates how the text ranges should be translated from the old to the new snapshot, if at all.  In your case, text is getting inserted and we want to make a zero-width selection within that text, so you pass null into the last argument so that no translation occurs (we are telling it the post-apply text ranges):

private void Editor_DocumentTextChanged(object sender, ActiproSoftware.Windows.Controls.SyntaxEditor.EditorSnapshotChangedEventArgs e) {
	if (!e.TextChange.IsUndo && !e.TextChange.IsRedo && e.TextChange.Type != TextChangeTypes.AutoComplete) {
		var change = editor.Document.CreateTextChange(TextChangeTypes.AutoComplete);

		var textToInsert = "/// <summary>\n/// \n/// </summary>\n";
		var caretOffsetInText = 18;
		change.ReplaceText(new TextRange(e.TextChange.Operations[0].StartOffset, e.TextChange.Operations[0].InsertionEndOffset), textToInsert);
		change.Apply(new TextRange[] { new TextRange(e.TextChange.Operations[0].StartOffset + caretOffsetInText) }, 0, null);
	}
}

[Modified 3 years ago]


Actipro Software Support

Posted 3 years ago by Sunshine - Appeon
Avatar

Yes, this is exactly what we need. Thank you for your assistance!

The latest build of this product (v24.1.1) was released 2 months ago, which was after the last post in this thread.

Add Comment

Please log in to a validated account to post comments.