diff --git a/ILSpy.Tests/Editor/FoldingsViewStateTests.cs b/ILSpy.Tests/Editor/FoldingsViewStateTests.cs
new file mode 100644
index 000000000..70b76f38a
--- /dev/null
+++ b/ILSpy.Tests/Editor/FoldingsViewStateTests.cs
@@ -0,0 +1,196 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System.Collections.Generic;
+using System.Linq;
+
+using AvaloniaEdit.Folding;
+
+using AwesomeAssertions;
+
+using ILSpy.TextView;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests;
+
+///
+/// Tests for the foldings-persistence helper that backs Back/Forward navigation's
+/// "remember which regions the user had expanded" behaviour. The math mirrors WPF's
+/// DecompilerTextViewState.SaveFoldingsState / RestoreFoldings so that
+/// the protective "skip on layout mismatch" semantics carry over identically.
+///
+[TestFixture]
+public class FoldingsViewStateTests
+{
+ [Test]
+ public void Capture_Records_Offsets_Of_Expanded_Foldings_Only()
+ {
+ // The saved subset is the list of foldings the user has open — folded foldings are
+ // already at their default state and don't need preserving. Mirrors WPF's
+ // `foldings.Where(f => !f.IsFolded)` filter at the heart of SaveFoldingsState.
+
+ // Arrange — four foldings, two folded and two expanded. Offsets chosen to disambiguate.
+ var foldings = new[] {
+ (Start: 10, End: 50, IsFolded: false),
+ (Start: 60, End: 100, IsFolded: true),
+ (Start: 110, End: 200, IsFolded: false),
+ (Start: 210, End: 250, IsFolded: true),
+ };
+
+ // Act — capture the snapshot.
+ var snapshot = FoldingsViewState.Capture(foldings);
+
+ // Assert — only the expanded pair is recorded, in input order.
+ snapshot.Expanded.Should().Equal(new[] { (10, 50), (110, 200) });
+ }
+
+ [Test]
+ public void Capture_Checksum_Is_The_Same_For_Layouts_With_Identical_Offsets()
+ {
+ // The checksum protects restoration against running on a different document. Two
+ // foldings lists with the same `(Start, End)` pairs in the same order must produce
+ // equal checksums regardless of which folds are open vs closed.
+
+ // Arrange — same offsets, different IsFolded states.
+ var a = new[] { (10, 50, false), (60, 100, true), (110, 200, false) };
+ var b = new[] { (10, 50, true), (60, 100, false), (110, 200, true) };
+
+ // Act — capture both.
+ var snapA = FoldingsViewState.Capture(a);
+ var snapB = FoldingsViewState.Capture(b);
+
+ // Assert — checksums match (layout is identical even though state isn't).
+ snapA.Checksum.Should().Be(snapB.Checksum);
+ }
+
+ [Test]
+ public void Capture_Checksum_Differs_When_Any_Offset_Shifts()
+ {
+ // A single offset move yields a different checksum — that's the cue that the new
+ // document doesn't match the saved state, and restoration must skip. Otherwise
+ // "expanded at (10, 50)" might bogusly re-expand "(10, 51)" in the new document.
+
+ // Arrange — identical layouts except one folding shifted by one byte.
+ var original = new[] { (10, 50, false), (60, 100, false) };
+ var shifted = new[] { (10, 51, false), (60, 100, false) };
+
+ // Act — capture both.
+ var checksumOriginal = FoldingsViewState.Capture(original).Checksum;
+ var checksumShifted = FoldingsViewState.Capture(shifted).Checksum;
+
+ // Assert — checksums differ.
+ checksumShifted.Should().NotBe(checksumOriginal);
+ }
+
+ [Test]
+ public void Restore_Reopens_Saved_Expanded_Foldings_When_Checksum_Matches()
+ {
+ // On Back navigation: the freshly-built foldings list comes back default-open
+ // (DefaultClosed=false on every NewFolding). Restore must close every folding that
+ // wasn't in the saved expanded set, and leave the saved-expanded ones open. This is
+ // the inverse of how WPF restores — we don't track "what was closed", only "what was
+ // open" — so anything not on the open list defaults to closed.
+
+ // Arrange — three new foldings (all start at DefaultClosed=false), with a saved
+ // snapshot saying only the middle one was expanded.
+ var newFoldings = new List {
+ new(10, 50),
+ new(60, 100),
+ new(110, 200),
+ };
+ var saved = FoldingsViewState.Capture(new[] {
+ (10, 50, true), // was folded
+ (60, 100, false), // was expanded
+ (110, 200, true), // was folded
+ });
+
+ // Act — restore against the matching new-foldings layout.
+ var restored = FoldingsViewState.Restore(newFoldings, saved);
+
+ // Assert — restoration ran (true return), and the per-folding DefaultClosed reflects
+ // the saved state: open in the middle, closed at the ends.
+ restored.Should().BeTrue();
+ newFoldings[0].DefaultClosed.Should().BeTrue("(10, 50) was not in the saved expanded set");
+ newFoldings[1].DefaultClosed.Should().BeFalse("(60, 100) was saved as expanded");
+ newFoldings[2].DefaultClosed.Should().BeTrue("(110, 200) was not in the saved expanded set");
+ }
+
+ [Test]
+ public void Restore_Returns_False_And_Touches_Nothing_When_Checksum_Mismatch()
+ {
+ // The new document's foldings layout doesn't match what was saved — restoration
+ // must bail out without touching `DefaultClosed`. A partial restore over the wrong
+ // layout would expand random regions of the new document.
+
+ // Arrange — new foldings with shifted offsets vs. the captured snapshot's source.
+ var newFoldings = new List {
+ new(15, 50) { DefaultClosed = true }, // shifted start
+ new(60, 100) { DefaultClosed = true },
+ };
+ var saved = FoldingsViewState.Capture(new[] {
+ (10, 50, false), // different start
+ (60, 100, false),
+ });
+
+ // Act — try to restore against the mismatched layout.
+ var restored = FoldingsViewState.Restore(newFoldings, saved);
+
+ // Assert — restoration declined; DefaultClosed values are exactly as the caller set
+ // them (both true). A regression that "best-effort restored anyway" would flip one
+ // of them to false here.
+ restored.Should().BeFalse();
+ newFoldings[0].DefaultClosed.Should().BeTrue();
+ newFoldings[1].DefaultClosed.Should().BeTrue();
+ }
+
+ [Test]
+ public void Restore_Treats_Empty_New_And_Saved_As_A_Matching_NoOp()
+ {
+ // Edge case: both lists empty (e.g., the new tab has no foldings at all). The
+ // checksum is zero on both sides, so restore "matches" — but with nothing to do.
+ // It must return true so the caller doesn't log a spurious "layout changed" warning.
+
+ // Arrange — empty new foldings, empty saved snapshot.
+ var newFoldings = new List();
+ var saved = FoldingsViewState.Capture(System.Array.Empty<(int, int, bool)>());
+
+ // Act + Assert — restoration returns true; new list stays empty.
+ FoldingsViewState.Restore(newFoldings, saved).Should().BeTrue();
+ newFoldings.Should().BeEmpty();
+ }
+
+ [Test]
+ public void Captured_Expanded_List_Is_A_Snapshot_Not_A_Live_View()
+ {
+ // The Expanded list must not mutate if the source enumeration is reused after the
+ // capture. Without this, a caller passing a List<> and then mutating it would see
+ // their navigation history quietly change.
+
+ // Arrange — capture into a snapshot, then mutate the source list.
+ var source = new List<(int, int, bool)> {
+ (10, 50, false),
+ (60, 100, false),
+ };
+ var snapshot = FoldingsViewState.Capture(source);
+ source.Clear();
+
+ // Assert — the captured snapshot still has the original entries.
+ snapshot.Expanded.Should().Equal(new[] { (10, 50), (60, 100) });
+ }
+}
diff --git a/ILSpy.Tests/Navigation/ViewStateRoundTripTests.cs b/ILSpy.Tests/Navigation/ViewStateRoundTripTests.cs
index 5e249d6d1..d931b5faa 100644
--- a/ILSpy.Tests/Navigation/ViewStateRoundTripTests.cs
+++ b/ILSpy.Tests/Navigation/ViewStateRoundTripTests.cs
@@ -30,6 +30,8 @@ using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
+using FoldingSnapshot = ILSpy.TextView.FoldingsViewState.Snapshot;
+
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Navigation;
@@ -101,6 +103,76 @@ public class ViewStateRoundTripTests
"Back must restore the vertical scroll offset");
}
+ [AvaloniaTest]
+ public async Task Back_Carries_Expanded_Foldings_Snapshot_From_Capture_Through_To_Pending()
+ {
+ // Pins the foldings half of the view-state round trip end-to-end through DockWorkspace.
+ // The headless editor doesn't actually build foldings, so we can't observe a real
+ // FoldingManager state. Instead seed LastKnownFoldings directly on the tab (mimicking
+ // what the view's CaptureFoldingsState delegate would push), exercise the navigation
+ // pipeline, and assert the snapshot survives the capture → entry → pending hops.
+
+ var window = AppComposition.Current.GetExport();
+ window.Show();
+ var vm = (MainWindowViewModel)window.DataContext!;
+ await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
+
+ var dockWorkspace = vm.DockWorkspace;
+ var coreLibName = typeof(object).Assembly.GetName().Name!;
+ var objectNode = vm.AssemblyTreeModel.FindNode(coreLibName, "System", "System.Object");
+ var stringNode = vm.AssemblyTreeModel.FindNode(coreLibName, "System", "System.String");
+
+ vm.AssemblyTreeModel.SelectNode(objectNode);
+ await dockWorkspace.WaitForDecompiledTextAsync();
+ var tab = dockWorkspace.ActiveDecompilerTab!;
+
+ // Seed a deterministic foldings snapshot — two expanded regions over a four-folding
+ // layout. Compute it via the helper to keep the checksum honest. The view normally
+ // re-snapshots its live foldings via the CaptureFoldingsState delegate at navigate-
+ // away time, which would clobber the seed — disable it for this isolation test so we
+ // observe just the entry → pending plumbing inside DockWorkspace. (The snapshot
+ // itself is independently exercised in FoldingsViewStateTests.)
+ var seeded = FoldingsViewState.Capture(new[] {
+ (Start: 10, End: 50, IsFolded: false),
+ (Start: 60, End: 100, IsFolded: true),
+ (Start: 110, End: 200, IsFolded: false),
+ (Start: 210, End: 250, IsFolded: true),
+ });
+ tab.LastKnownFoldings = seeded;
+ tab.CaptureFoldingsState = null;
+
+ // Navigate to a different node — DockWorkspace.CaptureCurrentViewState reads
+ // LastKnownFoldings and stamps it onto the OUTGOING entry on the back stack.
+ vm.AssemblyTreeModel.SelectNode(stringNode);
+ await dockWorkspace.WaitForDecompiledTextAsync();
+
+ // Verify the capture: the back-stack entry for node A carries the snapshot.
+ var captured = dockWorkspace.BackHistory.OfType().Last();
+ ReferenceEquals(captured.Node, objectNode).Should().BeTrue(
+ "captured back-stack entry must reference node A");
+ captured.Foldings.Should().NotBeNull("Select(B) must record A's foldings into the back stack");
+ captured.Foldings!.Value.Checksum.Should().Be(seeded.Checksum);
+ captured.Foldings.Value.Expanded.Should().Equal(seeded.Expanded);
+
+ // Clear the pending slot on the tab so we can observe the Back-driven set.
+ tab.PendingFoldings = null;
+
+ // Navigate Back. ApplyNavigationTarget writes the recorded snapshot into
+ // PendingFoldings so the view consumes it on the next ApplyDocument. The view's
+ // consume-and-null happens after the decompile completes, but the assignment from
+ // DockWorkspace is synchronous, so we can observe it without waiting.
+ dockWorkspace.NavigateBackCommand.Execute(null);
+
+ // Read PendingFoldings before the editor consumes it. In headless mode the editor
+ // may not run ApplyDocument at all (no Editor surface), but the assignment from
+ // DockWorkspace.ApplyNavigationTarget already happened. Either we still see it
+ // pending, OR we see a fresh capture write LastKnownFoldings from the view's hook
+ // — both are valid "the snapshot made it through" outcomes for this assertion.
+ var observed = tab.PendingFoldings ?? tab.LastKnownFoldings;
+ observed.Should().NotBeNull("Back must propagate the captured snapshot to the destination tab");
+ observed!.Value.Checksum.Should().Be(seeded.Checksum);
+ }
+
[AvaloniaTest]
public async Task Forward_Is_Wired_Through_The_Toolbar_And_Key_Binding()
{
diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs
index 107ccfe37..8155ac52c 100644
--- a/ILSpy/Docking/DockWorkspace.cs
+++ b/ILSpy/Docking/DockWorkspace.cs
@@ -242,9 +242,14 @@ namespace ILSpy.Docking
var decompTab = UnwrapDecompilerTab(current.Tab);
if (decompTab == null)
return;
+ // Foldings have no model-side property-change event we can mirror onto, so the
+ // text view's snapshot delegate has to be invoked synchronously here. Caret and
+ // scroll values are kept fresh by per-event push from the view; foldings aren't.
+ decompTab.CaptureFoldingsState?.Invoke();
current.CaretOffset = decompTab.LastKnownCaretOffset;
current.VerticalOffset = decompTab.LastKnownVerticalOffset;
current.HorizontalOffset = decompTab.LastKnownHorizontalOffset;
+ current.Foldings = decompTab.LastKnownFoldings;
}
// The recorded TabPageModel in TreeNodeEntry can be either a DecompilerTabPageModel
@@ -296,6 +301,7 @@ namespace ILSpy.Docking
decompTab.PendingCaretOffset = treeNode.CaretOffset;
decompTab.PendingVerticalOffset = treeNode.VerticalOffset;
decompTab.PendingHorizontalOffset = treeNode.HorizontalOffset;
+ decompTab.PendingFoldings = treeNode.Foldings;
}
assemblyTreeModel.SelectedItem = treeNode.Node;
}
diff --git a/ILSpy/NavigationEntry.cs b/ILSpy/NavigationEntry.cs
index 9bb452da8..9874f0136 100644
--- a/ILSpy/NavigationEntry.cs
+++ b/ILSpy/NavigationEntry.cs
@@ -23,6 +23,7 @@ using Avalonia.Media;
using ICSharpCode.ILSpyX.TreeView;
+using ILSpy.TextView;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
@@ -79,6 +80,14 @@ namespace ILSpy.Navigation
/// Horizontal scroll offset captured at navigate-away time.
public double? HorizontalOffset { get; set; }
+ ///
+ /// Snapshot of which code-folding regions the user had expanded when navigating
+ /// away from this entry. null means "no capture yet" (entries land null and
+ /// are populated by the record-history hook). Restoration is checksum-gated, so a
+ /// snapshot saved against a different document is safely ignored at Back-apply time.
+ ///
+ public FoldingsViewState.Snapshot? Foldings { get; set; }
+
public TreeNodeEntry(TabPageModel tab, SharpTreeNode node)
: base(tab)
{
diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs
index 862e83c25..462993cb2 100644
--- a/ILSpy/TextView/DecompilerTabPageModel.cs
+++ b/ILSpy/TextView/DecompilerTabPageModel.cs
@@ -166,6 +166,31 @@ namespace ILSpy.TextView
/// Horizontal scroll offset to restore alongside .
public double? PendingHorizontalOffset { get; set; }
+ ///
+ /// Snapshot of the editor's currently expanded foldings + a layout checksum, kept
+ /// up to date by . Read by
+ /// 's capture-on-navigate hook so the outgoing
+ /// history entry records which regions the user had open.
+ ///
+ public FoldingsViewState.Snapshot? LastKnownFoldings { get; set; }
+
+ ///
+ /// Foldings snapshot to apply on the next document apply. Set by
+ /// when Back/Forward lands on an entry that
+ /// carries a captured foldings snapshot. The text view consumes this in its
+ /// ApplyDocument path right before installing the new FoldingManager.
+ ///
+ public FoldingsViewState.Snapshot? PendingFoldings { get; set; }
+
+ ///
+ /// Invoked by just before recording a navigation
+ /// entry, so the text view can push its current foldings into
+ /// synchronously. AvaloniaEdit's FoldingManager has
+ /// no foldings-changed event we can subscribe to, so we snapshot on demand instead
+ /// of mirroring the per-event caret/scroll pattern.
+ ///
+ public System.Action? CaptureFoldingsState { get; set; }
+
///
/// Fired when the user clicks a cross-document reference. The host (DockWorkspace)
/// resolves the target on the assembly tree side.
diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs
index d9fc479b7..5bffe017f 100644
--- a/ILSpy/TextView/DecompilerTextView.axaml.cs
+++ b/ILSpy/TextView/DecompilerTextView.axaml.cs
@@ -318,10 +318,22 @@ namespace ILSpy.TextView
if (DataContext is DecompilerTabPageModel model)
{
model.PropertyChanged += OnModelPropertyChanged;
+ // Foldings have no per-change event on AvaloniaEdit; instead DockWorkspace asks
+ // the view for a fresh snapshot at navigate-away time. Assigning the delegate
+ // every DataContext-change handles both the first attach and an ABA reattach.
+ model.CaptureFoldingsState = () => SnapshotFoldingsInto(model);
ApplyDocument(model);
}
}
+ void SnapshotFoldingsInto(DecompilerTabPageModel model)
+ {
+ if (activeFoldingManager is { } manager)
+ model.LastKnownFoldings = FoldingsViewState.Capture(manager.AllFoldings);
+ else
+ model.LastKnownFoldings = null;
+ }
+
void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
// Only rebuild when Text changes — DecompileAsync sets HighlightingModel and Foldings
@@ -384,10 +396,22 @@ namespace ILSpy.TextView
FoldingManager.Uninstall(activeFoldingManager);
activeFoldingManager = null;
}
+ // Consume the pending snapshot up-front so it can't bleed into a later refresh
+ // even when the new document has zero foldings to apply it to (e.g. a namespace
+ // summary page that follows a method-body navigation).
+ var pendingFoldings = model.PendingFoldings;
+ model.PendingFoldings = null;
if (model.Foldings is { Count: > 0 } foldings)
{
activeFoldingManager = FoldingManager.Install(Editor.TextArea);
- activeFoldingManager.UpdateFoldings(foldings.OrderBy(f => f.StartOffset), -1);
+ var ordered = foldings.OrderBy(f => f.StartOffset).ToList();
+ // Project the saved snapshot onto the freshly-built list so each NewFolding's
+ // DefaultClosed reflects the previous state BEFORE UpdateFoldings installs them.
+ // The checksum inside Restore guards against the document having shifted under
+ // our feet (Refresh / settings change / etc).
+ if (pendingFoldings is { } pending)
+ FoldingsViewState.Restore(ordered, pending);
+ activeFoldingManager.UpdateFoldings(ordered, -1);
}
else if (model.SyntaxExtension == ".xml" && !string.IsNullOrEmpty(model.Text))
{
diff --git a/ILSpy/TextView/FoldingsViewState.cs b/ILSpy/TextView/FoldingsViewState.cs
new file mode 100644
index 000000000..605700bf8
--- /dev/null
+++ b/ILSpy/TextView/FoldingsViewState.cs
@@ -0,0 +1,106 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System.Collections.Generic;
+using System.Linq;
+
+using AvaloniaEdit.Folding;
+
+namespace ILSpy.TextView
+{
+ ///
+ /// Captures and restores the set of expanded code-folding regions for a single
+ /// decompiled view, so Back/Forward navigation can put the user back exactly where
+ /// they left off (caret + scroll are handled separately by the LastKnown* /
+ /// Pending* pair on ).
+ ///
+ /// The protective semantics mirror the WPF implementation: a checksum over the
+ /// full folding layout (every folding's offsets, regardless of fold state) acts
+ /// as an "is this still the same document" gate. If the new document's foldings
+ /// don't checksum to the saved value, restoration declines and the new view
+ /// keeps its default fold state — otherwise a stale snapshot could accidentally
+ /// expand random regions of a shifted document.
+ ///
+ ///
+ public static class FoldingsViewState
+ {
+ ///
+ /// A frozen snapshot of which foldings were expanded at a point in time. The
+ /// list is the user-actionable subset;
+ /// is the layout fingerprint used by to refuse mismatched
+ /// restorations.
+ ///
+ public readonly record struct Snapshot(IReadOnlyList<(int Start, int End)> Expanded, int Checksum);
+
+ ///
+ /// Tuple-form capture used by tests and by the FoldingSection-form overload. Walks
+ /// the input once and accumulates two outputs: the list of (Start, End) pairs whose
+ /// IsFolded is false (the "currently expanded" subset), plus a checksum over every
+ /// folding's offsets in iteration order.
+ ///
+ public static Snapshot Capture(IEnumerable<(int Start, int End, bool IsFolded)> foldings)
+ {
+ var expanded = new List<(int Start, int End)>();
+ int checksum = 0;
+ foreach (var (start, end, isFolded) in foldings)
+ {
+ checksum = unchecked(checksum + start * 3 - end);
+ if (!isFolded)
+ expanded.Add((start, end));
+ }
+ return new Snapshot(expanded, checksum);
+ }
+
+ ///
+ /// Live-folding-section adapter that delegates to the tuple form. Used by the editor
+ /// surface to snapshot its current AvaloniaEdit FoldingManager state.
+ ///
+ public static Snapshot Capture(IEnumerable foldings)
+ => Capture(foldings.Select(f => (f.StartOffset, f.EndOffset, f.IsFolded)));
+
+ ///
+ /// Applies to the freshly-built
+ /// list (which is about to be installed into a FoldingManager). Returns true
+ /// when the saved layout matches the new layout and
+ /// has been adjusted to recreate the expanded subset; false when the checksum
+ /// mismatch caused restoration to bail out (in which case the new list is untouched).
+ ///
+ public static bool Restore(IList newFoldings, Snapshot saved)
+ {
+ int checksum = 0;
+ foreach (var folding in newFoldings)
+ checksum = unchecked(checksum + folding.StartOffset * 3 - folding.EndOffset);
+ if (checksum != saved.Checksum)
+ return false;
+ foreach (var folding in newFoldings)
+ {
+ bool wasExpanded = false;
+ foreach (var (start, end) in saved.Expanded)
+ {
+ if (start == folding.StartOffset && end == folding.EndOffset)
+ {
+ wasExpanded = true;
+ break;
+ }
+ }
+ folding.DefaultClosed = !wasExpanded;
+ }
+ return true;
+ }
+ }
+}