SyntaxHighlighting.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  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. // DrawingText happens before DrawingContent so we use it to highlight
  255. _textView.DrawingText += (s, e) => HighlightTextBasedOnKeywords ();
  256. }
  257. private void ClearAllEvents ()
  258. {
  259. _textView.ClearEventHandlers ("DrawingText");
  260. _textView.InheritsPreviousAttribute = false;
  261. }
  262. private bool ContainsPosition (Match m, int pos) { return pos >= m.Index && pos < m.Index + m.Length; }
  263. private void HighlightTextBasedOnKeywords ()
  264. {
  265. // Comment blocks, quote blocks etc
  266. Dictionary<Rune, ColorScheme> blocks = new ();
  267. var comments = new Regex (@"/\*.*?\*/", RegexOptions.Singleline);
  268. MatchCollection commentMatches = comments.Matches (_textView.Text);
  269. var singleQuote = new Regex (@"'.*?'", RegexOptions.Singleline);
  270. MatchCollection singleQuoteMatches = singleQuote.Matches (_textView.Text);
  271. // Find all keywords (ignoring for now if they are in comments, quotes etc)
  272. Regex [] keywordRegexes =
  273. _keywords.Select (k => new Regex ($@"\b{k}\b", RegexOptions.IgnoreCase)).ToArray ();
  274. Match [] keywordMatches = keywordRegexes.SelectMany (r => r.Matches (_textView.Text)).ToArray ();
  275. var pos = 0;
  276. for (var y = 0; y < _textView.Lines; y++)
  277. {
  278. List<Cell> line = _textView.GetLine (y);
  279. for (var x = 0; x < line.Count; x++)
  280. {
  281. Cell cell = line [x];
  282. if (commentMatches.Any (m => ContainsPosition (m, pos)))
  283. {
  284. cell.Attribute = _green;
  285. }
  286. else if (singleQuoteMatches.Any (m => ContainsPosition (m, pos)))
  287. {
  288. cell.Attribute = _magenta;
  289. }
  290. else if (keywordMatches.Any (m => ContainsPosition (m, pos)))
  291. {
  292. cell.Attribute = _blue;
  293. }
  294. else
  295. {
  296. cell.Attribute = _white;
  297. }
  298. line [x] = cell;
  299. pos++;
  300. }
  301. // for the \n or \r\n that exists in Text but not the returned lines
  302. pos += Environment.NewLine.Length;
  303. }
  304. }
  305. private string IdxToWord (List<Rune> line, int idx)
  306. {
  307. string [] words = Regex.Split (
  308. new (line.Select (r => (char)r.Value).ToArray ()),
  309. "\\b"
  310. );
  311. var count = 0;
  312. string current = null;
  313. foreach (string word in words)
  314. {
  315. current = word;
  316. count += word.Length;
  317. if (count > idx)
  318. {
  319. break;
  320. }
  321. }
  322. return current?.Trim ();
  323. }
  324. private bool IsKeyword (List<Rune> line, int idx)
  325. {
  326. string word = IdxToWord (line, idx);
  327. if (string.IsNullOrWhiteSpace (word))
  328. {
  329. return false;
  330. }
  331. return _keywords.Contains (word, StringComparer.CurrentCultureIgnoreCase);
  332. }
  333. private void Quit () { Application.RequestStop (); }
  334. private void SaveCells ()
  335. {
  336. //Writing to file
  337. List<List<Cell>> cells = _textView.GetAllLines ();
  338. WriteToJsonFile (_path, cells);
  339. }
  340. private void WordWrap ()
  341. {
  342. _miWrap.Checked = !_miWrap.Checked;
  343. _textView.WordWrap = (bool)_miWrap.Checked;
  344. }
  345. }
  346. public static class EventExtensions
  347. {
  348. public static void ClearEventHandlers (this object obj, string eventName)
  349. {
  350. if (obj == null)
  351. {
  352. return;
  353. }
  354. Type objType = obj.GetType ();
  355. EventInfo eventInfo = objType.GetEvent (eventName);
  356. if (eventInfo == null)
  357. {
  358. return;
  359. }
  360. var isEventProperty = false;
  361. Type type = objType;
  362. FieldInfo eventFieldInfo = null;
  363. while (type != null)
  364. {
  365. /* Find events defined as field */
  366. eventFieldInfo = type.GetField (
  367. eventName,
  368. BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
  369. );
  370. if (eventFieldInfo != null
  371. && (eventFieldInfo.FieldType == typeof (MulticastDelegate)
  372. || eventFieldInfo.FieldType.IsSubclassOf (
  373. typeof (MulticastDelegate)
  374. )))
  375. {
  376. break;
  377. }
  378. /* Find events defined as property { add; remove; } */
  379. eventFieldInfo = type.GetField (
  380. "EVENT_" + eventName.ToUpper (),
  381. BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic
  382. );
  383. if (eventFieldInfo != null)
  384. {
  385. isEventProperty = true;
  386. break;
  387. }
  388. type = type.BaseType;
  389. }
  390. if (eventFieldInfo == null)
  391. {
  392. return;
  393. }
  394. if (isEventProperty)
  395. {
  396. // Default Events Collection Type
  397. RemoveHandler<EventHandlerList> (obj, eventFieldInfo);
  398. return;
  399. }
  400. if (!(eventFieldInfo.GetValue (obj) is Delegate eventDelegate))
  401. {
  402. return;
  403. }
  404. // Remove Field based event handlers
  405. foreach (Delegate d in eventDelegate.GetInvocationList ())
  406. {
  407. eventInfo.RemoveEventHandler (obj, d);
  408. }
  409. }
  410. private static void RemoveHandler<T> (object obj, FieldInfo eventFieldInfo)
  411. {
  412. Type objType = obj.GetType ();
  413. object eventPropertyValue = eventFieldInfo.GetValue (obj);
  414. if (eventPropertyValue == null)
  415. {
  416. return;
  417. }
  418. PropertyInfo propertyInfo = objType.GetProperties (BindingFlags.NonPublic | BindingFlags.Instance)
  419. .FirstOrDefault (p => p.Name == "Events" && p.PropertyType == typeof (T));
  420. if (propertyInfo == null)
  421. {
  422. return;
  423. }
  424. object eventList = propertyInfo?.GetValue (obj, null);
  425. switch (eventList)
  426. {
  427. case null:
  428. return;
  429. }
  430. }
  431. }