SyntaxHighlighting.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  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 ("Syntax Highlighting", "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. {
  18. private readonly HashSet<string> _keywords = new (StringComparer.CurrentCultureIgnoreCase)
  19. {
  20. "select",
  21. "distinct",
  22. "top",
  23. "from",
  24. "create",
  25. "CIPHER",
  26. "CLASS_ORIGIN",
  27. "CLIENT",
  28. "CLOSE",
  29. "COALESCE",
  30. "CODE",
  31. "COLUMNS",
  32. "COLUMN_FORMAT",
  33. "COLUMN_NAME",
  34. "COMMENT",
  35. "COMMIT",
  36. "COMPACT",
  37. "COMPLETION",
  38. "COMPRESSED",
  39. "COMPRESSION",
  40. "CONCURRENT",
  41. "CONNECT",
  42. "CONNECTION",
  43. "CONSISTENT",
  44. "CONSTRAINT_CATALOG",
  45. "CONSTRAINT_SCHEMA",
  46. "CONSTRAINT_NAME",
  47. "CONTAINS",
  48. "CONTEXT",
  49. "CONTRIBUTORS",
  50. "COPY",
  51. "CPU",
  52. "CURSOR_NAME",
  53. "primary",
  54. "key",
  55. "insert",
  56. "alter",
  57. "add",
  58. "update",
  59. "set",
  60. "delete",
  61. "truncate",
  62. "as",
  63. "order",
  64. "by",
  65. "asc",
  66. "desc",
  67. "between",
  68. "where",
  69. "and",
  70. "or",
  71. "not",
  72. "limit",
  73. "null",
  74. "is",
  75. "drop",
  76. "database",
  77. "table",
  78. "having",
  79. "in",
  80. "join",
  81. "on",
  82. "union",
  83. "exists"
  84. };
  85. private readonly string _path = "Cells.rce";
  86. private Attribute _blue;
  87. private Attribute _green;
  88. private Attribute _magenta;
  89. private MenuItem _miWrap;
  90. private TextView _textView;
  91. private Attribute _white;
  92. /// <summary>
  93. /// Reads an object instance from an Json file.
  94. /// <para>Object type must have a parameterless constructor.</para>
  95. /// </summary>
  96. /// <typeparam name="T">The type of object to read from the file.</typeparam>
  97. /// <param name="filePath">The file path to read the object instance from.</param>
  98. /// <returns>Returns a new instance of the object read from the Json file.</returns>
  99. public static T ReadFromJsonFile<T> (string filePath) where T : new ()
  100. {
  101. TextReader reader = null;
  102. try
  103. {
  104. reader = new StreamReader (filePath);
  105. string fileContents = reader.ReadToEnd ();
  106. return (T)JsonSerializer.Deserialize (fileContents, typeof (T));
  107. }
  108. finally
  109. {
  110. if (reader != null)
  111. {
  112. reader.Close ();
  113. }
  114. }
  115. }
  116. public override void Main ()
  117. {
  118. // Init
  119. Application.Init ();
  120. // Setup - Create a top-level application window and configure it.
  121. Toplevel appWindow = new ();
  122. var menu = new MenuBar
  123. {
  124. Menus =
  125. [
  126. new (
  127. "_TextView",
  128. new []
  129. {
  130. _miWrap = new (
  131. "_Word Wrap",
  132. "",
  133. () => WordWrap ()
  134. )
  135. {
  136. CheckType = MenuItemCheckStyle
  137. .Checked
  138. },
  139. null,
  140. new (
  141. "_Syntax Highlighting",
  142. "",
  143. () => ApplySyntaxHighlighting ()
  144. ),
  145. null,
  146. new (
  147. "_Load Rune Cells",
  148. "",
  149. () => ApplyLoadCells ()
  150. ),
  151. new (
  152. "_Save Rune Cells",
  153. "",
  154. () => SaveCells ()
  155. ),
  156. null,
  157. new ("_Quit", "", () => Quit ())
  158. }
  159. )
  160. ]
  161. };
  162. appWindow.Add (menu);
  163. _textView = new()
  164. {
  165. Y = 1,
  166. Width = Dim.Fill (),
  167. Height = Dim.Fill (1)
  168. };
  169. ApplySyntaxHighlighting ();
  170. appWindow.Add (_textView);
  171. var statusBar = new StatusBar ([new (Application.QuitKey, "Quit", Quit)]);
  172. appWindow.Add (statusBar);
  173. // Run - Start the application.
  174. Application.Run (appWindow);
  175. appWindow.Dispose ();
  176. // Shutdown - Calling Application.Shutdown is required.
  177. Application.Shutdown ();
  178. }
  179. /// <summary>
  180. /// Writes the given object instance to a Json file.
  181. /// <para>Object type must have a parameterless constructor.</para>
  182. /// <para>
  183. /// Only Public properties and variables will be written to the file. These can be any type though, even other
  184. /// classes.
  185. /// </para>
  186. /// <para>
  187. /// If there are public properties/variables that you do not want written to the file, decorate them with the
  188. /// [JsonIgnore] attribute.
  189. /// </para>
  190. /// </summary>
  191. /// <typeparam name="T">The type of object being written to the file.</typeparam>
  192. /// <param name="filePath">The file path to write the object instance to.</param>
  193. /// <param name="objectToWrite">The object instance to write to the file.</param>
  194. /// <param name="append">
  195. /// If false the file will be overwritten if it already exists. If true the contents will be appended
  196. /// to the file.
  197. /// </param>
  198. public static void WriteToJsonFile<T> (string filePath, T objectToWrite, bool append = false) where T : new ()
  199. {
  200. TextWriter writer = null;
  201. try
  202. {
  203. string contentsToWriteToFile = JsonSerializer.Serialize (objectToWrite);
  204. writer = new StreamWriter (filePath, append);
  205. writer.Write (contentsToWriteToFile);
  206. }
  207. finally
  208. {
  209. if (writer != null)
  210. {
  211. writer.Close ();
  212. }
  213. }
  214. }
  215. private void ApplyLoadCells ()
  216. {
  217. ClearAllEvents ();
  218. List<Cell> cells = new ();
  219. foreach (KeyValuePair<string, ColorScheme> color in Colors.ColorSchemes)
  220. {
  221. string csName = color.Key;
  222. foreach (Rune rune in csName.EnumerateRunes ())
  223. {
  224. cells.Add (new() { Rune = rune, Attribute = color.Value.Normal });
  225. }
  226. cells.Add (new() { Rune = (Rune)'\n', Attribute = color.Value.Focus });
  227. }
  228. if (File.Exists (_path))
  229. {
  230. //Reading the file
  231. List<List<Cell>> fileCells = ReadFromJsonFile<List<List<Cell>>> (_path);
  232. _textView.Load (fileCells);
  233. }
  234. else
  235. {
  236. _textView.Load (cells);
  237. }
  238. _textView.Autocomplete.SuggestionGenerator = new SingleWordSuggestionGenerator ();
  239. }
  240. private void ApplySyntaxHighlighting ()
  241. {
  242. ClearAllEvents ();
  243. _green = new Attribute (Color.Green, Color.Black);
  244. _blue = new Attribute (Color.Blue, Color.Black);
  245. _magenta = new Attribute (Color.Magenta, Color.Black);
  246. _white = new Attribute (Color.White, Color.Black);
  247. _textView.ColorScheme = new () { Focus = _white };
  248. _textView.Text =
  249. "/*Query to select:\nLots of data*/\nSELECT TOP 100 * \nfrom\n MyDb.dbo.Biochemistry where TestCode = 'blah';";
  250. _textView.Autocomplete.SuggestionGenerator = new SingleWordSuggestionGenerator
  251. {
  252. AllSuggestions = _keywords.ToList ()
  253. };
  254. _textView.TextChanged += (s, e) => HighlightTextBasedOnKeywords ();
  255. _textView.DrawContent += (s, e) => HighlightTextBasedOnKeywords ();
  256. _textView.DrawContentComplete += (s, e) => HighlightTextBasedOnKeywords ();
  257. }
  258. private void ClearAllEvents ()
  259. {
  260. _textView.ClearEventHandlers ("TextChanged");
  261. _textView.ClearEventHandlers ("DrawContent");
  262. _textView.ClearEventHandlers ("DrawContentComplete");
  263. _textView.InheritsPreviousAttribute = false;
  264. }
  265. private bool ContainsPosition (Match m, int pos) { return pos >= m.Index && pos < m.Index + m.Length; }
  266. private void HighlightTextBasedOnKeywords ()
  267. {
  268. // Comment blocks, quote blocks etc
  269. Dictionary<Rune, ColorScheme> blocks = new ();
  270. var comments = new Regex (@"/\*.*?\*/", RegexOptions.Singleline);
  271. MatchCollection commentMatches = comments.Matches (_textView.Text);
  272. var singleQuote = new Regex (@"'.*?'", RegexOptions.Singleline);
  273. MatchCollection singleQuoteMatches = singleQuote.Matches (_textView.Text);
  274. // Find all keywords (ignoring for now if they are in comments, quotes etc)
  275. Regex [] keywordRegexes =
  276. _keywords.Select (k => new Regex ($@"\b{k}\b", RegexOptions.IgnoreCase)).ToArray ();
  277. Match [] keywordMatches = keywordRegexes.SelectMany (r => r.Matches (_textView.Text)).ToArray ();
  278. var pos = 0;
  279. for (var y = 0; y < _textView.Lines; y++)
  280. {
  281. List<Cell> line = _textView.GetLine (y);
  282. for (var x = 0; x < line.Count; x++)
  283. {
  284. Cell cell = line [x];
  285. if (commentMatches.Any (m => ContainsPosition (m, pos)))
  286. {
  287. cell.Attribute = _green;
  288. }
  289. else if (singleQuoteMatches.Any (m => ContainsPosition (m, pos)))
  290. {
  291. cell.Attribute = _magenta;
  292. }
  293. else if (keywordMatches.Any (m => ContainsPosition (m, pos)))
  294. {
  295. cell.Attribute = _blue;
  296. }
  297. else
  298. {
  299. cell.Attribute = _white;
  300. }
  301. line [x] = cell;
  302. pos++;
  303. }
  304. // for the \n or \r\n that exists in Text but not the returned lines
  305. pos += Environment.NewLine.Length;
  306. }
  307. }
  308. private string IdxToWord (List<Rune> line, int idx)
  309. {
  310. string [] words = Regex.Split (
  311. new (line.Select (r => (char)r.Value).ToArray ()),
  312. "\\b"
  313. );
  314. var count = 0;
  315. string current = null;
  316. foreach (string word in words)
  317. {
  318. current = word;
  319. count += word.Length;
  320. if (count > idx)
  321. {
  322. break;
  323. }
  324. }
  325. return current?.Trim ();
  326. }
  327. private bool IsKeyword (List<Rune> line, int idx)
  328. {
  329. string word = IdxToWord (line, idx);
  330. if (string.IsNullOrWhiteSpace (word))
  331. {
  332. return false;
  333. }
  334. return _keywords.Contains (word, StringComparer.CurrentCultureIgnoreCase);
  335. }
  336. private void Quit () { Application.RequestStop (); }
  337. private void SaveCells ()
  338. {
  339. //Writing to file
  340. List<List<Cell>> cells = _textView.GetAllLines ();
  341. WriteToJsonFile (_path, cells);
  342. }
  343. private void WordWrap ()
  344. {
  345. _miWrap.Checked = !_miWrap.Checked;
  346. _textView.WordWrap = (bool)_miWrap.Checked;
  347. }
  348. }
  349. public static class EventExtensions
  350. {
  351. public static void ClearEventHandlers (this object obj, string eventName)
  352. {
  353. if (obj == null)
  354. {
  355. return;
  356. }
  357. Type objType = obj.GetType ();
  358. EventInfo eventInfo = objType.GetEvent (eventName);
  359. if (eventInfo == null)
  360. {
  361. return;
  362. }
  363. var isEventProperty = false;
  364. Type type = objType;
  365. FieldInfo eventFieldInfo = null;
  366. while (type != null)
  367. {
  368. /* Find events defined as field */
  369. eventFieldInfo = type.GetField (
  370. eventName,
  371. BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
  372. );
  373. if (eventFieldInfo != null
  374. && (eventFieldInfo.FieldType == typeof (MulticastDelegate)
  375. || eventFieldInfo.FieldType.IsSubclassOf (
  376. typeof (MulticastDelegate)
  377. )))
  378. {
  379. break;
  380. }
  381. /* Find events defined as property { add; remove; } */
  382. eventFieldInfo = type.GetField (
  383. "EVENT_" + eventName.ToUpper (),
  384. BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic
  385. );
  386. if (eventFieldInfo != null)
  387. {
  388. isEventProperty = true;
  389. break;
  390. }
  391. type = type.BaseType;
  392. }
  393. if (eventFieldInfo == null)
  394. {
  395. return;
  396. }
  397. if (isEventProperty)
  398. {
  399. // Default Events Collection Type
  400. RemoveHandler<EventHandlerList> (obj, eventFieldInfo);
  401. return;
  402. }
  403. if (!(eventFieldInfo.GetValue (obj) is Delegate eventDelegate))
  404. {
  405. return;
  406. }
  407. // Remove Field based event handlers
  408. foreach (Delegate d in eventDelegate.GetInvocationList ())
  409. {
  410. eventInfo.RemoveEventHandler (obj, d);
  411. }
  412. }
  413. private static void RemoveHandler<T> (object obj, FieldInfo eventFieldInfo)
  414. {
  415. Type objType = obj.GetType ();
  416. object eventPropertyValue = eventFieldInfo.GetValue (obj);
  417. if (eventPropertyValue == null)
  418. {
  419. return;
  420. }
  421. PropertyInfo propertyInfo = objType.GetProperties (BindingFlags.NonPublic | BindingFlags.Instance)
  422. .FirstOrDefault (p => p.Name == "Events" && p.PropertyType == typeof (T));
  423. if (propertyInfo == null)
  424. {
  425. return;
  426. }
  427. object eventList = propertyInfo?.GetValue (obj, null);
  428. switch (eventList)
  429. {
  430. case null:
  431. return;
  432. }
  433. }
  434. }