SyntaxHighlighting.cs 14 KB

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