using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using Terminal.Gui; namespace UICatalog.Scenarios; [ScenarioMetadata ("Syntax Highlighting", "Text editor with keyword highlighting using the TextView control.")] [ScenarioCategory ("Text and Formatting")] [ScenarioCategory ("Controls")] [ScenarioCategory ("TextView")] public class SyntaxHighlighting : Scenario { private readonly HashSet _keywords = new (StringComparer.CurrentCultureIgnoreCase) { "select", "distinct", "top", "from", "create", "CIPHER", "CLASS_ORIGIN", "CLIENT", "CLOSE", "COALESCE", "CODE", "COLUMNS", "COLUMN_FORMAT", "COLUMN_NAME", "COMMENT", "COMMIT", "COMPACT", "COMPLETION", "COMPRESSED", "COMPRESSION", "CONCURRENT", "CONNECT", "CONNECTION", "CONSISTENT", "CONSTRAINT_CATALOG", "CONSTRAINT_SCHEMA", "CONSTRAINT_NAME", "CONTAINS", "CONTEXT", "CONTRIBUTORS", "COPY", "CPU", "CURSOR_NAME", "primary", "key", "insert", "alter", "add", "update", "set", "delete", "truncate", "as", "order", "by", "asc", "desc", "between", "where", "and", "or", "not", "limit", "null", "is", "drop", "database", "table", "having", "in", "join", "on", "union", "exists" }; private readonly string _path = "Cells.rce"; private Attribute _blue; private Attribute _green; private Attribute _magenta; private MenuItem _miWrap; private TextView _textView; private Attribute _white; /// /// Reads an object instance from an Json file. /// Object type must have a parameterless constructor. /// /// The type of object to read from the file. /// The file path to read the object instance from. /// Returns a new instance of the object read from the Json file. public static T ReadFromJsonFile (string filePath) where T : new () { TextReader reader = null; try { reader = new StreamReader (filePath); string fileContents = reader.ReadToEnd (); return (T)JsonSerializer.Deserialize (fileContents, typeof (T)); } finally { if (reader != null) { reader.Close (); } } } public override void Main () { // Init Application.Init (); // Setup - Create a top-level application window and configure it. Toplevel appWindow = new (); var menu = new MenuBar { Menus = [ new ( "_TextView", new [] { _miWrap = new ( "_Word Wrap", "", () => WordWrap () ) { CheckType = MenuItemCheckStyle .Checked }, null, new ( "_Syntax Highlighting", "", () => ApplySyntaxHighlighting () ), null, new ( "_Load Rune Cells", "", () => ApplyLoadCells () ), new ( "_Save Rune Cells", "", () => SaveCells () ), null, new ("_Quit", "", () => Quit ()) } ) ] }; appWindow.Add (menu); _textView = new() { Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1) }; ApplySyntaxHighlighting (); appWindow.Add (_textView); var statusBar = new StatusBar ([new (Application.QuitKey, "Quit", Quit)]); appWindow.Add (statusBar); // Run - Start the application. Application.Run (appWindow); appWindow.Dispose (); // Shutdown - Calling Application.Shutdown is required. Application.Shutdown (); } /// /// Writes the given object instance to a Json file. /// Object type must have a parameterless constructor. /// /// Only Public properties and variables will be written to the file. These can be any type though, even other /// classes. /// /// /// If there are public properties/variables that you do not want written to the file, decorate them with the /// [JsonIgnore] attribute. /// /// /// The type of object being written to the file. /// The file path to write the object instance to. /// The object instance to write to the file. /// /// If false the file will be overwritten if it already exists. If true the contents will be appended /// to the file. /// public static void WriteToJsonFile (string filePath, T objectToWrite, bool append = false) where T : new () { TextWriter writer = null; try { string contentsToWriteToFile = JsonSerializer.Serialize (objectToWrite); writer = new StreamWriter (filePath, append); writer.Write (contentsToWriteToFile); } finally { if (writer != null) { writer.Close (); } } } private void ApplyLoadCells () { ClearAllEvents (); List cells = new (); foreach (KeyValuePair color in Colors.ColorSchemes) { string csName = color.Key; foreach (Rune rune in csName.EnumerateRunes ()) { cells.Add (new() { Rune = rune, Attribute = color.Value.Normal }); } cells.Add (new() { Rune = (Rune)'\n', Attribute = color.Value.Focus }); } if (File.Exists (_path)) { //Reading the file List> fileCells = ReadFromJsonFile>> (_path); _textView.Load (fileCells); } else { _textView.Load (cells); } _textView.Autocomplete.SuggestionGenerator = new SingleWordSuggestionGenerator (); } private void ApplySyntaxHighlighting () { ClearAllEvents (); _green = new Attribute (Color.Green, Color.Black); _blue = new Attribute (Color.Blue, Color.Black); _magenta = new Attribute (Color.Magenta, Color.Black); _white = new Attribute (Color.White, Color.Black); _textView.ColorScheme = new () { Focus = _white }; _textView.Text = "/*Query to select:\nLots of data*/\nSELECT TOP 100 * \nfrom\n MyDb.dbo.Biochemistry where TestCode = 'blah';"; _textView.Autocomplete.SuggestionGenerator = new SingleWordSuggestionGenerator { AllSuggestions = _keywords.ToList () }; _textView.TextChanged += (s, e) => HighlightTextBasedOnKeywords (); _textView.DrawContent += (s, e) => HighlightTextBasedOnKeywords (); _textView.DrawContentComplete += (s, e) => HighlightTextBasedOnKeywords (); } private void ClearAllEvents () { _textView.ClearEventHandlers ("TextChanged"); _textView.ClearEventHandlers ("DrawContent"); _textView.ClearEventHandlers ("DrawContentComplete"); _textView.InheritsPreviousAttribute = false; } private bool ContainsPosition (Match m, int pos) { return pos >= m.Index && pos < m.Index + m.Length; } private void HighlightTextBasedOnKeywords () { // Comment blocks, quote blocks etc Dictionary blocks = new (); var comments = new Regex (@"/\*.*?\*/", RegexOptions.Singleline); MatchCollection commentMatches = comments.Matches (_textView.Text); var singleQuote = new Regex (@"'.*?'", RegexOptions.Singleline); MatchCollection singleQuoteMatches = singleQuote.Matches (_textView.Text); // Find all keywords (ignoring for now if they are in comments, quotes etc) Regex [] keywordRegexes = _keywords.Select (k => new Regex ($@"\b{k}\b", RegexOptions.IgnoreCase)).ToArray (); Match [] keywordMatches = keywordRegexes.SelectMany (r => r.Matches (_textView.Text)).ToArray (); var pos = 0; for (var y = 0; y < _textView.Lines; y++) { List line = _textView.GetLine (y); for (var x = 0; x < line.Count; x++) { Cell cell = line [x]; if (commentMatches.Any (m => ContainsPosition (m, pos))) { cell.Attribute = _green; } else if (singleQuoteMatches.Any (m => ContainsPosition (m, pos))) { cell.Attribute = _magenta; } else if (keywordMatches.Any (m => ContainsPosition (m, pos))) { cell.Attribute = _blue; } else { cell.Attribute = _white; } line [x] = cell; pos++; } // for the \n or \r\n that exists in Text but not the returned lines pos += Environment.NewLine.Length; } } private string IdxToWord (List line, int idx) { string [] words = Regex.Split ( new (line.Select (r => (char)r.Value).ToArray ()), "\\b" ); var count = 0; string current = null; foreach (string word in words) { current = word; count += word.Length; if (count > idx) { break; } } return current?.Trim (); } private bool IsKeyword (List line, int idx) { string word = IdxToWord (line, idx); if (string.IsNullOrWhiteSpace (word)) { return false; } return _keywords.Contains (word, StringComparer.CurrentCultureIgnoreCase); } private void Quit () { Application.RequestStop (); } private void SaveCells () { //Writing to file List> cells = _textView.GetAllLines (); WriteToJsonFile (_path, cells); } private void WordWrap () { _miWrap.Checked = !_miWrap.Checked; _textView.WordWrap = (bool)_miWrap.Checked; } } public static class EventExtensions { public static void ClearEventHandlers (this object obj, string eventName) { if (obj == null) { return; } Type objType = obj.GetType (); EventInfo eventInfo = objType.GetEvent (eventName); if (eventInfo == null) { return; } var isEventProperty = false; Type type = objType; FieldInfo eventFieldInfo = null; while (type != null) { /* Find events defined as field */ eventFieldInfo = type.GetField ( eventName, BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ); if (eventFieldInfo != null && (eventFieldInfo.FieldType == typeof (MulticastDelegate) || eventFieldInfo.FieldType.IsSubclassOf ( typeof (MulticastDelegate) ))) { break; } /* Find events defined as property { add; remove; } */ eventFieldInfo = type.GetField ( "EVENT_" + eventName.ToUpper (), BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic ); if (eventFieldInfo != null) { isEventProperty = true; break; } type = type.BaseType; } if (eventFieldInfo == null) { return; } if (isEventProperty) { // Default Events Collection Type RemoveHandler (obj, eventFieldInfo); return; } if (!(eventFieldInfo.GetValue (obj) is Delegate eventDelegate)) { return; } // Remove Field based event handlers foreach (Delegate d in eventDelegate.GetInvocationList ()) { eventInfo.RemoveEventHandler (obj, d); } } private static void RemoveHandler (object obj, FieldInfo eventFieldInfo) { Type objType = obj.GetType (); object eventPropertyValue = eventFieldInfo.GetValue (obj); if (eventPropertyValue == null) { return; } PropertyInfo propertyInfo = objType.GetProperties (BindingFlags.NonPublic | BindingFlags.Instance) .FirstOrDefault (p => p.Name == "Events" && p.PropertyType == typeof (T)); if (propertyInfo == null) { return; } object eventList = propertyInfo?.GetValue (obj, null); switch (eventList) { case null: return; } } }