diff --git a/EditorContext/LanguageEnum.cs b/EditorContext/LanguageEnum.cs index bec4400..0bba1c6 100644 --- a/EditorContext/LanguageEnum.cs +++ b/EditorContext/LanguageEnum.cs @@ -6,6 +6,7 @@ public enum LanguageEnum Powershell, JSON, YAML, - XML + XML, + Markdown } } \ No newline at end of file diff --git a/EditorContext/MarkdownEditorContext.cs b/EditorContext/MarkdownEditorContext.cs new file mode 100644 index 0000000..2ac2267 --- /dev/null +++ b/EditorContext/MarkdownEditorContext.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Terminal.Gui; + +namespace psedit +{ + public class MarkdownEditorContext : EditorContext + { + private List parsedTokens; + + public MarkdownEditorContext(int TabWidth) + { + _tabWidth = TabWidth; + CanSyntaxHighlight = true; + } + public List ParseMarkdownToken(string text, List> Runes) + { + List returnValue = new List(); + var theme = ThemeService.Instance; + + try + { + var lines = text.Split('\n'); + bool inCodeBlock = false; + + for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++) + { + var line = lines[lineIndex]; + int lineNumber = lineIndex + 1; + + // Check for code block delimiters (```) + if (Regex.IsMatch(line, @"^\s*```")) + { + inCodeBlock = !inCodeBlock; + returnValue.Add(new ParseResult + { + LineNumber = lineNumber, + StartIndex = 1, + EndIndex = line.Length + 1, + Color = theme.GetColor("Accent") + }); + continue; + } + + // If we're inside a code block, highlight the entire line + if (inCodeBlock) + { + returnValue.Add(new ParseResult + { + LineNumber = lineNumber, + StartIndex = 1, + EndIndex = line.Length + 1, + Color = theme.GetColor("Accent") + }); + continue; + } + + // Headers (# ## ### #### ##### ######) + var headerMatch = Regex.Match(line, @"^(#{1,6})\s+(.*)$"); + if (headerMatch.Success) + { + returnValue.Add(new ParseResult + { + LineNumber = lineNumber, + StartIndex = 1, + EndIndex = line.Length + 1, + Color = theme.GetColor("Accent") + }); + continue; + } + + // Blockquotes (> at start) - color only the marker, then continue parsing + var blockquoteMatch = Regex.Match(line, @"^(>\s*)"); + if (blockquoteMatch.Success) + { + returnValue.Add(new ParseResult + { + LineNumber = lineNumber, + StartIndex = 1, + EndIndex = blockquoteMatch.Length + 1, + Color = theme.GetColor("Warning") + }); + // Don't continue - let other patterns color the rest of the line + } + + // Horizontal rules (--- or *** or ___) + if (Regex.IsMatch(line.Trim(), @"^(\*{3,}|-{3,}|_{3,})$")) + { + returnValue.Add(new ParseResult + { + LineNumber = lineNumber, + StartIndex = 1, + EndIndex = line.Length + 1, + Color = theme.GetColor("Accent") + }); + continue; + } + + // Lists (- or * or + at start, or numbered lists) + var listMatch = Regex.Match(line, @"^\s*([*\-+]|\d+\.)\s+"); + if (listMatch.Success) + { + returnValue.Add(new ParseResult + { + LineNumber = lineNumber, + StartIndex = listMatch.Index + 1, + EndIndex = listMatch.Index + listMatch.Length + 1, + Color = theme.GetColor("Warning") + }); + } + + // Bold (**text** or __text**) + foreach (Match match in Regex.Matches(line, @"(\*\*|__)(.+?)\1")) + { + returnValue.Add(new ParseResult + { + LineNumber = lineNumber, + StartIndex = match.Index + 1, + EndIndex = match.Index + match.Length + 1, + Color = theme.GetColor("Info") + }); + } + + // Italic (*text* or _text_) + foreach (Match match in Regex.Matches(line, @"(?(); + } + + return returnValue; + } + + public override void ParseText(int height, int topRow, int left, int right, string text, List> Runes) + { + // Quick exit when text is the same and the top row / right col has not changed + if (_originalText == text && topRow == _lastParseTopRow && right == _lastParseRightColumn) + { + return; + } + + if (_originalText != text) + { + // Clear errors before parsing new ones + Errors.Clear(); + ColumnErrors.Clear(); + + parsedTokens = ParseMarkdownToken(text, Runes); + } + + Dictionary returnDict = new Dictionary(); + int bottom = topRow + height; + _originalText = text; + _lastParseTopRow = topRow; + _lastParseRightColumn = right; + var row = 0; + + for (int idxRow = topRow; idxRow < Runes.Count; idxRow++) + { + if (row > bottom) + { + break; + } + + var line = EditorExtensions.GetLine(Runes, idxRow); + int lineRuneCount = line.Count; + var col = 0; + var tokenCol = 1 + left; + var rowTokens = parsedTokens.Where(m => m.LineNumber == idxRow + 1); + + for (int idxCol = left; idxCol < lineRuneCount; idxCol++) + { + var rune = idxCol >= lineRuneCount ? ' ' : line[idxCol]; + var cols = Rune.ColumnWidth(rune); + + // Get token, note that we must provide +1 for the end column, as Start will be 1 and End will be 2 for the example: A + var colToken = rowTokens.FirstOrDefault(e => + (e.StartIndex == null && e.EndIndex == null) || + (e.StartIndex == null && tokenCol + 1 <= e.EndIndex) || + (tokenCol >= e.StartIndex && e.EndIndex == null) || + (tokenCol >= e.StartIndex && tokenCol + 1 <= e.EndIndex)); + + if (rune == '\t') + { + cols += _tabWidth + 1; + if (col + cols > right) + { + cols = right - col; + } + for (int i = 1; i < cols; i++) + { + if (col + i < right) + { + // Handle tab spacing + } + } + tokenCol++; + } + else + { + var color = Color.White; + if (colToken != null) + { + color = colToken.Color; + } + var point = new Point(idxCol, row); + returnDict.Add(point, color); + tokenCol++; + } + + if (!EditorExtensions.SetCol(ref col, right, cols)) + { + break; + } + + if (idxCol + 1 < lineRuneCount && col + Rune.ColumnWidth(line[idxCol + 1]) > right) + { + break; + } + } + row++; + } + + pointColorDict = returnDict; + } + } +} diff --git a/EditorTextView.cs b/EditorTextView.cs index 3d98166..7415155 100644 --- a/EditorTextView.cs +++ b/EditorTextView.cs @@ -51,6 +51,10 @@ public void SetLanguage(LanguageEnum language) { editorContext = new XmlEditorContext(TabWidth); } + else if (language == LanguageEnum.Markdown) + { + editorContext = new MarkdownEditorContext(TabWidth); + } else { editorContext = null; diff --git a/README.md b/README.md index cb3ad96..e1d9a1f 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ psedit | JSON | ✖️ | ✔ | ✔ | | | YAML | ✖️ | ✔ | ✔ | | | XML | ✖️ | ✔ | ✔ | | +| Markdown | | ✔ | | | All other text based files are supported, and will be treated as plain text files. diff --git a/ShowEditorCommand.cs b/ShowEditorCommand.cs index 2ee9a09..c4a6925 100644 --- a/ShowEditorCommand.cs +++ b/ShowEditorCommand.cs @@ -97,6 +97,7 @@ protected override void BeginProcessing() _allowedFileTypes.Add(".yaml"); _allowedFileTypes.Add(".config"); _allowedFileTypes.Add(".csproj"); + _allowedFileTypes.Add(".md"); // ...existing code... } @@ -300,6 +301,9 @@ private void SetLanguage(string path) case ".xml": case ".config": case ".csproj": textEditor.SetLanguage(LanguageEnum.XML); break; + case ".md": + textEditor.SetLanguage(LanguageEnum.Markdown); + break; default: textEditor.SetLanguage(LanguageEnum.Text); break; diff --git a/ThemeService.cs b/ThemeService.cs index 0419f48..7d38cfe 100644 --- a/ThemeService.cs +++ b/ThemeService.cs @@ -74,7 +74,10 @@ public class Theme { "Accent", Color.Cyan }, { "Error", Color.Red }, { "Warning", Color.BrightYellow }, - { "Info", Color.BrightBlue } + { "Info", Color.BrightBlue }, + { "String", Color.White }, + { "Comment", Color.Green }, + { "Secondary", Color.Gray } } }; }