Add Git overlay, operation tools, and virtualized preview so large folders stay responsive.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-24 15:04:04 +02:00
parent 9bf451932f
commit a3c54bbb03
127 changed files with 14748 additions and 633 deletions

View File

@@ -0,0 +1,258 @@
namespace Explorer.Application;
public sealed record MarkdownDocument(IReadOnlyList<MarkdownBlock> Blocks, IReadOnlyList<MarkdownHeading> Headings);
public abstract record MarkdownBlock;
public sealed record MarkdownHeading(int Level, string Text, string Id) : MarkdownBlock;
public sealed record MarkdownParagraph(string Text) : MarkdownBlock;
public sealed record MarkdownList(bool Ordered, IReadOnlyList<string> Items) : MarkdownBlock;
public sealed record MarkdownCode(string Language, string Text) : MarkdownBlock;
public sealed record MarkdownTable(IReadOnlyList<string> Headers, IReadOnlyList<IReadOnlyList<string>> Rows) : MarkdownBlock;
public sealed record MarkdownRule : MarkdownBlock;
public sealed record MarkdownQuote(string Text) : MarkdownBlock;
public static class MarkdownParser
{
public static MarkdownDocument Parse(string text)
{
var lines = (text ?? "").Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
var blocks = new List<MarkdownBlock>();
var headings = new List<MarkdownHeading>();
var ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var i = 0;
while (i < lines.Length)
{
var raw = lines[i];
if (string.IsNullOrWhiteSpace(raw))
{
i++;
continue;
}
if (raw.StartsWith("```", StringComparison.Ordinal))
{
var lang = raw[3..].Trim();
i++;
var body = new List<string>();
while (i < lines.Length && !lines[i].StartsWith("```", StringComparison.Ordinal))
{
body.Add(lines[i]);
i++;
}
if (i < lines.Length)
{
i++;
}
blocks.Add(new MarkdownCode(lang, string.Join("\n", body)));
continue;
}
if (IsRule(raw))
{
blocks.Add(new MarkdownRule());
i++;
continue;
}
var heading = ParseHeading(raw);
if (heading is not null)
{
var id = UniqueId(Slug(heading.Text), ids);
var block = heading with { Id = id };
blocks.Add(block);
if (block.Level is 2 or 3)
{
headings.Add(block);
}
i++;
continue;
}
if (raw.TrimStart().StartsWith('|'))
{
var table = ReadTable(lines, ref i);
if (table is not null)
{
blocks.Add(table);
continue;
}
}
if (LooksLikeList(raw, out var ordered))
{
var items = new List<string>();
while (i < lines.Length && LooksLikeList(lines[i], out var next) && next == ordered)
{
items.Add(ListText(lines[i]));
i++;
}
blocks.Add(new MarkdownList(ordered, items));
continue;
}
if (raw.StartsWith("> ", StringComparison.Ordinal) || raw == ">")
{
var quote = new List<string>();
while (i < lines.Length && (lines[i].StartsWith("> ", StringComparison.Ordinal) || lines[i] == ">"))
{
quote.Add(lines[i].StartsWith("> ", StringComparison.Ordinal) ? lines[i][2..] : "");
i++;
}
blocks.Add(new MarkdownQuote(string.Join(" ", quote)));
continue;
}
var para = new List<string> { raw.Trim() };
i++;
while (i < lines.Length
&& !string.IsNullOrWhiteSpace(lines[i])
&& ParseHeading(lines[i]) is null
&& !IsRule(lines[i])
&& !lines[i].StartsWith("```", StringComparison.Ordinal)
&& !LooksLikeList(lines[i], out _)
&& !lines[i].TrimStart().StartsWith('|'))
{
para.Add(lines[i].Trim());
i++;
}
blocks.Add(new MarkdownParagraph(string.Join(" ", para)));
}
return new MarkdownDocument(blocks, headings);
}
private static MarkdownHeading? ParseHeading(string line)
{
if (!line.StartsWith('#') || line.Length < 3)
{
return null;
}
var level = 0;
while (level < line.Length && line[level] == '#' && level < 6)
{
level++;
}
if (level == 0 || level >= line.Length || line[level] != ' ')
{
return null;
}
return new MarkdownHeading(level, line[(level + 1)..].Trim(), "");
}
private static MarkdownTable? ReadTable(string[] lines, ref int i)
{
var header = SplitRow(lines[i]);
if (header.Count == 0)
{
return null;
}
i++;
if (i >= lines.Length || !IsAlignmentRow(lines[i]))
{
return new MarkdownTable(header, []);
}
i++;
var rows = new List<IReadOnlyList<string>>();
while (i < lines.Length && lines[i].TrimStart().StartsWith('|'))
{
rows.Add(SplitRow(lines[i]));
i++;
}
return new MarkdownTable(header, rows);
}
private static List<string> SplitRow(string line)
{
var trimmed = line.Trim();
if (trimmed.StartsWith('|'))
{
trimmed = trimmed[1..];
}
if (trimmed.EndsWith('|'))
{
trimmed = trimmed[..^1];
}
return trimmed.Split('|').Select(c => c.Trim()).ToList();
}
private static bool IsAlignmentRow(string line)
=> line.Contains('|') && line.All(c => c is '|' or ':' or '-' or ' ' or '\t');
private static bool IsRule(string line)
{
var t = line.Trim();
return t is "---" or "***" or "___" || (t.Length >= 3 && t.All(c => c == '-' || c == '*' || c == '_'));
}
private static bool LooksLikeList(string line, out bool ordered)
{
ordered = false;
var t = line.TrimStart();
if (t.StartsWith("- ", StringComparison.Ordinal) || t.StartsWith("* ", StringComparison.Ordinal))
{
return true;
}
var dot = t.IndexOf(". ", StringComparison.Ordinal);
if (dot > 0 && t[..dot].All(char.IsDigit))
{
ordered = true;
return true;
}
return false;
}
private static string ListText(string line)
{
var t = line.TrimStart();
if (t.StartsWith("- ", StringComparison.Ordinal) || t.StartsWith("* ", StringComparison.Ordinal))
{
return t[2..].Trim();
}
var dot = t.IndexOf(". ", StringComparison.Ordinal);
return dot > 0 ? t[(dot + 2)..].Trim() : t;
}
private static string Slug(string text)
{
var chars = text.Where(c => char.IsLetterOrDigit(c) || c is ' ' or '-').ToArray();
return new string(chars).Trim().Replace(' ', '-').ToLowerInvariant();
}
private static string UniqueId(string slug, HashSet<string> ids)
{
var id = string.IsNullOrEmpty(slug) ? "section" : slug;
var n = 2;
var candidate = id;
while (!ids.Add(candidate))
{
candidate = id + "-" + n;
n++;
}
return candidate;
}
}