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);
TextEditor.LayoutManager.SetTemporaryAttributes(
new NSDictionary(NSStringAttributeKey.ForegroundColor, NSColor.ControlText), 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
}