Skip to content

Commit

Permalink
Add support for array folding. Use algorithm that supports multiple d…
Browse files Browse the repository at this point in the history
…elimiters. (#77)

Update foldings when document text changes, debounce 500ms. Timer was hogging CPU.
Remove Timer and Interval property.
  • Loading branch information
Curlack authored Jun 21, 2022
1 parent 052ea2e commit 50c6657
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 147 deletions.
173 changes: 173 additions & 0 deletions src/CosmosDbExplorer.Core/Helpers/FoldFinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace CosmosDbExplorer.Core.Helpers
{
/// <summary>
/// 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
/// </summary>
public class FoldFinder
{
private readonly bool _foldableRoot;
private readonly IList<Delimiter> _delimiters;
private readonly Regex _scanner;

public FoldFinder(IList<Delimiter> delimiters, bool foldableRoot = true)
{
_foldableRoot = foldableRoot;
_delimiters = delimiters;
_scanner = RegexifyDelimiters(delimiters);
}

public int FirstErrorOffset { get; private set; } = -1;

public IList<FoldMatch> Scan(string code, int start = 0, int end = -1, bool throwOnError = true)
{
FirstErrorOffset = -1;

var positions = new List<FoldMatch>();
var stack = new Stack<FoldStackItem>();

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<Group>().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<Delimiter> 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;
}
}
20 changes: 20 additions & 0 deletions src/CosmosDbExplorer.Core/Helpers/TimedDebounce.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
67 changes: 15 additions & 52 deletions src/CosmosDbExplorer/AvalonEdit/BraceFoldingStrategy.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using CosmosDbExplorer.Core.Helpers;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Folding;

Expand All @@ -9,23 +11,18 @@ namespace CosmosDbExplorer.AvalonEdit
/// </summary>
public class BraceFoldingStrategy
{
/// <summary>
/// Gets/Sets the opening brace. The default value is '{'.
/// </summary>
public char OpeningBrace { get; set; }

/// <summary>
/// Gets/Sets the closing brace. The default value is '}'.
/// </summary>
public char ClosingBrace { get; set; }

private readonly FoldFinder _foldFinder;
/// <summary>
/// Creates a new BraceFoldingStrategy.
/// </summary>
public BraceFoldingStrategy()
{
OpeningBrace = '{';
ClosingBrace = '}';
_foldFinder = new FoldFinder(new List<Delimiter> {
//Json Object Delimiters
new Delimiter { Start = "{", End = "}" },
//Json Array Delimiters
new Delimiter { Start = "[", End = "]" }
}, false);
}

public void UpdateFoldings(FoldingManager manager, TextDocument document)
Expand All @@ -39,47 +36,13 @@ public void UpdateFoldings(FoldingManager manager, TextDocument document)
/// </summary>
/// <param name="document"></param>
/// <param name="firstErrorOffset"></param>
public IEnumerable<NewFolding> CreateNewFoldings(TextDocument document, out int firstErrorOffset)
{
firstErrorOffset = -1;
return CreateNewFoldings(document);
}

/// <summary>
/// Create <see cref="NewFolding"/>s for the specified document.
/// </summary>
/// <param name="document"></param>
public IEnumerable<NewFolding> CreateNewFoldings(ITextSource document)
private IEnumerable<NewFolding> CreateNewFoldings(TextDocument document, out int firstErrorOffset)
{
var newFoldings = new List<NewFolding>();

var startOffsets = new Stack<int>();
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;
}
}
}
Loading

0 comments on commit 50c6657

Please sign in to comment.