SyntaxHighlighting.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Reflection;
  7. using System.Text;
  8. using System.Text.Json;
  9. using System.Text.RegularExpressions;
  10. using Terminal.Gui;
  11. namespace UICatalog.Scenarios {
  12. [ScenarioMetadata (Name: "Syntax Highlighting", Description: "Text editor with keyword highlighting using the TextView control.")]
  13. [ScenarioCategory ("Text and Formatting")]
  14. [ScenarioCategory ("Controls")]
  15. [ScenarioCategory ("TextView")]
  16. public class SyntaxHighlighting : Scenario {
  17. TextView textView;
  18. MenuItem miWrap;
  19. string path = "RuneCells.rce";
  20. private HashSet<string> keywords = new HashSet<string> (StringComparer.CurrentCultureIgnoreCase){
  21. "select",
  22. "distinct",
  23. "top",
  24. "from",
  25. "create",
  26. "CIPHER",
  27. "CLASS_ORIGIN",
  28. "CLIENT",
  29. "CLOSE",
  30. "COALESCE",
  31. "CODE",
  32. "COLUMNS",
  33. "COLUMN_FORMAT",
  34. "COLUMN_NAME",
  35. "COMMENT",
  36. "COMMIT",
  37. "COMPACT",
  38. "COMPLETION",
  39. "COMPRESSED",
  40. "COMPRESSION",
  41. "CONCURRENT",
  42. "CONNECT",
  43. "CONNECTION",
  44. "CONSISTENT",
  45. "CONSTRAINT_CATALOG",
  46. "CONSTRAINT_SCHEMA",
  47. "CONSTRAINT_NAME",
  48. "CONTAINS",
  49. "CONTEXT",
  50. "CONTRIBUTORS",
  51. "COPY",
  52. "CPU",
  53. "CURSOR_NAME",
  54. "primary",
  55. "key",
  56. "insert",
  57. "alter",
  58. "add",
  59. "update",
  60. "set",
  61. "delete",
  62. "truncate",
  63. "as",
  64. "order",
  65. "by",
  66. "asc",
  67. "desc",
  68. "between",
  69. "where",
  70. "and",
  71. "or",
  72. "not",
  73. "limit",
  74. "null",
  75. "is",
  76. "drop",
  77. "database",
  78. "table",
  79. "having",
  80. "in",
  81. "join",
  82. "on",
  83. "union",
  84. "exists",
  85. };
  86. private ColorScheme blue;
  87. private ColorScheme magenta;
  88. private ColorScheme white;
  89. private ColorScheme green;
  90. public override void Setup ()
  91. {
  92. Win.Title = this.GetName ();
  93. var menu = new MenuBar (new MenuBarItem [] {
  94. new MenuBarItem ("_TextView", new MenuItem [] {
  95. miWrap = new MenuItem ("_Word Wrap", "", () => WordWrap()){CheckType = MenuItemCheckStyle.Checked},
  96. null,
  97. new MenuItem ("_Syntax Highlighting", "", () => ApplySyntaxHighlighting()),
  98. null,
  99. new MenuItem ("_Load Rune Cells", "", () => ApplyLoadRuneCells()),
  100. new MenuItem ("_Save Rune Cells", "", () => SaveRuneCells()),
  101. null,
  102. new MenuItem ("_Quit", "", () => Quit()),
  103. })
  104. });
  105. Application.Top.Add (menu);
  106. textView = new TextView () {
  107. X = 0,
  108. Y = 0,
  109. Width = Dim.Fill (),
  110. Height = Dim.Fill ()
  111. };
  112. ApplySyntaxHighlighting ();
  113. Win.Add (textView);
  114. var statusBar = new StatusBar (new StatusItem [] {
  115. new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()),
  116. });
  117. Application.Top.Add (statusBar);
  118. }
  119. private void ApplySyntaxHighlighting ()
  120. {
  121. ClearAllEvents ();
  122. green = new ColorScheme (new Attribute (Color.Green, Color.Black));
  123. blue = new ColorScheme (new Attribute (Color.Blue, Color.Black));
  124. magenta = new ColorScheme (new Attribute (Color.Magenta, Color.Black));
  125. white = new ColorScheme (new Attribute (Color.White, Color.Black));
  126. textView.ColorScheme = white;
  127. textView.Text = "/*Query to select:\nLots of data*/\nSELECT TOP 100 * \nfrom\n MyDb.dbo.Biochemistry where TestCode = 'blah';";
  128. textView.Autocomplete.SuggestionGenerator = new SingleWordSuggestionGenerator () {
  129. AllSuggestions = keywords.ToList ()
  130. };
  131. textView.TextChanged += (s, e) => HighlightTextBasedOnKeywords ();
  132. textView.DrawContent += (s, e) => HighlightTextBasedOnKeywords ();
  133. textView.DrawContentComplete += (s, e) => HighlightTextBasedOnKeywords ();
  134. }
  135. private void ApplyLoadRuneCells ()
  136. {
  137. ClearAllEvents ();
  138. List<RuneCell> runeCells = new List<RuneCell> ();
  139. foreach (var color in Colors.ColorSchemes) {
  140. string csName = color.Key;
  141. foreach (var rune in csName.EnumerateRunes ()) {
  142. runeCells.Add (new RuneCell { Rune = rune, ColorScheme = color.Value });
  143. }
  144. runeCells.Add (new RuneCell { Rune = (Rune)'\n', ColorScheme = color.Value });
  145. }
  146. if (File.Exists (path)) {
  147. //Reading the file
  148. var cells = ReadFromJsonFile<List<List<RuneCell>>> (path);
  149. textView.Load (cells);
  150. } else {
  151. textView.Load (runeCells);
  152. }
  153. textView.Autocomplete.SuggestionGenerator = new SingleWordSuggestionGenerator ();
  154. }
  155. private void SaveRuneCells ()
  156. {
  157. //Writing to file
  158. var cells = textView.GetAllLines ();
  159. WriteToJsonFile (path, cells);
  160. }
  161. private void ClearAllEvents ()
  162. {
  163. textView.ClearEventHandlers ("TextChanged");
  164. textView.ClearEventHandlers ("DrawContent");
  165. textView.ClearEventHandlers ("DrawContentComplete");
  166. textView.InheritsPreviousColorScheme = false;
  167. }
  168. private void HighlightTextBasedOnKeywords ()
  169. {
  170. // Comment blocks, quote blocks etc
  171. Dictionary<Rune, ColorScheme> blocks = new Dictionary<Rune, ColorScheme> ();
  172. var comments = new Regex (@"/\*.*?\*/", RegexOptions.Singleline);
  173. var commentMatches = comments.Matches (textView.Text);
  174. var singleQuote = new Regex (@"'.*?'", RegexOptions.Singleline);
  175. var singleQuoteMatches = singleQuote.Matches (textView.Text);
  176. // Find all keywords (ignoring for now if they are in comments, quotes etc)
  177. Regex [] keywordRegexes = keywords.Select (k => new Regex ($@"\b{k}\b", RegexOptions.IgnoreCase)).ToArray ();
  178. Match [] keywordMatches = keywordRegexes.SelectMany (r => r.Matches (textView.Text)).ToArray ();
  179. int pos = 0;
  180. for (int y = 0; y < textView.Lines; y++) {
  181. var line = textView.GetLine (y);
  182. for (int x = 0; x < line.Count; x++) {
  183. if (commentMatches.Any (m => ContainsPosition (m, pos))) {
  184. line [x].ColorScheme = green;
  185. } else if (singleQuoteMatches.Any (m => ContainsPosition (m, pos))) {
  186. line [x].ColorScheme = magenta;
  187. } else if (keywordMatches.Any (m => ContainsPosition (m, pos))) {
  188. line [x].ColorScheme = blue;
  189. } else {
  190. line [x].ColorScheme = white;
  191. }
  192. pos++;
  193. }
  194. // for the \n or \r\n that exists in Text but not the returned lines
  195. pos += Environment.NewLine.Length;
  196. }
  197. }
  198. private bool ContainsPosition (Match m, int pos)
  199. {
  200. return pos >= m.Index && pos < m.Index + m.Length;
  201. }
  202. private void WordWrap ()
  203. {
  204. miWrap.Checked = !miWrap.Checked;
  205. textView.WordWrap = (bool)miWrap.Checked;
  206. }
  207. private void Quit ()
  208. {
  209. Application.RequestStop ();
  210. }
  211. private bool IsKeyword (List<Rune> line, int idx)
  212. {
  213. var word = IdxToWord (line, idx);
  214. if (string.IsNullOrWhiteSpace (word)) {
  215. return false;
  216. }
  217. return keywords.Contains (word, StringComparer.CurrentCultureIgnoreCase);
  218. }
  219. private string IdxToWord (List<Rune> line, int idx)
  220. {
  221. var words = Regex.Split (
  222. new string (line.Select (r => (char)r.Value).ToArray ()),
  223. "\\b");
  224. int count = 0;
  225. string current = null;
  226. foreach (var word in words) {
  227. current = word;
  228. count += word.Length;
  229. if (count > idx) {
  230. break;
  231. }
  232. }
  233. return current?.Trim ();
  234. }
  235. /// <summary>
  236. /// Writes the given object instance to a Json file.
  237. /// <para>Object type must have a parameterless constructor.</para>
  238. /// <para>Only Public properties and variables will be written to the file. These can be any type though, even other classes.</para>
  239. /// <para>If there are public properties/variables that you do not want written to the file, decorate them with the [JsonIgnore] attribute.</para>
  240. /// </summary>
  241. /// <typeparam name="T">The type of object being written to the file.</typeparam>
  242. /// <param name="filePath">The file path to write the object instance to.</param>
  243. /// <param name="objectToWrite">The object instance to write to the file.</param>
  244. /// <param name="append">If false the file will be overwritten if it already exists. If true the contents will be appended to the file.</param>
  245. public static void WriteToJsonFile<T> (string filePath, T objectToWrite, bool append = false) where T : new()
  246. {
  247. TextWriter writer = null;
  248. try {
  249. var contentsToWriteToFile = JsonSerializer.Serialize (objectToWrite);
  250. writer = new StreamWriter (filePath, append);
  251. writer.Write (contentsToWriteToFile);
  252. } finally {
  253. if (writer != null) {
  254. writer.Close ();
  255. }
  256. }
  257. }
  258. /// <summary>
  259. /// Reads an object instance from an Json file.
  260. /// <para>Object type must have a parameterless constructor.</para>
  261. /// </summary>
  262. /// <typeparam name="T">The type of object to read from the file.</typeparam>
  263. /// <param name="filePath">The file path to read the object instance from.</param>
  264. /// <returns>Returns a new instance of the object read from the Json file.</returns>
  265. public static T ReadFromJsonFile<T> (string filePath) where T : new()
  266. {
  267. TextReader reader = null;
  268. try {
  269. reader = new StreamReader (filePath);
  270. var fileContents = reader.ReadToEnd ();
  271. return (T)JsonSerializer.Deserialize (fileContents, typeof (T));
  272. } finally {
  273. if (reader != null) {
  274. reader.Close ();
  275. }
  276. }
  277. }
  278. }
  279. public static class EventExtensions {
  280. public static void ClearEventHandlers (this object obj, string eventName)
  281. {
  282. if (obj == null) {
  283. return;
  284. }
  285. var objType = obj.GetType ();
  286. var eventInfo = objType.GetEvent (eventName);
  287. if (eventInfo == null) {
  288. return;
  289. }
  290. var isEventProperty = false;
  291. var type = objType;
  292. FieldInfo eventFieldInfo = null;
  293. while (type != null) {
  294. /* Find events defined as field */
  295. eventFieldInfo = type.GetField (eventName, BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
  296. if (eventFieldInfo != null && (eventFieldInfo.FieldType == typeof (MulticastDelegate) || eventFieldInfo.FieldType.IsSubclassOf (typeof (MulticastDelegate)))) {
  297. break;
  298. }
  299. /* Find events defined as property { add; remove; } */
  300. eventFieldInfo = type.GetField ("EVENT_" + eventName.ToUpper (), BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic);
  301. if (eventFieldInfo != null) {
  302. isEventProperty = true;
  303. break;
  304. }
  305. type = type.BaseType;
  306. }
  307. if (eventFieldInfo == null) {
  308. return;
  309. }
  310. if (isEventProperty) {
  311. // Default Events Collection Type
  312. RemoveHandler<EventHandlerList> (obj, eventFieldInfo);
  313. return;
  314. }
  315. if (!(eventFieldInfo.GetValue (obj) is Delegate eventDelegate)) {
  316. return;
  317. }
  318. // Remove Field based event handlers
  319. foreach (var d in eventDelegate.GetInvocationList ()) {
  320. eventInfo.RemoveEventHandler (obj, d);
  321. }
  322. }
  323. private static void RemoveHandler<T> (object obj, FieldInfo eventFieldInfo)
  324. {
  325. var objType = obj.GetType ();
  326. var eventPropertyValue = eventFieldInfo.GetValue (obj);
  327. if (eventPropertyValue == null) {
  328. return;
  329. }
  330. var propertyInfo = objType.GetProperties (BindingFlags.NonPublic | BindingFlags.Instance)
  331. .FirstOrDefault (p => p.Name == "Events" && p.PropertyType == typeof (T));
  332. if (propertyInfo == null) {
  333. return;
  334. }
  335. var eventList = propertyInfo?.GetValue (obj, null);
  336. switch (eventList) {
  337. case null:
  338. return;
  339. }
  340. }
  341. }
  342. }