483 lines
14 KiB
C#
483 lines
14 KiB
C#
|
using AppKit;
|
|||
|
using Foundation;
|
|||
|
|
|||
|
namespace Cauldron.Macos.SourceWriter;
|
|||
|
|
|||
|
[Register("LanguageFormatter")]
|
|||
|
public class LanguageFormatter : NSObject
|
|||
|
{
|
|||
|
#region Private Variables
|
|||
|
|
|||
|
/// <summary>The current language syntax highlighting descriptor.</summary>
|
|||
|
private LanguageDescriptor _language = new();
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Computed Properties
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets or sets the text view that this language formatter will be performing syntax
|
|||
|
/// highlighting on.
|
|||
|
/// </summary>
|
|||
|
/// <value>The <c>NSTextView</c> to syntax highlight.</value>
|
|||
|
public NSTextView TextEditor { get; set; }
|
|||
|
|
|||
|
/// <summary>Gets or sets the newline character used to define a given line of text.</summary>
|
|||
|
/// <value>The newline character.</value>
|
|||
|
public char Newline { get; set; } = '\n';
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets or sets the Unitext line separator used to define a given line of text.
|
|||
|
/// </summary>
|
|||
|
/// <value>The line separator.</value>
|
|||
|
public char LineSeparator { get; set; } = '\u2028';
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets or sets the Unitext paragraph separator used to define a given paragraph of text.
|
|||
|
/// </summary>
|
|||
|
/// <value>The paragraph separator.</value>
|
|||
|
public char ParagraphSeparator { get; set; } = '\u2029';
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Gets or sets the descriptor used to define the syntax highlighting rules for a given
|
|||
|
/// language.
|
|||
|
/// </summary>
|
|||
|
/// <value>The <see cref="LanguageDescriptor"/> to syntax highlight.</value>
|
|||
|
public LanguageDescriptor Language
|
|||
|
{
|
|||
|
get { return _language; }
|
|||
|
set
|
|||
|
{
|
|||
|
_language = value;
|
|||
|
_language.Define();
|
|||
|
Reformat();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Constructor
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Initializes a new instance of the <see cref="LanguageFormatter"/> class.
|
|||
|
/// </summary>
|
|||
|
/// <param name="textEditor">
|
|||
|
/// The <c>NSTextView</c> that this language formatter will syntax highlight.
|
|||
|
/// </param>
|
|||
|
/// <param name="language">The <see cref="LanguageDescriptor"/> defining the
|
|||
|
/// language syntax highlighting rules.</param>
|
|||
|
public LanguageFormatter(NSTextView textEditor, LanguageDescriptor language)
|
|||
|
{
|
|||
|
this.TextEditor = textEditor;
|
|||
|
this.Language = language;
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
#region Public Methods
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Forces all of the text in the attached <c>NSTextView</c> (the <c>TextEditor</c> property) to
|
|||
|
/// have its syntax rehighlighted by re-running the formatter.
|
|||
|
/// </summary>
|
|||
|
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);
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>Determines whether the passed in character is a language separator.</summary>
|
|||
|
/// <returns>
|
|||
|
/// <c>true</c> if the character is a language separator; otherwise, <c>false</c>.
|
|||
|
/// </returns>
|
|||
|
/// <param name="c">The character being tested.</param>
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Finds the word boundries as defined by the <c>LanguageSeparators</c> in the
|
|||
|
/// <see cref="AppKit.TextKit.Formatter.LanguageDescriptor"/> that is currently
|
|||
|
/// being syntax highlighted.
|
|||
|
/// </summary>
|
|||
|
/// <returns>An <c>NSRange</c> containing the starting and ending character locations
|
|||
|
/// of the current word.</returns>
|
|||
|
/// <param name="text">The string to be searched.</param>
|
|||
|
/// <param name="position">
|
|||
|
/// The <c>NSRange</c> specifying the starting location of a possible word.
|
|||
|
/// </param>
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Finds the line boundries as defined by the <c>NewLine</c>, <c>LineSeparator</c>
|
|||
|
/// and <c>ParagraphSeparator</c> characters.
|
|||
|
/// </summary>
|
|||
|
/// <returns>An <c>NSRange</c> containing the starting and ending character locations
|
|||
|
/// of the current line of text.</returns>
|
|||
|
/// <param name="text">The string to be searched.</param>
|
|||
|
/// <param name="position">The <c>NSRange</c> specifying the starting location of a possible
|
|||
|
/// line of text.</param>
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Finds the start of line for the given location in the text as defined by the <c>NewLine</c>,
|
|||
|
/// <c>LineSeparator</c> and <c>ParagraphSeparator</c> characters.
|
|||
|
/// </summary>
|
|||
|
/// <returns>
|
|||
|
/// A <c>NSRange</c> containing the start of the line to the current cursor position.
|
|||
|
/// </returns>
|
|||
|
/// <param name="text">The text to find the start of the line in.</param>
|
|||
|
/// <param name="position">
|
|||
|
/// The current location of the cursor in the text and possible selection.
|
|||
|
/// </param>
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Finds the start of end for the given location in the text as defined by the <c>NewLine</c>,
|
|||
|
/// <c>LineSeparator</c> and <c>ParagraphSeparator</c> characters.
|
|||
|
/// </summary>
|
|||
|
/// <returns>
|
|||
|
/// A <c>NSRange</c> containing the end of the line from the current cursor position.
|
|||
|
/// </returns>
|
|||
|
/// <param name="text">The text to find the end of the line in.</param>
|
|||
|
/// <param name="position">
|
|||
|
/// The current location of the cursor in the text and possible selection.
|
|||
|
/// </param>
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>Tests to see if the preceeding character is whitespace or terminator.</summary>
|
|||
|
/// <returns>
|
|||
|
/// <c>true</c>, if character is whitespace or terminator, <c>false</c> otherwise.
|
|||
|
/// </returns>
|
|||
|
/// <param name="text">The text to test.</param>
|
|||
|
/// <param name="position">The current cursor position inside the text.</param>
|
|||
|
/// <remarks>Returns <c>true</c> if at start of line.</remarks>
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>Tests to see if the trailing character is whitespace or terminator.</summary>
|
|||
|
/// <returns>
|
|||
|
/// <c>true</c>, if character is whitespace or terminator, <c>false</c> otherwise.
|
|||
|
/// </returns>
|
|||
|
/// <param name="text">The text to test.</param>
|
|||
|
/// <param name="position">The current cursor position inside the text.</param>
|
|||
|
/// <remarks>Returns <c>true</c> if at end of line.</remarks>
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Uses the current <c>Language</c> (<see cref="LanguageDescriptor"/>) to syntax highlight the
|
|||
|
/// given word in the attached <c>TextEditor</c> (<c>NSTextView</c>) at the given character
|
|||
|
/// locations.
|
|||
|
/// </summary>
|
|||
|
/// <param name="word">The possible keyword to highlight.</param>
|
|||
|
/// <param name="range">An <c>NSRange</c> specifying the starting and ending character locations
|
|||
|
/// for the word to highlight.</param>
|
|||
|
/// <remarks>
|
|||
|
/// TODO: The Text Kit <c>SetTemporaryAttributes</c> routines are handiling the format of
|
|||
|
/// character strings such as HTML or XML tag incorrectly.
|
|||
|
/// </remarks>
|
|||
|
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
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>
|
|||
|
/// Based on the current <c>Language</c> (<see cref="AppKit.TextKit.Formatter.LanguageDescriptor"/>),
|
|||
|
/// highlight the syntax of the given character region.
|
|||
|
/// </summary>
|
|||
|
/// <param name="text">The string value to be syntax highlighted.</param>
|
|||
|
/// <param name="position">The starting location of the text to be highlighted.</param>
|
|||
|
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
|
|||
|
}
|