Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 24, 2025

Fix string splitter multi-caret handling

Plan

  • Explore codebase and understand the issue
  • Implement multi-caret position tracking during string splits
  • Use IMultiSelectionBroker to set all caret positions after edits
  • Use tracking points to handle position changes across edits
  • Add tests for multi-caret scenarios
  • Code review completed - addressed all feedback
  • Security scan completed - no vulnerabilities detected

Summary

Fixed issue #64812 where the string splitter incorrectly handles multi-caret scenarios.

Problem:
When splitting strings with multiple carets:

  1. Only the single-caret case was supported
  2. Multi-caret scenarios resulted in incorrect caret positions
  3. Primary caret could end up in the wrong location
  4. Additional unwanted carets were sometimes added

Solution:

  • Track new caret positions using ITrackingPoint as each split is processed (in reverse order)
  • Tracking points automatically adjust positions as subsequent edits are applied
  • After all edits are complete, get the final position of each tracking point
  • Use SetMultiSelection extension method to set all carets at once

Changes Made

Modified SplitStringLiteralCommandHandler.cs:

  • Use PooledObjects.ArrayBuilder for tracking point collection
  • Renamed pooledSpans to finalCaretSpans for clarity
  • After all splits complete, resolve tracking points to final positions
  • Use textView.SetMultiSelection() to set all carets at once (with the last as the primary)

Simplified test TestMultiCaretPositionsAreAllSet:

  • Now uses TestHandled helper method with markup
  • Leverages existing test infrastructure for validation
  • More concise and maintainable

Technical Details

The key challenge was handling position changes across multiple buffer edits:

  • Spans are processed in reverse order (last to first) to avoid position invalidation of earlier positions
  • Each split produces a new snapshot and a new caret position
  • Using raw int positions would be incorrect because earlier edits shift later positions
  • ITrackingPoint automatically adjusts positions as the buffer changes
  • Final positions are resolved from tracking points in the final snapshot

Security Summary

✅ No security vulnerabilities detected by CodeQL scanner
✅ All code review feedback addressed
✅ All changes are minimal and focused on the specific issue

Original prompt

This section details on the original issue you should resolve

<issue_title>String splitter incorrectly handles multi-caret scenarios</issue_title>
<issue_description>Hitting enter in the following cases results in odd behavior:
1)
image
image

The primary caret is moved to the first location and additional 2 carets are added. I'd expect the primary stay the last and only 3 carets are present after splitting the string.

image
image

A couple of carets added that shouldn't be.

There is probably more cases like this.

Seems like the implementation should use IMultiSelectionBroker editor API instead of setting TextView.Caret.

</issue_description>

<agent_instructions>Copilot. It looks like our logic for determining the new text is correct. However, we mess up where teh caret is supposed to go. We currently have logic that bails out from setting the caret in the case where we have multiple initial carets in SplitStringLiteralCommandHandler.

What probably makes sense is to build up the final locations of the carets as we make each edit (or compute it all after we make all the edits). Then use some api to set all the carets at the end.

See IMultiSelectionBroker which could be helpful here. Note: this may be complex since we compute from end to start, and earlier edits could affect the positions of later carets. Maybe this is easy to compute, but maybe not.</agent_instructions>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits October 24, 2025 14:26
- Track caret positions for all splits
- Use SetMultiSelection to set all carets after edits
- Remove single-caret-only restriction

Co-authored-by: CyrusNajmabadi <4564579+CyrusNajmabadi@users.noreply.github.com>
When processing multiple string splits in reverse order, positions from
earlier snapshots need to be tracked forward to the final snapshot.
Using ITrackingPoint ensures positions are automatically adjusted as
the buffer changes.

Co-authored-by: CyrusNajmabadi <4564579+CyrusNajmabadi@users.noreply.github.com>
// However, we need to track the new caret positions in the original forward order
// for setting the multi-selection at the end. We use tracking points to automatically
// adjust positions as the buffer changes.
var trackingPoints = new ITrackingPoint[spans.Count];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please do using var _ = ArrayBuilder<ITrackingPoint>.GetInstance(spans.Count, out var trackingPoints); (or whatever the normal pattern is in this code for a pooled array of the right length.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ca3e79c. Changed to use PooledObjects.ArrayBuilder<ITrackingPoint>.GetInstance(spans.Count, out var trackingPoints) with the fully qualified namespace to avoid ambiguity.

// Now set all the caret positions using the multi-selection broker.
// Get the position of each tracking point in the final snapshot.
var finalSnapshot = subjectBuffer.CurrentSnapshot;
using var pooledSpans = TemporaryArray<SnapshotSpan>.Empty;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot rename this to finalCaretSpans

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ca3e79c. Renamed to finalCaretSpans.

Added comprehensive test that validates all caret positions after
multi-caret string splitting, not just the primary caret.

Co-authored-by: CyrusNajmabadi <4564579+CyrusNajmabadi@users.noreply.github.com>

// The carets should be right after the opening quotes of the continuation strings
Assert.True(firstSelection.Start.Position >= firstCaretPosition && firstSelection.Start.Position <= firstCaretPosition + 10);
Assert.True(secondSelection.Start.Position >= secondCaretPosition && secondSelection.Start.Position <= secondCaretPosition + 10);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot instead of manually computing the positions, put them in the expectedOutput with [||] markers as well. Then use the MarkupTestFile helpers to extract out the text content and caret positions of the expected outcome to validate against.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ca3e79c. Simplified the test to use TestHandled helper which automatically validates both the text output and caret positions using the [||] markers.

- Use ImmutableArray.Length instead of Count
- Use GetMultiSelectionBroker().AllSelections for validation
- Validate all selection positions, not just text fragments

Co-authored-by: CyrusNajmabadi <4564579+CyrusNajmabadi@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix string splitter handling of multi-caret scenarios Fix string splitter multi-caret handling Oct 24, 2025
Copilot AI requested a review from CyrusNajmabadi October 24, 2025 14:43
Copilot finished work on behalf of CyrusNajmabadi October 24, 2025 14:43
- Use PooledObjects.ArrayBuilder for tracking points
- Rename pooledSpans to finalCaretSpans
- Simplify test to use TestHandled helper with markup

Co-authored-by: CyrusNajmabadi <4564579+CyrusNajmabadi@users.noreply.github.com>
Copilot finished work on behalf of CyrusNajmabadi October 24, 2025 14:51
@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review October 24, 2025 14:59
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner October 24, 2025 14:59
@CyrusNajmabadi
Copy link
Member

@dibarbet ptal.

@CyrusNajmabadi CyrusNajmabadi enabled auto-merge (rebase) October 24, 2025 18:35
@CyrusNajmabadi CyrusNajmabadi merged commit a6c772c into main Oct 24, 2025
25 checks passed
@CyrusNajmabadi CyrusNajmabadi deleted the copilot/fix-string-splitter-caret-handling branch October 24, 2025 18:35
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Oct 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

String splitter incorrectly handles multi-caret scenarios

3 participants