SyntaxHighlighting.cs 14 KB

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