diff options
author | Kirill Osenkov <github@osenkov.com> | 2018-08-15 21:43:04 +0300 |
---|---|---|
committer | Kirill Osenkov <github@osenkov.com> | 2018-08-15 21:43:04 +0300 |
commit | 21b22d2687687c4013d8e7873dd515518b06b386 (patch) | |
tree | 087647d48385e1639f41fe49a9405526a175f465 | |
parent | 419aee36d47be0a340a04b55476b2ab9b4cf1b3b (diff) |
Add SelectionState.cs and ExtensionMethods.cs.
-rw-r--r-- | src/Text/Util/TextUIUtil/ExtensionMethods.cs | 143 | ||||
-rw-r--r-- | src/Text/Util/TextUIUtil/SelectionState.cs | 151 |
2 files changed, 294 insertions, 0 deletions
diff --git a/src/Text/Util/TextUIUtil/ExtensionMethods.cs b/src/Text/Util/TextUIUtil/ExtensionMethods.cs new file mode 100644 index 0000000..c791f15 --- /dev/null +++ b/src/Text/Util/TextUIUtil/ExtensionMethods.cs @@ -0,0 +1,143 @@ +using System; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Formatting; +using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; + +namespace Microsoft.VisualStudio.Text.MultiSelection +{ + public static class ExtensionMethods + { + public static VirtualSnapshotPoint NormalizePoint(this ITextView view, VirtualSnapshotPoint point) + { + var line = view.GetTextViewLineContainingBufferPosition(point.Position); + + //If point is at the end of the line, return it (including any virtual space offset) + if (point.Position >= line.End) + { + return new VirtualSnapshotPoint(line.End, point.VirtualSpaces); + } + else + { + //Otherwise align it with the begining of the containing text element & + //return that (losing any virtual space). + SnapshotSpan element = line.GetTextElementSpan(point.Position); + return new VirtualSnapshotPoint(element.Start); + } + } + + public static Selection MapToSnapshot(this Selection region, ITextSnapshot snapshot, ITextView view) + { + var newInsertion = view.NormalizePoint(region.InsertionPoint.TranslateTo(snapshot)); + var newActive = view.NormalizePoint(region.ActivePoint.TranslateTo(snapshot)); + var newAnchor = view.NormalizePoint(region.AnchorPoint.TranslateTo(snapshot)); + + return new Selection(newInsertion, newAnchor, newActive, region.InsertionPointAffinity); + } + + /// <summary> + /// Remaps a given x-coordinate to a valid point. If the provided x-coordinate is past the right end of the line, it will + /// be clipped to the correct position depending on the virtual space settings. If the ISmartIndent is providing indentation + /// settings, the x-coordinate will be changed based on that. + /// </summary> + public static double MapXCoordinate(this ITextViewLine textLine, ITextView textView, + double xCoordinate, ISmartIndentationService smartIndentationService, bool userSpecifiedXCoordinate) + { + if (textLine == null) + { + throw new ArgumentNullException(nameof(textLine)); + } + + if (textView == null) + { + throw new ArgumentNullException(nameof(textView)); + } + + // if the clicked point is to the right of the text and virtual space is disabled, the coordinate + // needs to be fixed + if ((xCoordinate > textLine.TextRight) && !textView.IsVirtualSpaceOrBoxSelectionEnabled()) + { + double indentationWidth = 0.0; + + // ask the ISmartIndent to see if any indentation is necessary for empty lines + if (textLine.End == textLine.Start) + { + int? indentation = smartIndentationService?.GetDesiredIndentation(textView, textLine.Start.GetContainingLine()); + if (indentation.HasValue) + { + //The indentation specified by the smart indent service is desired column position of the caret. Find out how much virtual space + //need to be at the end of the line to satisfy that. + // TODO: need a way to determine column width in xplat scenarios, bug https://devdiv.visualstudio.com/DevDiv/_workitems/edit/637741 + double columnWidth = 7; + indentationWidth = Math.Max(0.0, (((double)indentation.Value) * columnWidth - textLine.TextWidth)); + + // if the coordinate is specified by the user and the user has selected a coordinate to the left + // of the indentation suggested by ISmartIndent, overrule the ISmartIndent provided value and + // do not use any indentation. + if (userSpecifiedXCoordinate && (xCoordinate < (textLine.TextRight + indentationWidth))) + indentationWidth = 0.0; + } + } + + xCoordinate = textLine.TextRight + indentationWidth; + } + + return xCoordinate; + } + + /// <summary> + /// If you are looking at this, you're likely maintaining selection code, and should be aware that + /// virtual whitespace allowances are not simply checking a flag. + /// + /// When dealing with virtual whitespace we have 3 major considerations: + /// 1) Is the editor option enabled that allows arbitrary virtual whitespace navigation? + /// 2) Is the current selection a box selection? + /// 3) Are we at the beginning of a line that is impacted by Auto-Indent. + /// + /// This method ignores the 3rd element, since the virtual whitespace added there is not usually based + /// on the previous whitespace, but on the auto-indent. This method is a convienence method that will return + /// whether either of the first two conditions apply, and should be used anywhere arbitrary virtual whitespace + /// is an option. + /// </summary> + public static bool IsVirtualSpaceOrBoxSelectionEnabled(this ITextView textView) + { + return textView.Options.IsVirtualSpaceEnabled() || textView.GetMultiSelectionBroker().IsBoxSelection; + } + + public static bool TryGetClosestTextViewLine(this ITextView textView, double yCoordinate, out ITextViewLine closestLine) + { + if (textView == null) + { + throw new ArgumentNullException(nameof(textView)); + } + + if (textView.IsClosed || textView.InLayout) + { + closestLine = null; + return false; + } + + ITextViewLine textLine = null; + + ITextViewLineCollection textLines = textView.TextViewLines; + + if (textLines != null && textLines.Count > 0) + { + textLine = textLines.GetTextViewLineContainingYCoordinate(yCoordinate); + + if (textLine == null) + { + if (yCoordinate <= textLines.FirstVisibleLine.Bottom) + textLine = textLines.FirstVisibleLine; + else if (yCoordinate >= textLines.LastVisibleLine.Top) + textLine = textLines.LastVisibleLine; + } + + closestLine = textLine; + return true; + } + + closestLine = null; + return false; + } + } +} diff --git a/src/Text/Util/TextUIUtil/SelectionState.cs b/src/Text/Util/TextUIUtil/SelectionState.cs new file mode 100644 index 0000000..0499b88 --- /dev/null +++ b/src/Text/Util/TextUIUtil/SelectionState.cs @@ -0,0 +1,151 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations +{ + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + + public class SelectionState + { + private readonly SingleSelection[] _selections; + private readonly SingleSelection _primary; + private bool _isBox; + + public SelectionState(ITextView view) + { + var selectionBroker = view.GetMultiSelectionBroker(); + var map = SelectionState.EditToDataMap(view); + + if (selectionBroker.IsBoxSelection) + { + _primary = new SingleSelection(map, selectionBroker.BoxSelection); + _isBox = true; + } + else + { + if (selectionBroker.HasMultipleSelections) + { + var selections = selectionBroker.AllSelections; + _selections = new SingleSelection[selections.Count]; + for (int i = 0; (i < _selections.Length); ++i) + { + _selections[i] = new SingleSelection(map, selections[i]); + } + } + + _primary = new SingleSelection(map, selectionBroker.PrimarySelection); + } + } + + public void Restore(ITextView view) + { + var selectionBroker = view.GetMultiSelectionBroker(); + var map = SelectionState.EditToDataMap(view); + + if (_isBox) + { + selectionBroker.SetBoxSelection(_primary.Rehydrate(map, selectionBroker.CurrentSnapshot)); + } + else + { + Selection[] rehydradedSelections = null; + if (_selections != null) + { + rehydradedSelections = new Selection[_selections.Length]; + for (int i = 0; (i < _selections.Length); ++i) + { + rehydradedSelections[i] = _selections[i].Rehydrate(map, selectionBroker.CurrentSnapshot); + } + } + + selectionBroker.SetSelectionRange(rehydradedSelections, _primary.Rehydrate(map, selectionBroker.CurrentSnapshot)); + } + } + + public bool Matches(SelectionState other) + { + if ((_isBox == other._isBox) && _primary.Matches(other._primary)) + { + if (_selections != null) + { + if ((other._selections != null) && (_selections.Length == other._selections.Length)) + { + for (int i = 0; (i < _selections.Length); ++i) + { + if (!_selections[i].Matches(other._selections[i])) + { + return false; + } + } + + return true; + } + } + else if (other._selections == null) + return true; + } + + return false; + } + + public static IMapEditToData EditToDataMap(ITextView view) + { + return (view.TextViewModel.EditBuffer != view.TextViewModel.DataBuffer) && view.Properties.TryGetProperty(typeof(IMapEditToData), out IMapEditToData map) && (map != null) + ? map + : VacuousMapToEdit.Identity; + } + + struct SingleSelection + { + public readonly VirtualPoint Anchor; + public readonly VirtualPoint Active; + public readonly VirtualPoint Insertion; + public readonly PositionAffinity Affinity; + + public SingleSelection(IMapEditToData map, Selection selection) + { + this.Anchor = new VirtualPoint(map, selection.AnchorPoint); + this.Active = new VirtualPoint(map, selection.ActivePoint); + this.Insertion = new VirtualPoint(map, selection.InsertionPoint); + this.Affinity = selection.InsertionPointAffinity; + } + + public Selection Rehydrate(IMapEditToData map, ITextSnapshot snapshot) + { + return new Selection(this.Insertion.Rehydrate(map, snapshot), + this.Anchor.Rehydrate(map, snapshot), + this.Active.Rehydrate(map, snapshot), + this.Affinity); + } + + public bool Matches(SingleSelection other) + { + return this.Anchor.Matches(other.Anchor) && this.Active.Matches(other.Active) && this.Insertion.Matches(other.Insertion) && (this.Affinity == other.Affinity); + } + } + + struct VirtualPoint + { + public readonly int Position; + public readonly int VirtualSpaces; + public VirtualPoint(IMapEditToData map, VirtualSnapshotPoint point) { this.Position = map.MapEditToData(point.Position); this.VirtualSpaces = point.VirtualSpaces; } + + public VirtualSnapshotPoint Rehydrate(IMapEditToData map, ITextSnapshot snapshot) => new VirtualSnapshotPoint(new SnapshotPoint(snapshot, map.MapDataToEdit(this.Position)), this.VirtualSpaces); + + public bool Matches(VirtualPoint other) => (this.Position == other.Position) && (this.VirtualSpaces == other.VirtualSpaces); + } + + private class VacuousMapToEdit : IMapEditToData + { + public static readonly IMapEditToData Identity = new VacuousMapToEdit(); + + public int MapDataToEdit(int dataPoint) => dataPoint; + public int MapEditToData(int editPoint) => editPoint; + } + } +} |