using AppKit; using Foundation; namespace Cauldron.Macos.SourceWriter; [Register("LanguageFormatter")] public class LanguageFormatter : NSObject { #region Private Variables /// The current language syntax highlighting descriptor. private LanguageDescriptor _language = new(); #endregion #region Computed Properties /// /// Gets or sets the text view that this language formatter will be performing syntax /// highlighting on. /// /// The NSTextView to syntax highlight. public NSTextView TextEditor { get; set; } /// Gets or sets the newline character used to define a given line of text. /// The newline character. public char Newline { get; set; } = '\n'; /// /// Gets or sets the Unitext line separator used to define a given line of text. /// /// The line separator. public char LineSeparator { get; set; } = '\u2028'; /// /// Gets or sets the Unitext paragraph separator used to define a given paragraph of text. /// /// The paragraph separator. public char ParagraphSeparator { get; set; } = '\u2029'; /// /// Gets or sets the descriptor used to define the syntax highlighting rules for a given /// language. /// /// The to syntax highlight. public LanguageDescriptor Language { get { return _language; } set { _language = value; _language.Define(); Reformat(); } } #endregion #region Constructor /// /// Initializes a new instance of the class. /// /// /// The NSTextView that this language formatter will syntax highlight. /// /// The defining the /// language syntax highlighting rules. public LanguageFormatter(NSTextView textEditor, LanguageDescriptor language) { this.TextEditor = textEditor; this.Language = language; } #endregion #region Public Methods /// /// Forces all of the text in the attached NSTextView (the TextEditor property) to /// have its syntax rehighlighted by re-running the formatter. /// public virtual void Reformat() { // Reformat all text in the view control NSRange range = new(0, TextEditor.Value.Length); TextEditor.LayoutManager.RemoveTemporaryAttribute(NSStringAttributeKey.ForegroundColor, range); HighlightSyntaxRegion(TextEditor.Value, range); TextEditor.SetNeedsDisplay(TextEditor.Frame, false); } /// Determines whether the passed in character is a language separator. /// /// true if the character is a language separator; otherwise, false. /// /// The character being tested. public virtual bool IsLanguageSeparator(char c) { // Found separator? for (var n = 0; n < Language.LanguageSeparators.Length; ++n) { if (Language.LanguageSeparators[n] == c) return true; } // Not found return false; } /// /// Finds the word boundries as defined by the LanguageSeparators in the /// that is currently /// being syntax highlighted. /// /// An NSRange containing the starting and ending character locations /// of the current word. /// The string to be searched. /// /// The NSRange specifying the starting location of a possible word. /// public virtual NSRange FindWordBoundries(string text, NSRange position) { NSRange results = new(position.Location, 0); bool found = false; // Find starting "word" boundry while (results.Location > 0 && !found) { var c = text[(int)results.Location - 1]; found = char.IsWhiteSpace(c) || IsLanguageSeparator(c); if (!found) results.Location -= 1; }; // Find ending "word" boundry found = false; while ((int)(results.Location + results.Length) < text.Length && !found) { var c = text[(int)(results.Location + results.Length)]; found = char.IsWhiteSpace(c) || IsLanguageSeparator(c); if (!found) results.Length += 1; }; return results; } /// /// Finds the line boundries as defined by the NewLine, LineSeparator /// and ParagraphSeparator characters. /// /// An NSRange containing the starting and ending character locations /// of the current line of text. /// The string to be searched. /// The NSRange specifying the starting location of a possible /// line of text. public virtual NSRange FindLineBoundries(string text, NSRange position) { NSRange results = position; bool found = false; // Find starting line boundry while (results.Location > 0 && !found) { var c = text[(int)results.Location - 1]; found = (c == Newline || c == LineSeparator || c == ParagraphSeparator); if (!found) results.Location -= 1; }; // Find ending line boundry found = false; while ((int)(results.Location + results.Length) < text.Length && !found) { var c = text[(int)(results.Location + results.Length)]; found = (c == Newline || c == LineSeparator || c == ParagraphSeparator); if (!found) results.Length += 1; }; return results; } /// /// Finds the start of line for the given location in the text as defined by the NewLine, /// LineSeparator and ParagraphSeparator characters. /// /// /// A NSRange containing the start of the line to the current cursor position. /// /// The text to find the start of the line in. /// /// The current location of the cursor in the text and possible selection. /// public virtual NSRange FindStartOfLine(string text, NSRange position) { NSRange results = new(position.Location, position.Length); bool found = false; // Find starting line boundry while (results.Location > 0 && !found) { var c = text[(int)results.Location - 1]; found = (c == Newline || c == LineSeparator || c == ParagraphSeparator); if (!found) results.Location -= 1; }; // Calculate length results.Length = position.Location - results.Location; return results; } /// /// Finds the start of end for the given location in the text as defined by the NewLine, /// LineSeparator and ParagraphSeparator characters. /// /// /// A NSRange containing the end of the line from the current cursor position. /// /// The text to find the end of the line in. /// /// The current location of the cursor in the text and possible selection. /// public virtual NSRange FindEndOfLine(string text, NSRange position) { NSRange results = position; // Find ending line boundry bool found = false; while ((int)(results.Location + results.Length) < text.Length && !found) { char c = text[(int)(results.Location + results.Length)]; found = (c == Newline || c == LineSeparator || c == ParagraphSeparator); if (!found) results.Length += 1; }; return results; } /// Tests to see if the preceeding character is whitespace or terminator. /// /// true, if character is whitespace or terminator, false otherwise. /// /// The text to test. /// The current cursor position inside the text. /// Returns true if at start of line. public virtual bool PreceedingCharacterIsWhitespaceOrTerminator(string text, NSRange position) { // At start of line? if (position.Location == 0) { // Yes, always true return true; } // Found? char c = text[(int)(position.Location - 1)]; bool found = c == ' ' | c == Newline || c == LineSeparator || c == ParagraphSeparator; // Return result return found; } /// Tests to see if the trailing character is whitespace or terminator. /// /// true, if character is whitespace or terminator, false otherwise. /// /// The text to test. /// The current cursor position inside the text. /// Returns true if at end of line. public virtual bool TrailingCharacterIsWhitespaceOrTerminator(string text, NSRange position) { // At end of line? if (position.Location >= text.Length - 1) { // Yes, always true return true; } // Found? char c = text[(int)(position.Location + 1)]; bool found = c == ' ' | c == Newline || c == LineSeparator || c == ParagraphSeparator; // Return result return found; } /// /// Uses the current Language () to syntax highlight the /// given word in the attached TextEditor (NSTextView) at the given character /// locations. /// /// The possible keyword to highlight. /// An NSRange specifying the starting and ending character locations /// for the word to highlight. /// /// TODO: The Text Kit SetTemporaryAttributes routines are handiling the format of /// character strings such as HTML or XML tag incorrectly. /// public virtual void HighlightSyntax(string word, NSRange range) { try { // Found a keyword? if (Language.Keywords.TryGetValue(word, out KeywordDescriptor info)) { // Yes, adjust attributes TextEditor.LayoutManager.SetTemporaryAttributes( new NSDictionary(NSStringAttributeKey.ForegroundColor, info.Color), range); } else { TextEditor.LayoutManager.RemoveTemporaryAttribute( NSStringAttributeKey.ForegroundColor, range); } } catch { // Ignore any exceptions at this point } } /// /// Based on the current Language (), /// highlight the syntax of the given character region. /// /// The string value to be syntax highlighted. /// The starting location of the text to be highlighted. public virtual void HighlightSyntaxRegion(string text, NSRange position) { NSRange range = FindLineBoundries(text, position); string word = ""; nint location = range.Location; char l = ' '; NSRange segment = new(range.Location, 0); bool handled = false; FormatDescriptor inFormat = null; // Initialize Language.ClearFormats(); // Process all characters in range for (int n = 0; n < range.Length; ++n) { // Get next character char c = text[(int)(range.Location + n)]; //Console.Write ("[{0}]={1}", n, c); // Excape character? if (c == Language.EscapeCharacter || l == Language.EscapeCharacter) { // Was the last chanacter an escape? if (l == Language.EscapeCharacter) { // Handling outlying format exception c = ' '; } handled = true; // Are we inside a format? if (inFormat != null) { // Yes, increase segment count ++segment.Length; } } else { // Are we inside of a formatter? if (inFormat == null) { // No, see if this character is recognized by a formatter foreach (FormatDescriptor format in Language.Formats) { if (format.MatchesCharacter(c)) { if (format.Triggered) { Language.ClearFormats(); inFormat = format; inFormat.Active = true; segment = new NSRange((range.Location + n) - (inFormat.StartsWith.Length - 1), inFormat.StartsWith.Length); //Console.WriteLine ("Found Format [{0}] = {1}", inFormat.StartsWith, segment); } handled = true; } } } else { // Prefix or enclosure? if (inFormat.Type == FormatDescriptorType.Prefix) { // At end of line? if (c == Newline || c == LineSeparator || c == ParagraphSeparator) { ++segment.Length; TextEditor.LayoutManager.SetTemporaryAttributes(new NSDictionary(NSStringAttributeKey.ForegroundColor, inFormat.Color), segment); //Console.WriteLine ("Complete Prefix [{0}] = {1}", inFormat.StartsWith, segment); location = range.Location + n + 1; word = ""; inFormat = null; Language.ClearFormats(); } else { ++segment.Length; } handled = true; } else { if (inFormat.MatchesCharacter(c)) { if (inFormat.Triggered) { ++segment.Length; TextEditor.LayoutManager.SetTemporaryAttributes(new NSDictionary(NSStringAttributeKey.ForegroundColor, inFormat.Color), segment); //Console.WriteLine ("Complete Enclosure [{0}] = {1}", inFormat.EndsWith, segment); inFormat = null; Language.ClearFormats(); } } ++segment.Length; handled = true; } } } // Has this character already been handled? if (!handled) { // No, handle normal characters bool found = char.IsWhiteSpace(c) || IsLanguageSeparator(c); if (found) { segment = new NSRange(location, word.Length); if (segment.Length > 0) { HighlightSyntax(word, segment); } location = range.Location + n + 1; word = ""; } else { word += c; } // Clear any fully unmatched formats if (inFormat == null) { Language.ClearFormats(); } } // Save last character l = c; handled = false; } // Finalize if (inFormat != null) { if (inFormat.Type == FormatDescriptorType.Prefix) { TextEditor.LayoutManager.SetTemporaryAttributes(new NSDictionary(NSStringAttributeKey.ForegroundColor, inFormat.Color), segment); //Console.WriteLine ("Finalize Prefix [{0}] = {1}", inFormat.StartsWith, segment); } Language.ClearFormats(); } else if (word != "") { segment = new NSRange(location, word.Length); if (segment.Length > 0) { HighlightSyntax(word, segment); } } //Console.WriteLine (";"); } #endregion }