diff options
author | Sandy Armstrong <sandy@xamarin.com> | 2019-06-21 01:20:20 +0300 |
---|---|---|
committer | Sandy Armstrong <sandy@xamarin.com> | 2019-06-24 17:08:06 +0300 |
commit | 914e1cd7bda1f0ca497f8c4c527aa948686b2a86 (patch) | |
tree | c45ddf481b20a935f39592a09ee203df97b9939d | |
parent | 556c2b6ac5dbf978d52a2d4750cf760f21457407 (diff) |
Sync with vsec for fold commandspr-sandy-fold-commands
3 files changed, 472 insertions, 0 deletions
diff --git a/src/Editor/Text/Def/TextUI/Commanding/CommandSelector.cs b/src/Editor/Text/Def/TextUI/Commanding/CommandSelector.cs index d53e551..7e8ebd7 100644 --- a/src/Editor/Text/Def/TextUI/Commanding/CommandSelector.cs +++ b/src/Editor/Text/Def/TextUI/Commanding/CommandSelector.cs @@ -275,6 +275,14 @@ namespace Microsoft.VisualStudio.Text.Editor.Commanding /// <summary>⌘L</summary> public const string GoToLine = "goToLine:"; + public const string ToggleOutliningEnabled = "toggleOutliningEnabled:"; + + public const string ToggleOutliningExpansion = "toggleOutliningExpansion:"; + + public const string ToggleAllOutlining = "toggleAllOutlining:"; + + public const string ToggleOutliningDefinitions = "toggleOutliningDefinitions:"; + #endregion } diff --git a/src/Editor/Text/Def/TextUI/Commanding/Commands/OutliningCommandArgs.cs b/src/Editor/Text/Def/TextUI/Commanding/Commands/OutliningCommandArgs.cs new file mode 100644 index 0000000..47631e2 --- /dev/null +++ b/src/Editor/Text/Def/TextUI/Commanding/Commands/OutliningCommandArgs.cs @@ -0,0 +1,30 @@ +namespace Microsoft.VisualStudio.Text.Editor.Commanding.Commands +{ + public sealed class ToggleOutliningExpansionCommandArgs : EditorCommandArgs + { + public ToggleOutliningExpansionCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : base(textView, subjectBuffer) + { + } + } + + public sealed class ToggleAllOutliningCommandArgs : EditorCommandArgs + { + public ToggleAllOutliningCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : base(textView, subjectBuffer) + { + } + } + + public sealed class ToggleOutliningDefinitionsCommandArgs : EditorCommandArgs + { + public ToggleOutliningDefinitionsCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : base(textView, subjectBuffer) + { + } + } + + public sealed class ToggleOutliningEnabledCommandArgs : EditorCommandArgs + { + public ToggleOutliningEnabledCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : base(textView, subjectBuffer) + { + } + } +}
\ No newline at end of file diff --git a/src/Editor/Text/Impl/Outlining/OutliningCommandHandler.cs b/src/Editor/Text/Impl/Outlining/OutliningCommandHandler.cs new file mode 100644 index 0000000..7ccb571 --- /dev/null +++ b/src/Editor/Text/Impl/Outlining/OutliningCommandHandler.cs @@ -0,0 +1,434 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; + +using Microsoft.VisualStudio.Commanding; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Text.Outlining.Implementation +{ + /// <summary> + /// Commands for collapsing and expanding text + /// </summary> + [Name(nameof(OutliningCommandHandler))] + [ContentType("text")] + [Export(typeof(ICommandHandler))] + internal sealed class OutliningCommandHandler : + ICommandHandler<ToggleOutliningExpansionCommandArgs>, + ICommandHandler<ToggleAllOutliningCommandArgs>, + ICommandHandler<ToggleOutliningDefinitionsCommandArgs>, + ICommandHandler<ToggleOutliningEnabledCommandArgs> + { + [Import] + IOutliningManagerService outliningManagerService; + + string INamed.DisplayName => nameof(OutliningCommandHandler); + + bool ICommandHandler<ToggleOutliningExpansionCommandArgs>.ExecuteCommand(ToggleOutliningExpansionCommandArgs args, CommandExecutionContext executionContext) + { + var outliningManager = outliningManagerService.GetOutliningManager(args.TextView); + if (outliningManager == null || !outliningManager.Enabled) + { + return false; + } + + var collapsibles = GetCollapsiblesForCurrentSelection(outliningManager, args.TextView); + if ((collapsibles != null) && (collapsibles.Any())) + { + var firstCollapsible = collapsibles.First(); + var lastCollapsible = collapsibles.Last(); + if ((firstCollapsible != null) && (lastCollapsible != null)) + { + var snapshot = args.TextView.TextSnapshot; + var sortedCollapsedMatcher = new SortedCollapsedMatcher(collapsibles.Select((collapsible) => collapsible.Extent), snapshot); + var firstSnapshotSpan = firstCollapsible.Extent.GetSpan(snapshot); + var lastSnapshotSpan = ((lastCollapsible == firstCollapsible) ? firstSnapshotSpan : lastCollapsible.Extent.GetSpan(snapshot)); + var containingSnapshotSpan = new SnapshotSpan(firstSnapshotSpan.Start, lastSnapshotSpan.End); + + if (ShouldExpandToggledCollapsibles(collapsibles)) + { + outliningManager.ExpandAll(containingSnapshotSpan, sortedCollapsedMatcher.Match); + } + else + { + outliningManager.CollapseAll(containingSnapshotSpan, sortedCollapsedMatcher.Match); + } + + // Ensure caret visible + args.TextView.Caret.EnsureVisible(); + } + } + + return true; + } + + bool ICommandHandler<ToggleAllOutliningCommandArgs>.ExecuteCommand(ToggleAllOutliningCommandArgs args, CommandExecutionContext executionContext) + { + var outliningManager = outliningManagerService.GetOutliningManager(args.TextView); + if (outliningManager == null || !outliningManager.Enabled) + { + return false; + } + + var snapshot = args.TextView.TextBuffer.CurrentSnapshot; + var snapshotSpan = new SnapshotSpan(snapshot, 0, snapshot.Length); + + if (outliningManager.GetCollapsedRegions(snapshotSpan).Any()) + { + outliningManager.ExpandAll(snapshotSpan, collapsed => true); + } + else + { + outliningManager.CollapseAll(snapshotSpan, collapsed => true); + } + + args.TextView.Caret.EnsureVisible(); + + return true; + } + + bool ICommandHandler<ToggleOutliningDefinitionsCommandArgs>.ExecuteCommand(ToggleOutliningDefinitionsCommandArgs args, CommandExecutionContext executionContext) + { + // NOTE: For VSwin, this isn't something you toggle. The single related command is "collapse to definitions". + // It expands everthing then collapses all implementation blocks. + // VSmac's old editor lets you toggle, which is a little less clear-cut when other folding has occurred. + + var outliningManager = outliningManagerService.GetOutliningManager(args.TextView) as IAccurateOutliningManager; + if (outliningManager == null) + { + return false; + } + + // Expand non-implementation + var snapshot = args.TextView.TextBuffer.CurrentSnapshot; + var snapshotSpan = new SnapshotSpan(snapshot, 0, snapshot.Length); + + // If any definitions are already collapsed, we will be expanding + var expandDefinitions = outliningManager.GetCollapsedRegions(snapshotSpan).Any(r => r.Tag.IsImplementation); + + outliningManager.ExpandAll(snapshotSpan, (collapsible => !collapsible.Tag.IsImplementation)); + + if (expandDefinitions) + { + outliningManager.ExpandAll(snapshotSpan, (collapsible => collapsible.Tag.IsImplementation)); + } + else + { + outliningManager.CollapseAll(snapshotSpan, (collapsible => collapsible.Tag.IsImplementation)); + } + + args.TextView.Caret.EnsureVisible(); + + return true; + } + + bool ICommandHandler<ToggleOutliningEnabledCommandArgs>.ExecuteCommand(ToggleOutliningEnabledCommandArgs args, CommandExecutionContext executionContext) + { + var outliningManager = outliningManagerService.GetOutliningManager(args.TextView); + if (outliningManager == null) + { + return false; + } + + outliningManager.Enabled = !outliningManager.Enabled; + + // TODO: What about adhoc regions? (Windows has special logic here, but Mac does not have AdhocOutliner) + + return true; + } + + CommandState ICommandHandler<ToggleOutliningExpansionCommandArgs>.GetCommandState(ToggleOutliningExpansionCommandArgs args) + { + var outliningManager = outliningManagerService.GetOutliningManager(args.TextView); + if (outliningManager != null && outliningManager.Enabled) + { + var collapsibles = GetCollapsiblesForCurrentSelection(outliningManager, args.TextView); + if (collapsibles != null && collapsibles.Any()) + { + return CommandState.Available; + } + } + + return CommandState.Unavailable; + } + + CommandState ICommandHandler<ToggleAllOutliningCommandArgs>.GetCommandState(ToggleAllOutliningCommandArgs args) + { + return outliningManagerService.GetOutliningManager(args.TextView) is IOutliningManager mgr && mgr.Enabled + ? CommandState.Available + : CommandState.Unavailable; + } + + CommandState ICommandHandler<ToggleOutliningDefinitionsCommandArgs>.GetCommandState(ToggleOutliningDefinitionsCommandArgs args) + { + return outliningManagerService.GetOutliningManager(args.TextView) is IAccurateOutliningManager mgr && mgr.Enabled + ? CommandState.Available + : CommandState.Unavailable; + } + + CommandState ICommandHandler<ToggleOutliningEnabledCommandArgs>.GetCommandState(ToggleOutliningEnabledCommandArgs args) + { + return CommandState.Available; + } + + internal IEnumerable<ICollapsible> GetCollapsiblesForCurrentSelection(IOutliningManager outliningManager, ITextView textView) + { + if (outliningManager == null) + { + return null; + } + + // Try for all collapsibles which are contained within the selection span + SnapshotSpan selectionSnapshotSpan = textView.Selection.StreamSelectionSpan.SnapshotSpan; + ITextSnapshot selectionSnapshot = selectionSnapshotSpan.Snapshot; + var intersectingCollapsibles = outliningManager.GetAllRegions(selectionSnapshotSpan); + var filteredCollapsibles = intersectingCollapsibles.Where(collapsible => selectionSnapshotSpan.Contains(collapsible.Extent.GetSpan(selectionSnapshot))); + var tornoffCollapsibles = GetTornoffCollapsibles(filteredCollapsibles); + if (tornoffCollapsibles != null) + { + return tornoffCollapsibles; + } + + // Try for innermost collapsibles which intersect and start within the selection span + filteredCollapsibles = intersectingCollapsibles.Where(collapsible => selectionSnapshotSpan.Contains(collapsible.Extent.GetSpan(selectionSnapshot).Start)); + tornoffCollapsibles = GetInnermostCollapsibles(selectionSnapshot, filteredCollapsibles); + if (tornoffCollapsibles != null) + { + return tornoffCollapsibles; + } + + // Try for all collapsibles which are contained within the selection's first line + ITextSnapshotLine selectionStartLine = selectionSnapshot.GetLineFromPosition(selectionSnapshotSpan.Start); + SnapshotSpan selectionFirstLine = new SnapshotSpan(selectionSnapshot, selectionStartLine.Start, selectionStartLine.Length); + intersectingCollapsibles = outliningManager.GetAllRegions(selectionFirstLine); + filteredCollapsibles = intersectingCollapsibles.Where(collapsible => selectionFirstLine.Contains(collapsible.Extent.GetSpan(selectionSnapshot))); + tornoffCollapsibles = GetTornoffCollapsibles(filteredCollapsibles); + if (tornoffCollapsibles != null) + { + return tornoffCollapsibles; + } + + // Try for innermost collapsibles which intersect and start within the selection's first line + filteredCollapsibles = intersectingCollapsibles.Where(collapsible => selectionFirstLine.Contains(collapsible.Extent.GetSpan(selectionSnapshot).Start)); + tornoffCollapsibles = GetInnermostCollapsibles(selectionSnapshot, filteredCollapsibles); + if (tornoffCollapsibles != null) + { + return tornoffCollapsibles; + } + + // Try for innermost collapsibles which simply intersect the selection's first line + tornoffCollapsibles = GetInnermostCollapsibles(selectionSnapshot, intersectingCollapsibles); + if (tornoffCollapsibles != null) + { + return tornoffCollapsibles; + } + + return null; + } + + internal static IEnumerable<ICollapsible> GetTornoffCollapsibles(IEnumerable<ICollapsible> collapsibles) + { + List<ICollapsible> tornoffCollapsibles = null; + + if (collapsibles != null) + { + foreach (ICollapsible collapsible in collapsibles) + { + AddCollapsible(ref tornoffCollapsibles, collapsible); + } + } + + return tornoffCollapsibles; + } + + internal static IEnumerable<ICollapsible> GetInnermostCollapsibles(ITextSnapshot snapshot, IEnumerable<ICollapsible> collapsibles) + { + if (snapshot == null) + { + throw new ArgumentNullException(nameof(snapshot)); + } + + List<ICollapsible> innermostCollapsibles = null; + + if (collapsibles != null) + { + // OutliningManager returns collapsibles "sorted" such that contained follow containing and preceed not contained + // Include collapsibles which have no following collapsibles which are contained within them + ICollapsible previousCollapsible = null; + foreach (ICollapsible collapsible in collapsibles) + { + if (previousCollapsible != null) + { + if (!previousCollapsible.Extent.GetSpan(snapshot).Contains(collapsible.Extent.GetSpan(snapshot))) + { + AddCollapsible(ref innermostCollapsibles, previousCollapsible); + } + } + previousCollapsible = collapsible; + } + if (previousCollapsible != null) + { + AddCollapsible(ref innermostCollapsibles, previousCollapsible); + } + } + + return innermostCollapsibles; + } + + internal static void AddCollapsible(ref List<ICollapsible> collapsibles, ICollapsible collapsible) + { + if (collapsibles == null) + { + collapsibles = new List<ICollapsible>(); + } + collapsibles.Add(collapsible); + } + + internal static bool ShouldExpandToggledCollapsibles(IEnumerable<ICollapsible> collapsibles) + { + if (collapsibles != null) + { + // Determine whether homogenous collapse state for given collapsibles + bool homogenousCollapseState = true; + ICollapsible previousCollapsible = null; + foreach (ICollapsible collapsible in collapsibles) + { + if (previousCollapsible != null) + { + if (previousCollapsible.IsCollapsed != collapsible.IsCollapsed) + { + homogenousCollapseState = false; + break; + } + } + previousCollapsible = collapsible; + } + + // Check whether non empty collection of collapsibles + if (previousCollapsible != null) + { + // Check whether homogenous collapse state and collapsed + if (homogenousCollapseState && previousCollapsible.IsCollapsed) + { + // Should expand collapsibles + return true; + } + else + { + // Should collapse collapsibles + return false; + } + } + } + + return false; + } + + internal static bool AreExpandable(IEnumerable<ICollapsible> collapsibles) + { + if (collapsibles != null) + { + foreach (ICollapsible collapsible in collapsibles) + { + if (collapsible.IsCollapsed) + { + return true; + } + } + } + + return false; + } + + internal bool AreCollapsible(IEnumerable<ICollapsible> collapsibles, ITextSnapshot snapshot) + { + if (collapsibles != null) + { + // OutliningManager returns collapsibles "sorted" such that contained follow containing and preceed not contained + // Return true if find uncollapsed not nested within collapsed + ICollapsible previousCollapsed = null; + foreach (ICollapsible collapsible in collapsibles) + { + bool nestedInPreviousCollapsed = ((previousCollapsed != null) && previousCollapsed.Extent.GetSpan(snapshot).Contains(collapsible.Extent.GetSpan(snapshot))); + if (!nestedInPreviousCollapsed) + { + if (!collapsible.IsCollapsed) + { + return true; + } + previousCollapsed = collapsible; + } + } + } + + return false; + } + + internal class SortedCollapsibleMatcher + { + private IEnumerator<ITrackingSpan> _trackingSpanEnum; + private ITextSnapshot _textSnapshot; + private ITrackingSpan _currentTrackingSpan; + internal bool MatchReturn { get; set; } + + internal SortedCollapsibleMatcher(IEnumerable<ITrackingSpan> trackingSpans, ITextSnapshot textSnapshot) + { + _trackingSpanEnum = ((trackingSpans != null) ? trackingSpans.GetEnumerator() : null); + _textSnapshot = textSnapshot; + if ((_trackingSpanEnum != null) && (_textSnapshot != null)) + { + _currentTrackingSpan = (_trackingSpanEnum.MoveNext() ? _trackingSpanEnum.Current : null); + } + MatchReturn = true; + } + internal bool Match(ICollapsible givenCollapsible) + { + if (givenCollapsible != null) + { + while (_currentTrackingSpan != null) + { + SnapshotSpan currentSnapshotSpan = _currentTrackingSpan.GetSpan(_textSnapshot); + SnapshotSpan givenSnapshotSpan = givenCollapsible.Extent.GetSpan(_textSnapshot); + int comparison = ((currentSnapshotSpan.Start != givenSnapshotSpan.Start) ? + currentSnapshotSpan.Start.CompareTo(givenSnapshotSpan.Start) : + -currentSnapshotSpan.Length.CompareTo(givenSnapshotSpan.Length)); + if (comparison <= 0) + { + // Advance current + _currentTrackingSpan = (_trackingSpanEnum.MoveNext() ? _trackingSpanEnum.Current : null); + + if (comparison == 0) + { + // Match + return MatchReturn; + } + + // Continue loop to try updated current + } + else + { + // Break loop to advance given + break; + } + } + } + return !MatchReturn; + } + } + + internal class SortedCollapsedMatcher : SortedCollapsibleMatcher + { + internal SortedCollapsedMatcher(IEnumerable<ITrackingSpan> trackingSpans, ITextSnapshot textSnapshot) : + base(trackingSpans, textSnapshot) + { + } + internal bool Match(ICollapsed givenCollapsed) + { + return base.Match(givenCollapsed); + } + } + } +} |