Reputation: 484
Has someone ever tried to create a FoldingStrategy by the indent level? Like for the programming language python.
BraceFoldingStrategy is no problem because you have a fixed start and end tag. Has someone a idea to create this for tab indents?
Upvotes: 2
Views: 1496
Reputation: 484
I recognized that the AvalonEdit.TextDocument also can be used line based. So i was able to create my own solution:
using System;
using System.Collections.Generic;
using ICSharpCode.AvalonEdit.Document;
namespace ICSharpCode.AvalonEdit.Folding
{
/// <summary>
/// Allows producing tab based foldings
/// </summary>
public class TabFoldingStrategy : AbstractFoldingStrategy
{
internal class TabIndent
{
public int IndentSize;
public int LineStart;
public int LineEnd;
public int StartOffset => LineStart + IndentSize - 1;
public int TextLength => LineEnd - StartOffset;
public TabIndent(int i_indentSize, int i_lineStart, int i_lineEnd)
{
IndentSize = i_indentSize;
LineStart = i_lineStart;
LineEnd = i_lineEnd;
}
}
/// <summary>
/// Creates a new TabFoldingStrategy.
/// </summary>
public TabFoldingStrategy()
{
}
/// <summary>
/// Create <see cref="NewFolding"/>s for the specified document.
/// </summary>
public override IEnumerable<NewFolding> CreateNewFoldings(TextDocument document, out int firstErrorOffset)
{
firstErrorOffset = -1;
return CreateNewFoldings(document);
}
/// <summary>
/// Create <see cref="NewFolding"/>s for the specified document.
/// </summary>
public IEnumerable<NewFolding> CreateNewFoldings(TextDocument document)
{
List<NewFolding> newFoldings = new List<NewFolding>();
int documentIndent = 0;
List<TabIndent> tabIndents = new List<TabIndent>();
foreach (DocumentLine line in document.Lines) {
int lineIndent = 0;
for (int i = line.Offset; i < line.EndOffset; i++) {
char c = document.GetCharAt(i);
if (c == '\t') {
lineIndent++;
} else {
break;
}
}
if (lineIndent > documentIndent) {
tabIndents.Add(new TabIndent(lineIndent, line.PreviousLine.Offset, line.PreviousLine.EndOffset));
} else if (lineIndent < documentIndent) {
List<TabIndent> closedIndents = tabIndents.FindAll(x => x.IndentSize > lineIndent);
closedIndents.ForEach(x => {
newFoldings.Add(new NewFolding(x.StartOffset, line.PreviousLine.EndOffset) {
Name = document.GetText(x.StartOffset, x.TextLength)
});
tabIndents.Remove(x);
});
}
documentIndent = lineIndent;
}
tabIndents.ForEach(x => {
newFoldings.Add(new NewFolding(x.StartOffset, document.TextLength));
});
newFoldings.Sort((a, b) => a.StartOffset.CompareTo(b.StartOffset));
return newFoldings;
}
}
}
Update:
I think this is also very useful when someone will use python with AvalonEdit. A Syntax Highlighting xshd for dark theme:
<?xml version="1.0"?>
<SyntaxDefinition name ="Python" extensions = ".py" xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color name="Comment" foreground="#808080" />
<Color name="String" foreground="#6A8759" />
<Color name="Keywords" foreground="#c75454" fontWeight="bold" />
<Color name="NumAndTypes" foreground="#21b0b0" />
<Color name="FunctionCall" foreground="#38a1d4" />
<Color name="Words" foreground="#8fb1ba" />
<RuleSet>
<Span color="Comment">
<Begin>\#</Begin>
</Span>
<Span color="String" multiline="true">
<Begin>'</Begin>
<End>'</End>
</Span>
<Span color="String" multiline="true">
<Begin>"</Begin>
<End>"</End>
</Span>
<!-- Digits -->
<Rule color="NumAndTypes">
\b0[xX][0-9a-fA-F]+ # hex number
|
\b0[0-9]+ # octal number
|
( \b\d+(\.[0-9]+)? #number with optional floating point
| \.[0-9]+ #or just starting with floating point
)
([eE][+-]?[0-9]+)? # optional exponent
</Rule>
<Keywords color="NumAndTypes">
<Word>False</Word>
<Word>True</Word>
<Word>None</Word>
</Keywords>
<Keywords color="Keywords">
<Word>class</Word>
<Word>finally</Word>
<Word>is</Word>
<Word>return</Word>
<Word>continue</Word>
<Word>for</Word>
<Word>lambda</Word>
<Word>try</Word>
<Word>def</Word>
<Word>from</Word>
<Word>nonlocal</Word>
<Word>while</Word>
<Word>and</Word>
<Word>del</Word>
<Word>global</Word>
<Word>not</Word>
<Word>with</Word>
<Word>as</Word>
<Word>elif</Word>
<Word>if</Word>
<Word>or</Word>
<Word>yield</Word>
<Word>assert</Word>
<Word>else</Word>
<Word>import</Word>
<Word>pass</Word>
<Word>break</Word>
<Word>except</Word>
<Word>in</Word>
<Word>raise</Word>
</Keywords>
<Rule color="FunctionCall">
\b
[\d\w_]+ # an identifier
(?=\s*\() # followed by (
</Rule>
<Rule color="Words">\w+</Rule>
</RuleSet>
</SyntaxDefinition>
Upvotes: 4
Reputation: 1015
Here is a mostly-functional solution. I don't have time to test all possibilities but with a few of my python scripts I had sitting around (which admittedly are well-formatted) it behaved well. I put some notes in about making it so that it shows the line contents when it's collapsed. Basically just buffer the start line and then put that on the stack with the start index in a Tuple or something.
I am sure that this code won't handle many cases with comments correctly. That's something you will have to test and tweak for. The code is relatively naive, so you may also want to add checks for keywords instead of just the "if it has a colon it is a new folding start" logic. Also, it is done in a more complicated iterating through characters way because of my experience with folding previously. I have tried other "easier" methods like running regex checks on the lines and that can be very, very slow.
Finally, you may want to actually use the error offset thing instead of just bailing as I have done. That should be pretty easy to add in.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Highlighting.Xshd;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Folding;
using System.Text.RegularExpressions;
namespace Foo.languages
{
public class TabFoldingStrategy : AbstractFoldingStrategy
{
// How many spaces == one tab
private const int SpacesInTab = 4;
/// <summary>
/// Creates a new TabFoldingStrategy.
/// </summary>
public TabFoldingStrategy() {
}
/// <summary>
/// Create <see cref="NewFolding"/>s for the specified document.
/// </summary>
public override IEnumerable<NewFolding> CreateNewFoldings(TextDocument document, out int firstErrorOffset)
{
firstErrorOffset = -1;
return CreateNewFoldingsByLine(document);
}
/// <summary>
/// Create <see cref="NewFolding"/>s for the specified document.
/// </summary>
public IEnumerable<NewFolding> CreateNewFoldingsByLine(ITextSource document)
{
List<NewFolding> newFoldings = new List<NewFolding>();
if (document == null || (document as TextDocument).LineCount <= 1)
{
return newFoldings;
}
//Can keep track of offset ourself and from testing it seems to be accurate
int offsetTracker = 0;
// Keep track of start points since things nest
Stack<int> startOffsets = new Stack<int>();
StringBuilder lineBuffer = new StringBuilder();
foreach (DocumentLine line in (document as TextDocument).Lines)
{
if (offsetTracker >= document.TextLength)
{
break;
}
lineBuffer.Clear();
// First task is to get the line and figure out the spacing in front of it
int spaceCounter = 0;
bool foundText = false;
bool foundColon = false;
//for (int i = 0; i < line.Length; i++)
int i = 0;
//TODO buffer the characters so you can have the line contents on the stack too for the folding name (display text)
while (i < line.Length && !(foundText && foundColon))
{
char c = document.GetCharAt(offsetTracker + i);
switch (c)
{
case ' ': // spaces count as one
if (!foundText) {
spaceCounter++;
}
break;
case '\t': // Tabs count as N
if (!foundText) {
spaceCounter += SpacesInTab;
}
break;
case ':': // Tabs count as N
foundColon = true;
break;
default: // anything else means we encountered not spaces or tabs, so keep making the line but stop counting
foundText = true;
break;
}
i++;
}
// before we continue, we need to make sure its a correct multiple
int remainder = spaceCounter % SpacesInTab;
if (remainder > 0)
{
// Some tabbing isn't correct. ignore this line for folding purposes.
// This may break all foldings below that, but it's a complex problem to address.
continue;
}
// Now we need to figure out if this line is a new folding by checking its tabing
// relative to the current stack count. Convert into virtual tabs and compare to stack level
int numTabs = spaceCounter / SpacesInTab; // we know this will be an int because of the above check
if (numTabs >= startOffsets.Count && foundText && foundColon)
{
// we are starting a new folding
startOffsets.Push(offsetTracker);
}
else // numtabs < offsets
{
// we know that this is the end of a folding. It could be the end of multiple foldings. So pop until it matches.
while (numTabs < startOffsets.Count)
{
int foldingStart = startOffsets.Pop();
NewFolding tempFolding = new NewFolding();
//tempFolding.Name = < could add logic here, possibly by tracking key words when starting the folding, to control what is shown when it's folded >
tempFolding.StartOffset = foldingStart;
tempFolding.EndOffset = offsetTracker - 2;
newFoldings.Add(tempFolding);
}
}
// Increment tracker. Much faster than getting it from the line
offsetTracker += line.TotalLength;
}
// Complete last foldings
while (startOffsets.Count > 0)
{
int foldingStart = startOffsets.Pop();
NewFolding tempFolding = new NewFolding();
//tempFolding.Name = < could add logic here, possibly by tracking key words when starting the folding, to control what is shown when it's folded >
tempFolding.StartOffset = foldingStart;
tempFolding.EndOffset = offsetTracker;
newFoldings.Add(tempFolding);
}
newFoldings.Sort((a, b) => (a.StartOffset.CompareTo(b.StartOffset)));
return newFoldings;
}
}
}
Upvotes: 8