SyntaxHighlighting.cs 14 KB

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