diff --git a/src/CosmosDbExplorer.Core/Helpers/FoldFinder.cs b/src/CosmosDbExplorer.Core/Helpers/FoldFinder.cs
new file mode 100644
index 0000000..58153a5
--- /dev/null
+++ b/src/CosmosDbExplorer.Core/Helpers/FoldFinder.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace CosmosDbExplorer.Core.Helpers
+{
+ ///
+ /// Based on Daniel Gimendez's answer for "Code folding in RichTextBox". Altered to fit our use case
+ /// Source: https://stackoverflow.com/questions/18156451/code-folding-in-richtextbox
+ ///
+ public class FoldFinder
+ {
+ private readonly bool _foldableRoot;
+ private readonly IList _delimiters;
+ private readonly Regex _scanner;
+
+ public FoldFinder(IList delimiters, bool foldableRoot = true)
+ {
+ _foldableRoot = foldableRoot;
+ _delimiters = delimiters;
+ _scanner = RegexifyDelimiters(delimiters);
+ }
+
+ public int FirstErrorOffset { get; private set; } = -1;
+
+ public IList Scan(string code, int start = 0, int end = -1, bool throwOnError = true)
+ {
+ FirstErrorOffset = -1;
+
+ var positions = new List();
+ var stack = new Stack();
+
+ int regexGroupIndex;
+ bool isStartToken;
+ Delimiter matchedDelimiter;
+ var currentItem = default(FoldStackItem);
+
+ foreach (Match match in _scanner.Matches(code, start))
+ {
+ if (!_foldableRoot && match.Index == 0)
+ {
+ continue;
+ }
+
+ // the pattern for every group is that 0 corresponds to SectionDelimter, 1 corresponds to Start
+ // and 2 corresponds to End.
+ regexGroupIndex = match.Groups.Cast().Select((g, i) => new { g.Success, Index = i })
+ .Where(r => r.Success && r.Index > 0).First().Index;
+
+ matchedDelimiter = _delimiters[(regexGroupIndex - 1) / 3];
+ isStartToken = match.Groups[regexGroupIndex + 1].Success;
+
+ if (isStartToken)
+ {
+ currentItem = new FoldStackItem()
+ {
+ Delimter = matchedDelimiter,
+ Position = new FoldMatch() { Start = match.Index }
+ };
+ stack.Push(currentItem);
+ }
+ else
+ {
+ if (stack.Count == 0)
+ {
+ if (FirstErrorOffset == -1)
+ {
+ FirstErrorOffset = match.Index;
+ }
+
+ if (throwOnError)
+ {
+ throw new Exception(string.Format("Unexpected end of string at {0}", match.Index));
+ }
+
+ break;
+ }
+
+ currentItem = stack.Pop();
+ if (currentItem.Delimter == matchedDelimiter)
+ {
+ currentItem.Position.End = match.Index + match.Length;
+ //Only add folding if it spans more than 2 lines
+ if (HasAtLeastNLines(code[currentItem.Position.Start..currentItem.Position.End], 2))
+ {
+ positions.Add(currentItem.Position);
+ }
+
+ // if searching for an end, and we've passed it, and the stack is empty then quit.
+ if (end > -1 && currentItem.Position.End >= end && stack.Count == 0)
+ {
+ break;
+ }
+ }
+ else
+ {
+ if (FirstErrorOffset == -1)
+ {
+ FirstErrorOffset = match.Index;
+ }
+
+ if (throwOnError)
+ {
+ throw new Exception(string.Format("Invalid Ending Token at {0}", match.Index));
+ }
+
+ break;
+ }
+ }
+ }
+
+ if (stack.Count > 0 && throwOnError)
+ {
+ if (FirstErrorOffset == -1)
+ {
+ FirstErrorOffset = code.Length;
+ }
+
+ throw new Exception("Not enough closing symbols.");
+ }
+
+ return positions;
+ }
+
+ private static bool HasAtLeastNLines(string search, int n = 1)
+ {
+ int count = 0, index = 0;
+ if (n <= 1)
+ {
+ return (search?.Length ?? 0) > 0;
+ }
+
+ while ((index = search.IndexOf("\r\n", index, StringComparison.Ordinal)) != -1)
+ {
+ index += 2;
+ count++;
+ if (count + 1 >= n)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static Regex RegexifyDelimiters(IList delimiters)
+ {
+ return new Regex(
+ string.Join("|", delimiters.Select(d =>
+ string.Format("((\\{0})|(\\{1}))", d.Start, d.End))), RegexOptions.Compiled | RegexOptions.Multiline);
+ }
+
+ private class FoldStackItem
+ {
+ public FoldMatch Position;
+ public Delimiter Delimter;
+ }
+ }
+
+ public class FoldMatch
+ {
+ public int Start;
+ public int End;
+ }
+
+ public class Delimiter
+ {
+ public string Start;
+ public string End;
+ }
+}
diff --git a/src/CosmosDbExplorer.Core/Helpers/TimedDebounce.cs b/src/CosmosDbExplorer.Core/Helpers/TimedDebounce.cs
new file mode 100644
index 0000000..107da0e
--- /dev/null
+++ b/src/CosmosDbExplorer.Core/Helpers/TimedDebounce.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Threading;
+
+namespace CosmosDbExplorer.Core.Helpers
+{
+ public class TimedDebounce
+ {
+ public event EventHandler Idled = delegate { };
+ public int WaitingMilliSeconds { get; set; }
+ private readonly Timer _timer;
+
+ public TimedDebounce(int waitingMilliSeconds = 600)
+ {
+ WaitingMilliSeconds = waitingMilliSeconds;
+ _timer = new Timer(p => Idled(this, EventArgs.Empty));
+ }
+
+ public void DebounceEvent() => _timer.Change(WaitingMilliSeconds, Timeout.Infinite);
+ }
+}
diff --git a/src/CosmosDbExplorer/AvalonEdit/BraceFoldingStrategy.cs b/src/CosmosDbExplorer/AvalonEdit/BraceFoldingStrategy.cs
index 191976e..e91bc35 100644
--- a/src/CosmosDbExplorer/AvalonEdit/BraceFoldingStrategy.cs
+++ b/src/CosmosDbExplorer/AvalonEdit/BraceFoldingStrategy.cs
@@ -1,4 +1,6 @@
using System.Collections.Generic;
+using System.Linq;
+using CosmosDbExplorer.Core.Helpers;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Folding;
@@ -9,23 +11,18 @@ namespace CosmosDbExplorer.AvalonEdit
///
public class BraceFoldingStrategy
{
- ///
- /// Gets/Sets the opening brace. The default value is '{'.
- ///
- public char OpeningBrace { get; set; }
-
- ///
- /// Gets/Sets the closing brace. The default value is '}'.
- ///
- public char ClosingBrace { get; set; }
-
+ private readonly FoldFinder _foldFinder;
///
/// Creates a new BraceFoldingStrategy.
///
public BraceFoldingStrategy()
{
- OpeningBrace = '{';
- ClosingBrace = '}';
+ _foldFinder = new FoldFinder(new List {
+ //Json Object Delimiters
+ new Delimiter { Start = "{", End = "}" },
+ //Json Array Delimiters
+ new Delimiter { Start = "[", End = "]" }
+ }, false);
}
public void UpdateFoldings(FoldingManager manager, TextDocument document)
@@ -39,47 +36,13 @@ public void UpdateFoldings(FoldingManager manager, TextDocument document)
///
///
///
- public IEnumerable CreateNewFoldings(TextDocument document, out int firstErrorOffset)
- {
- firstErrorOffset = -1;
- return CreateNewFoldings(document);
- }
-
- ///
- /// Create s for the specified document.
- ///
- ///
- public IEnumerable CreateNewFoldings(ITextSource document)
+ private IEnumerable CreateNewFoldings(TextDocument document, out int firstErrorOffset)
{
- var newFoldings = new List();
-
- var startOffsets = new Stack();
- var lastNewLineOffset = 0;
- var openingBrace = OpeningBrace;
- var closingBrace = ClosingBrace;
- for (var i = 0; i < document.TextLength; i++)
- {
- var c = document.GetCharAt(i);
- if (c == openingBrace)
- {
- startOffsets.Push(i);
- }
- else if (c == closingBrace && startOffsets.Count > 0)
- {
- var startOffset = startOffsets.Pop();
- // don't fold if opening and closing brace are on the same line
- if (startOffset < lastNewLineOffset)
- {
- newFoldings.Add(new NewFolding(startOffset, i + 1));
- }
- }
- else if (c == '\n' || c == '\r')
- {
- lastNewLineOffset = i + 1;
- }
- }
- newFoldings.Sort((a, b) => a.StartOffset.CompareTo(b.StartOffset));
- return newFoldings;
+ var result = _foldFinder.Scan(document.Text, throwOnError: false)
+ .OrderBy(o => o.Start)
+ .Select(o => new NewFolding(o.Start, o.End));
+ firstErrorOffset = _foldFinder.FirstErrorOffset;
+ return result;
}
}
}
diff --git a/src/CosmosDbExplorer/Behaviors/AvalonTextEditorBraceFoldingBehavior.cs b/src/CosmosDbExplorer/Behaviors/AvalonTextEditorBraceFoldingBehavior.cs
index 9e21f64..8952d9e 100644
--- a/src/CosmosDbExplorer/Behaviors/AvalonTextEditorBraceFoldingBehavior.cs
+++ b/src/CosmosDbExplorer/Behaviors/AvalonTextEditorBraceFoldingBehavior.cs
@@ -2,6 +2,7 @@
using System.Windows;
using System.Windows.Threading;
using CosmosDbExplorer.AvalonEdit;
+using CosmosDbExplorer.Core.Helpers;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Folding;
using Microsoft.Xaml.Behaviors;
@@ -12,53 +13,43 @@ public class AvalonTextEditorBraceFoldingBehavior : Behavior
{
private readonly BraceFoldingStrategy _foldingStrategy = new();
private FoldingManager? _foldingManager;
- private readonly DispatcherTimer _timer = new();
+ private readonly TimedDebounce _timer = new() { WaitingMilliSeconds = 500 };
protected override void OnAttached()
{
base.OnAttached();
-
- //_timer.Interval = System.TimeSpan.FromMilliseconds(Interval);
- _timer.Tick += OnTimerTicked;
-
- //if (UseFolding)
- //{
- // _timer.Start();
- //}
+ _timer.Idled += OnTextChangedIdle;
+ AssociatedObject.TextChanged += OnTextChanged;
}
-
protected override void OnDetaching()
{
- _timer.Stop();
- _timer.Tick -= OnTimerTicked;
-
- if (_foldingManager is not null)
- {
- FoldingManager.Uninstall(_foldingManager);
- }
-
+ _timer.Idled -= OnTextChangedIdle;
+ AssociatedObject.TextChanged -= OnTextChanged;
+ ReleaseFoldingManager();
base.OnDetaching();
}
- public int Interval
+ private void OnTextChanged(object? sender, EventArgs e)
{
- get { return (int)GetValue(IntervalProperty); }
- set { SetValue(IntervalProperty, value); }
- }
+ if (_foldingManager is null || AssociatedObject is null)
+ {
+ return;
+ }
- public static readonly DependencyProperty IntervalProperty =
- DependencyProperty.Register(
- "Interval",
- typeof(int),
- typeof(AvalonTextEditorBraceFoldingBehavior),
- new PropertyMetadata(1000, OnIntervalPropertyChanged));
+ _timer.DebounceEvent();
+ }
- private static void OnIntervalPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ private void OnTextChangedIdle(object? sender, EventArgs e)
{
- if (d is AvalonTextEditorBraceFoldingBehavior behavior)
+ if (_foldingManager is null)
{
- behavior.OnIntervalChanged();
+ return;
}
+
+ Dispatcher.Invoke(() =>
+ {
+ _foldingStrategy.UpdateFoldings(_foldingManager, AssociatedObject.Document);
+ });
}
public bool UseFolding
@@ -87,47 +78,29 @@ private void OnUseFoldingChanged()
{
if (UseFolding)
{
- _timer.Start();
+ InitFoldingManager();
}
else
{
- if (_foldingManager != null)
- {
- _foldingManager.Clear();
- FoldingManager.Uninstall(_foldingManager);
- }
-
- _timer.Stop();
+ ReleaseFoldingManager();
}
}
- private void OnIntervalChanged()
+ private void InitFoldingManager()
{
- if (AssociatedObject is null)
+ if (_foldingManager is null && AssociatedObject?.TextArea is not null)
{
- return;
+ _foldingManager = FoldingManager.Install(AssociatedObject.TextArea);
}
-
- _timer.Stop();
- _timer.Interval = System.TimeSpan.FromMilliseconds(Interval);
- _timer.Start();
}
- private void OnTimerTicked(object? sender, System.EventArgs e)
+ private void ReleaseFoldingManager()
{
- if (AssociatedObject is null)
- {
- return;
- }
-
- if (_foldingManager == null && AssociatedObject?.TextArea?.Document?.Text != null)
- {
- _foldingManager = FoldingManager.Install(AssociatedObject.TextArea);
- }
-
- if (_foldingManager != null)
+ if (_foldingManager is not null)
{
- _foldingStrategy.UpdateFoldings(_foldingManager, AssociatedObject.Document);
+ _foldingManager.Clear();
+ FoldingManager.Uninstall(_foldingManager);
+ _foldingManager = null;
}
}
}
diff --git a/src/CosmosDbExplorer/Views/ImportDocumentView.xaml b/src/CosmosDbExplorer/Views/ImportDocumentView.xaml
index b238cb8..dbf1a27 100644
--- a/src/CosmosDbExplorer/Views/ImportDocumentView.xaml
+++ b/src/CosmosDbExplorer/Views/ImportDocumentView.xaml
@@ -9,12 +9,12 @@
xmlns:behaviors="clr-namespace:CosmosDbExplorer.Behaviors"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
-
+
-
+
diff --git a/src/CosmosDbExplorer/Views/JsonEditorView.xaml.cs b/src/CosmosDbExplorer/Views/JsonEditorView.xaml.cs
index 62d28f0..0c34450 100644
--- a/src/CosmosDbExplorer/Views/JsonEditorView.xaml.cs
+++ b/src/CosmosDbExplorer/Views/JsonEditorView.xaml.cs
@@ -1,17 +1,5 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows;
+using System.Windows;
using System.Windows.Controls;
-using System.Windows.Data;
-using System.Windows.Documents;
-using System.Windows.Input;
-using System.Windows.Media;
-using System.Windows.Media.Imaging;
-using System.Windows.Navigation;
-using System.Windows.Shapes;
namespace CosmosDbExplorer.Views
{
@@ -41,8 +29,6 @@ private static void OnZoomLevelChanged(DependencyObject d, DependencyPropertyCha
target.zoomBehavior.ZoomLevel = value;
}
-
-
public bool UseFolding
{
get { return (bool)GetValue(UseFoldingProperty); }
@@ -59,24 +45,5 @@ private static void OnUseFoldingChanged(DependencyObject d, DependencyPropertyCh
var target = (JsonEditorView)d;
target.foldingBehavior.UseFolding = value;
}
-
-
-
- public int FoldingInterval
- {
- get { return (int)GetValue(FoldingIntervalProperty); }
- set { SetValue(FoldingIntervalProperty, value); }
- }
-
- // Using a DependencyProperty as the backing store for FoldingInterval. This enables animation, styling, binding, etc...
- public static readonly DependencyProperty FoldingIntervalProperty =
- DependencyProperty.Register("FoldingInterval", typeof(int), typeof(JsonEditorView), new PropertyMetadata(1000, OnFoldingIntervalChanged));
-
- private static void OnFoldingIntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
- {
- var value = (int)e.NewValue;
- var target = (JsonEditorView)d;
- target.foldingBehavior.Interval = value;
- }
}
}