diff --git a/Cauldron.Macos/Cauldron.Macos.csproj b/Cauldron.Macos/Cauldron.Macos.csproj index 8c65803..949a383 100644 --- a/Cauldron.Macos/Cauldron.Macos.csproj +++ b/Cauldron.Macos/Cauldron.Macos.csproj @@ -52,4 +52,12 @@ + + + + + + + + diff --git a/Cauldron.Macos/Main.storyboard b/Cauldron.Macos/Main.storyboard index f12936c..bbae890 100644 --- a/Cauldron.Macos/Main.storyboard +++ b/Cauldron.Macos/Main.storyboard @@ -522,7 +522,7 @@ - + diff --git a/Cauldron.Macos/MainWindow.cs b/Cauldron.Macos/MainWindow.cs index 41f4722..23a2332 100644 --- a/Cauldron.Macos/MainWindow.cs +++ b/Cauldron.Macos/MainWindow.cs @@ -1,6 +1,8 @@ using System; using System.Threading; using AppKit; +using Cauldron.Macos.SourceWriter; +using Cauldron.Macos.SourceWriter.LanguageFormats; namespace Cauldron.Macos; @@ -26,11 +28,11 @@ public partial class MainWindow : NSWindowController .ContentView.DocumentView as NSOutlineView; } - private NSTextView ScriptEditorTextBox + private SourceTextView ScriptEditorTextBox { get => (this.MainContentController .SplitViewItems[0].ViewController.View as NSScrollView) - .ContentView.DocumentView as NSTextView; + .ContentView.DocumentView as SourceTextView; } public WebKit.WKWebView ScriptOutputWebView @@ -58,7 +60,7 @@ public partial class MainWindow : NSWindowController this.RunScriptToolbarButton.Activated += RunScript; - NSTextView scriptTextBox = this.ScriptEditorTextBox; + SourceTextView scriptTextBox = this.ScriptEditorTextBox; scriptTextBox.Font = NSFont.MonospacedSystemFont(new nfloat(14), NSFontWeight.Regular); scriptTextBox.AutomaticQuoteSubstitutionEnabled = false; scriptTextBox.AutomaticDashSubstitutionEnabled = false; @@ -67,6 +69,9 @@ public partial class MainWindow : NSWindowController scriptTextBox.AutomaticTextCompletionEnabled = false; scriptTextBox.AutomaticTextReplacementEnabled = false; scriptTextBox.AutomaticLinkDetectionEnabled = false; + + scriptTextBox.Formatter = new LanguageFormatter(scriptTextBox, new CSharpDescriptor()); + scriptTextBox.Formatter.Reformat(); } public void RunScript(object sender, EventArgs e) diff --git a/Cauldron.Macos/SourceWriter/FormatDescriptor.cs b/Cauldron.Macos/SourceWriter/FormatDescriptor.cs new file mode 100644 index 0000000..47bb4e6 --- /dev/null +++ b/Cauldron.Macos/SourceWriter/FormatDescriptor.cs @@ -0,0 +1,137 @@ +using AppKit; +using Foundation; + +namespace Cauldron.Macos.SourceWriter; + +public class FormatDescriptor : NSObject +{ + #region Computed Properties + + /// Gets or sets the FormatDescriptorType for this format descriptor. + /// The FormatDescriptorType. + public FormatDescriptorType Type { get; set; } = FormatDescriptorType.Prefix; + + /// + /// Gets or sets the forground color that text matching this format will be set to. + /// + /// The NSColor. + public NSColor Color { get; set; } = NSColor.Gray; + + /// Gets or sets the character sequence that this format starts with. + /// The starting string sequence. + public string StartsWith { get; set; } = ""; + + /// + /// Gets or sets the character sequence that text matching this format ends with. + /// + /// The ending string sequence. + /// + /// This value will be an empty string ("") if the Type is a Prefix format. + /// + public string EndsWith { get; set; } = ""; + + /// + /// Gets or sets the index of the last matching character within either the StartsWith or + /// EndsWith based on the state of the Active property. + /// + /// The index of the char. + /// + /// This value should ONLY be changed by the . + /// + public int CharIndex { get; set; } = 0; + + /// + /// Gets or sets if this format has been "activated" (if the matching StartsWith character sequence + /// has been found). + /// + /// true if the matching StartsWith character sequence + /// has been found; otherwise, false. + /// + /// This value should ONLY be changed by the . + /// + public bool Active { get; set; } = false; + + /// + /// Gets a value indicating whether this is "triggered" + /// (all of the StartsWith or EndsWith characters have been found based on the + /// Active property). + /// + /// true if triggered; otherwise, false. + public bool Triggered + { + get + { + if (Active) + { + return CharIndex > (EndsWith.Length - 1); + } + else + { + return CharIndex > (StartsWith.Length - 1); + } + } + } + + #endregion + + #region Constructors + + /// Initializes a new instance of the class. + /// The starting character sequence for this format. + /// The NSColor that text in this sequence will be set too. + /// The type will automatically be set to Prefix. + public FormatDescriptor(string startsWith, NSColor color) + { + this.Type = FormatDescriptorType.Prefix; + this.StartsWith = startsWith; + this.Color = color; + } + + /// Initializes a new instance of the class. + /// The starting character sequence for this format. + /// The ending character sequence for this format. + /// The NSColor that text in this sequence will be set too. + /// The type will automatically be set to Enclosure. + public FormatDescriptor(string startsWith, string endsWith, NSColor color) + { + this.Type = FormatDescriptorType.Enclosure; + this.StartsWith = startsWith; + this.EndsWith = endsWith; + this.Color = color; + } + #endregion + + #region Public Methods + + /// + /// Tests to see if the passed in character matches the character at CharIndex of + /// either the StartsWith or EndsWith character sequence based on the state + /// of the Active property. + /// + /// true, if character was matched, false otherwise. + /// The character being tested. + public bool MatchesCharacter(char c) + { + bool matches; + + // Is this format currently active? + if (Active) + { + matches = c == EndsWith[CharIndex]; + } + else + { + matches = (c == StartsWith[CharIndex]); + } + + // Increment + if (matches) + { + ++CharIndex; + } + + return matches; + } + + #endregion +} diff --git a/Cauldron.Macos/SourceWriter/FormatDescriptorType.cs b/Cauldron.Macos/SourceWriter/FormatDescriptorType.cs new file mode 100644 index 0000000..cf40571 --- /dev/null +++ b/Cauldron.Macos/SourceWriter/FormatDescriptorType.cs @@ -0,0 +1,17 @@ +namespace Cauldron.Macos.SourceWriter; + +public enum FormatDescriptorType +{ + /// + /// Defines a format that starts with a given character sequence and runs to + /// the end of the line. + /// + Prefix, + + /// + /// Defines a format that is enclosed between a starting and ending character + /// sequence. + /// + Enclosure +} + diff --git a/Cauldron.Macos/SourceWriter/KeywordDescriptor.cs b/Cauldron.Macos/SourceWriter/KeywordDescriptor.cs new file mode 100644 index 0000000..463bd12 --- /dev/null +++ b/Cauldron.Macos/SourceWriter/KeywordDescriptor.cs @@ -0,0 +1,48 @@ +using AppKit; + +namespace Cauldron.Macos.SourceWriter; + +public class KeywordDescriptor +{ + #region Computed Properties + + /// Gets or sets the KeywordType. + /// The type. + public KeywordType Type { get; set; } = KeywordType.Keyword; + + /// + /// Gets or sets the NSColor that the + /// will set this keyword to. + /// + /// The NSColor. + public NSColor Color { get; set; } = NSColor.Black; + + /// Gets or sets the tooltip used to define this keyword. + /// The tooltip. + public string Tooltip { get; set; } = ""; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public KeywordDescriptor() { } + + /// + /// Initializes a new instance of the class. + /// + /// Specifies the KeywordType. + /// Specifies the NSColor that this keyword will be set to. + /// Defines the tool tip for this keyword. + public KeywordDescriptor(KeywordType type, NSColor color, string toolTip) + { + this.Type = type; + this.Color = color; + this.Tooltip = toolTip; + } + + #endregion +} + diff --git a/Cauldron.Macos/SourceWriter/KeywordType.cs b/Cauldron.Macos/SourceWriter/KeywordType.cs new file mode 100644 index 0000000..3e38f95 --- /dev/null +++ b/Cauldron.Macos/SourceWriter/KeywordType.cs @@ -0,0 +1,104 @@ +namespace Cauldron.Macos.SourceWriter; + +public enum KeywordType +{ + /// + /// A generic keyword that doesn't fall under one of the other types. + /// + Keyword, + + /// + /// The generic variable type keyword such as var. + /// + Type, + + /// + /// A variable type keyword such as string or int. + /// + ValueType, + + /// + /// A reference variable type such as object. + /// + ReferenceType, + + /// + /// An access modifier keyword such as public or private. + /// + AccessModifier, + + /// + /// A geeneric modifier type of keyword. + /// + Modifier, + + /// + /// A selection statement keyword such as if. + /// + SelectionStatement, + + /// + /// An iteration statement keyword such as for. + /// + IterationStatement, + + /// + /// A jump statement keyword such as break. + /// + JumpStatement, + + /// + /// A exception handling statement keyword such as try or catch. + /// + ExceptionHandlingStatement, + + /// + /// A generic statement keyword. + /// + Statement, + + /// + /// A method parameters ketword such as out. + /// + MethodParameters, + + /// + /// A namespace keyword. + /// + NamespaceKeyword, + + /// + /// An operator keyword such as sizeof. + /// + OperatorKeyword, + + /// + /// A conversion keyword such as explicit. + /// + ConversionKeyword, + + /// + /// An access keyword such as this. + /// + AccessKeywords, + + /// + /// A literal keyword such as null. + /// + LiteralKeywords, + + /// + /// A contextual keyword such as get or set. + /// + ContextualKeywords, + + /// + /// A query keywords such as select. + /// + QueryKeywords, + + /// + /// A preprocessor directive keyword like #if. + /// + PreprocessorDirective +} diff --git a/Cauldron.Macos/SourceWriter/LanguageClosure.cs b/Cauldron.Macos/SourceWriter/LanguageClosure.cs new file mode 100644 index 0000000..bab2229 --- /dev/null +++ b/Cauldron.Macos/SourceWriter/LanguageClosure.cs @@ -0,0 +1,49 @@ +using Foundation; + +namespace Cauldron.Macos.SourceWriter +{ + public class LanguageClosure : NSObject + { + #region Computed Properties + + /// Gets or sets the starting character for this closure. + /// The starting character. + public char StartingCharacter { get; set; } + + /// Gets or sets the ending character for this closure. + /// The ending character. + public char EndingCharacter { get; set; } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public LanguageClosure() { } + + /// + /// Initializes a new instance of the class. + /// + /// The character that both starts and ends this closure. + public LanguageClosure(char character) + { + this.StartingCharacter = character; + this.EndingCharacter = character; + } + + /// + /// Initializes a new instance of the class. + /// + /// The character that starts the closure. + /// The character that ends the closure. + public LanguageClosure(char startingCharacter, char endingCharacter) + { + this.StartingCharacter = startingCharacter; + this.EndingCharacter = endingCharacter; + } + + #endregion + } +} diff --git a/Cauldron.Macos/SourceWriter/LanguageDescriptor.cs b/Cauldron.Macos/SourceWriter/LanguageDescriptor.cs new file mode 100644 index 0000000..eacc3a1 --- /dev/null +++ b/Cauldron.Macos/SourceWriter/LanguageDescriptor.cs @@ -0,0 +1,522 @@ +using System; +using AppKit; +using Foundation; +using System.Collections.Generic; + +namespace Cauldron.Macos.SourceWriter; + +[Register("LanguageDescriptor")] +public class LanguageDescriptor : NSObject +{ + #region Computed Properties + + /// Gets the language identifier. + /// The language identifier. + public virtual string LanguageIdentifier { get => "Default"; } + + /// + /// Gets or sets the language separators that can be used to define a "word" in the given + /// language. + /// + /// The language separators. + public virtual char[] LanguageSeparators { get; set; } = new char[] { '.' }; + + /// + /// Gets the escape character for the given language. + /// + /// The escape character. + public virtual char EscapeCharacter { get => '\\'; } + + /// + /// Gets or sets the collection of used to define + /// the keywords for this language. + /// + /// The keywords. + public Dictionary Keywords { get; set; } = new Dictionary(); + + /// + /// Gets or sets the collection of formats used to syntax + /// highlight this language. + /// + /// The formats. + public List Formats { get; set; } = new List(); + + /// + /// Gets or sets the collection of used to auto complete to + /// closure of text such as (), [], "" or ''. + /// + /// The closures. + public List Closures { get; set; } = new List(); + + /// + /// Gets or sets the formatting commands that can be added to the user interface for the user to + /// select and apply to a selection of text in the editor. + /// + /// The items. + public List FormattingCommands { get; set; } + = new List(); + + /// Gets or sets the color of generic keywords. + /// The NSColor of the keyword. + [Export("KeywordColor")] + public NSColor KeywordColor + { + get => LoadColor("KeywordColor", NSColor.FromRgba(0.06f, 0.52f, 0.50f, 1.0f)); + set + { + WillChangeValue("KeywordColor"); + SaveColor("KeywordColor", value, true); + DidChangeValue("KeywordColor"); + } + } + + /// Gets or sets the color of generic keyword type. + /// The NSColor of the type. + [Export("TypeColor")] + public NSColor TypeColor + { + get => LoadColor("TypeColor", NSColor.FromRgba(0.06f, 0.52f, 0.50f, 1.0f)); + set + { + WillChangeValue("TypeColor"); + SaveColor("TypeColor", value, true); + DidChangeValue("TypeColor"); + } + } + + /// Gets or sets the color of a value type keyword. + /// The NSColor of the value type. + [Export("ValueTypeColor")] + public NSColor ValueTypeColor + { + get => LoadColor("ValueTypeColor", NSColor.Blue); + set + { + WillChangeValue("ValueTypeColor"); + SaveColor("ValueTypeColor", value, true); + DidChangeValue("ValueTypeColor"); + } + } + + /// Gets or sets the color of a reference type keyword. + /// The NSColor of the reference type. + [Export("ReferenceTypeColor")] + public NSColor ReferenceTypeColor + { + get => LoadColor("ReferenceTypeColor", NSColor.FromRgba(0f, 0.56f, 0.80f, 1.0f)); + set + { + WillChangeValue("ReferenceTypeColor"); + SaveColor("ReferenceTypeColor", value, true); + DidChangeValue("ReferenceTypeColor"); + } + } + + /// Gets or sets the color of a access modifier keyword. + /// The NSColor of the access modifier. + [Export("AccessModifierColor")] + public NSColor AccessModifierColor + { + get => LoadColor("AccessModifierColor", NSColor.FromRgba(0.06f, 0.52f, 0.50f, 1.0f)); + set + { + WillChangeValue("AccessModifierColor"); + SaveColor("AccessModifierColor", value, true); + DidChangeValue("AccessModifierColor"); + } + } + + /// Gets or sets the color of a generic modifier keyword. + /// The NSColor of the modifier. + [Export("ModifierColor")] + public NSColor ModifierColor + { + get => LoadColor("ModifierColor", NSColor.FromRgba(0.06f, 0.52f, 0.50f, 1.0f)); + set + { + WillChangeValue("ModifierColor"); + SaveColor("ModifierColor", value, true); + DidChangeValue("ModifierColor"); + } + } + + /// Gets or sets the color of a selection statement keyword. + /// The NSColor of the selection statement. + [Export("SelectionStatementColor")] + public NSColor SelectionStatementColor + { + get => LoadColor("SelectionStatementColor", NSColor.FromRgba(0.50f, 0.25f, 0f, 1.0f)); + set + { + WillChangeValue("SelectionStatementColor"); + SaveColor("SelectionStatementColor", value, true); + DidChangeValue("SelectionStatementColor"); + } + } + + /// Gets or sets the color of a iteration statement keyword. + /// The NSColor of the iteration statement. + [Export("IterationStatementColor")] + public NSColor IterationStatementColor + { + get => LoadColor("IterationStatementColor", NSColor.FromRgba(0.50f, 0f, 0f, 1.0f)); + set + { + WillChangeValue("IterationStatementColor"); + SaveColor("IterationStatementColor", value, true); + DidChangeValue("IterationStatementColor"); + } + } + + /// Gets or sets the color of a jump statement keyword. + /// The NSColor of the jump statement. + [Export("JumpStatementColor")] + public NSColor JumpStatementColor + { + get => LoadColor("JumpStatementColor", NSColor.FromRgba(0.50f, 0.50f, 0.0f, 1.0f)); + set + { + WillChangeValue("JumpStatementColor"); + SaveColor("JumpStatementColor", value, true); + DidChangeValue("JumpStatementColor"); + } + } + + /// Gets or sets the color of a exception handling keyword. + /// The NSColor of the exception handling. + [Export("ExceptionHandlingColor")] + public NSColor ExceptionHandlingColor + { + get => LoadColor("ExceptionHandlingColor", NSColor.FromRgba(1f, 0f, 0f, 1.0f)); + set + { + WillChangeValue("ExceptionHandlingColor"); + SaveColor("ExceptionHandlingColor", value, true); + DidChangeValue("ExceptionHandlingColor"); + } + } + + /// Gets or sets the color of a generic statement keyword. + /// The NSColor of the statement. + [Export("StatementColor")] + public NSColor StatementColor + { + get => LoadColor("StatementColor", NSColor.FromRgba(1f, 0f, 0.50f, 1.0f)); + set + { + WillChangeValue("StatementColor"); + SaveColor("StatementColor", value, true); + DidChangeValue("StatementColor"); + } + } + + /// Gets or sets the color of a method parameter keyword. + /// The NSColor of the method parameter. + [Export("MethodParameterColor")] + public NSColor MethodParameterColor + { + get => LoadColor("MethodParameterColor", NSColor.FromRgba(0.06f, 0.52f, 0.50f, 1.0f)); + set + { + WillChangeValue("MethodParameterColor"); + SaveColor("MethodParameterColor", value, true); + DidChangeValue("MethodParameterColor"); + } + } + + /// Gets or sets the color of a namespace keyword. + /// The NSColor of the namespace. + [Export("NamespaceColor")] + public NSColor NamespaceColor + { + get => LoadColor("NamespaceColor", NSColor.FromRgba(0.06f, 0.52f, 0.50f, 1.0f)); + set + { + WillChangeValue("NamespaceColor"); + SaveColor("NamespaceColor", value, true); + DidChangeValue("NamespaceColor"); + } + } + + /// Gets or sets the color of a operator keyword. + /// The NSColor of the operator keyword. + [Export("OperatorKeywordColor")] + public NSColor OperatorKeywordColor + { + get => LoadColor("OperatorKeywordColor", NSColor.FromRgba(0.80f, 0.40f, 1f, 1.0f)); + set + { + WillChangeValue("OperatorKeywordColor"); + SaveColor("OperatorKeywordColor", value, true); + DidChangeValue("OperatorKeywordColor"); + } + } + + /// Gets or sets the color of a conversion keyword. + /// The NSColor of the conversion keyword. + [Export("ConversionKeywordColor")] + public NSColor ConversionKeywordColor + { + get => LoadColor("ConversionKeywordColor", NSColor.Purple); + set + { + WillChangeValue("ConversionKeywordColor"); + SaveColor("ConversionKeywordColor", value, true); + DidChangeValue("ConversionKeywordColor"); + } + } + + /// Gets or sets the color of a access keyword. + /// The NSColor of the access keyword. + [Export("AccessKeywordColor")] + public NSColor AccessKeywordColor + { + get => LoadColor("AccessKeywordColor", NSColor.Purple); + set + { + WillChangeValue("AccessKeywordColor"); + SaveColor("AccessKeywordColor", value, true); + DidChangeValue("AccessKeywordColor"); + } + } + + /// Gets or sets the color of a literal keyword. + /// The NSColor of the literal keyword. + [Export("LiteralKeywordColor")] + public NSColor LiteralKeywordColor + { + get => LoadColor("LiteralKeywordColor", NSColor.Purple); + set + { + WillChangeValue("LiteralKeywordColor"); + SaveColor("LiteralKeywordColor", value, true); + DidChangeValue("LiteralKeywordColor"); + } + } + + /// Gets or sets the color of a contextual keyword. + /// The NSColor of the contextual keyword. + [Export("ContextualKeywordColor")] + public NSColor ContextualKeywordColor + { + get => LoadColor("ContextualKeywordColor", NSColor.FromRgba(0f, 0.50f, 0.25f, 1.0f)); + set + { + WillChangeValue("ContextualKeywordColor"); + SaveColor("ContextualKeywordColor", value, true); + DidChangeValue("ContextualKeywordColor"); + } + } + + /// Gets or sets the color of a query keyword. + /// The NSColor of the query keyword. + [Export("QueryKeywordColor")] + public NSColor QueryKeywordColor + { + get => LoadColor("QueryKeywordColor", NSColor.Orange); + set + { + WillChangeValue("QueryKeywordColor"); + SaveColor("QueryKeywordColor", value, true); + DidChangeValue("QueryKeywordColor"); + } + } + + /// Gets or sets the color of a preprocessor directive keyword. + /// The NSColor of the preprocessor directive. + [Export("PreprocessorDirectiveColor")] + public NSColor PreprocessorDirectiveColor + { + get => LoadColor("PreprocessorDirectiveColor", NSColor.FromRgba(0.69f, 0.03f, 0.61f, 1.0f)); + set + { + WillChangeValue("PreprocessorDirectiveColor"); + SaveColor("PreprocessorDirectiveColor", value, true); + DidChangeValue("PreprocessorDirectiveColor"); + } + } + + /// Gets or sets the color of a comment. + /// The NSColor of the comment. + [Export("CommentColor")] + public NSColor CommentColor + { + get => LoadColor("CommentColor", NSColor.Gray); + set + { + WillChangeValue("CommentColor"); + SaveColor("CommentColor", value, true); + DidChangeValue("CommentColor"); + } + } + + /// Gets or sets the color of a string literal. + /// The NSColor of the string literal. + [Export("StringLiteralColor")] + public NSColor StringLiteralColor + { + get => LoadColor("StringLiteralColor", NSColor.Orange); + set + { + WillChangeValue("StringLiteralColor"); + SaveColor("StringLiteralColor", value, true); + DidChangeValue("StringLiteralColor"); + } + } + + #endregion + + #region Constructors + /// + /// Initializes a new instance of the class. + /// + public LanguageDescriptor() + { + } + #endregion + + #region Public Methods + + /// Define this instance. + public virtual void Define() + { + // Initialize + this.Keywords.Clear(); + this.Formats.Clear(); + this.Closures.Clear(); + this.FormattingCommands.Clear(); + + // Define the default set of closures + this.Closures.Add(new LanguageClosure('(', ')')); + this.Closures.Add(new LanguageClosure('[', ']')); + this.Closures.Add(new LanguageClosure('<', '>')); + this.Closures.Add(new LanguageClosure('{', '}')); + this.Closures.Add(new LanguageClosure('"')); + } + + /// Formats the passed in string of text for previewing. + /// The string formatted for preview. + /// Text. + public virtual string FormatForPreview(string text) + { + return text; + } + + /// + /// Resets all of the for this language to their default states + /// of unmatched and inactive. + /// + /// This should only be called ba a . + public virtual void ClearFormats() + { + // Clear the process state of all formats + foreach (FormatDescriptor format in Formats) + { + format.CharIndex = 0; + format.Active = false; + } + } + + /// + /// Converts the given color into a web style hex string in the form #RRBBGG or optionally + /// #RRBBGGAA. + /// + /// The web hex string representing the given color. + /// The NSColor to convert. + /// + /// If set to true with the alpha (transparency) of the color will be included. + /// + public static string NSColorToHexString(NSColor color, bool withAlpha) + { + // Break color into pieces + color.GetRgba(out nfloat red, out nfloat green, out nfloat blue, out nfloat alpha); + + // Adjust to byte + alpha *= 255; + red *= 255; + green *= 255; + blue *= 255; + + //With the alpha value? + if (withAlpha) + { + return string.Format("#{0:X2}{1:X2}{2:X2}{3:X2}", + (int)alpha, (int)red, (int)green, (int)blue); + } + else + { + return string.Format("#{0:X2}{1:X2}{2:X2}", (int)red, (int)green, (int)blue); + } + } + + /// + /// Converts a web formatted hex string in the form #RRGGBB or #RRGGBBAA into a color. + /// + /// The NSColor represented by the hex string. + /// The web formatted hex string in the form #RRGGBB or #RRGGBBAA. + public static NSColor NSColorFromHexString(string hexValue) + { + string colorString = hexValue.Replace("#", ""); + float red, green, blue, alpha; + + // Convert color based on length + switch (colorString.Length) + { + case 3: // #RGB + red = Convert.ToInt32(string.Format("{0}{0}", colorString.Substring(0, 1)), 16) / 255f; + green = Convert.ToInt32(string.Format("{0}{0}", colorString.Substring(1, 1)), 16) / 255f; + blue = Convert.ToInt32(string.Format("{0}{0}", colorString.Substring(2, 1)), 16) / 255f; + return NSColor.FromRgba(red, green, blue, 1.0f); + case 6: // #RRGGBB + red = Convert.ToInt32(colorString.Substring(0, 2), 16) / 255f; + green = Convert.ToInt32(colorString.Substring(2, 2), 16) / 255f; + blue = Convert.ToInt32(colorString.Substring(4, 2), 16) / 255f; + return NSColor.FromRgba(red, green, blue, 1.0f); + case 8: // #AARRGGBB + alpha = Convert.ToInt32(colorString.Substring(0, 2), 16) / 255f; + red = Convert.ToInt32(colorString.Substring(2, 2), 16) / 255f; + green = Convert.ToInt32(colorString.Substring(4, 2), 16) / 255f; + blue = Convert.ToInt32(colorString.Substring(6, 2), 16) / 255f; + return NSColor.FromRgba(red, green, blue, alpha); + default: + throw new ArgumentOutOfRangeException(string.Format("Invalid color value '{0}'. " + + "It should be a hex value of the form #RBG, #RRGGBB or #AARRGGBB", hexValue)); + } + } + + /// Loads the requested color from system-wide user defaults. + /// The NSColor for the given key or the default value if the key + /// cannot be found in the user defaults. + /// The user default key for the color. + /// The default NSColor value. + public NSColor LoadColor(string key, NSColor defaultValue) + { + // Attempt to read color, add the language ID to make unique + string hex = NSUserDefaults.StandardUserDefaults.StringForKey(LanguageIdentifier + key); + + // Take action based on value + if (hex == null) + { + return defaultValue; + } + else + { + return NSColorFromHexString(hex); + } + } + + /// + /// Saves the given color to the systwm-wide user defaults with the give keyword. + /// + /// The NSColor to save to the user defaults. + /// The user default key to assign the color to. + /// If set to true sync changes to preferences. + public void SaveColor(string key, NSColor color, bool sync) + { + // Save to default, add the language ID to make unique + NSUserDefaults.StandardUserDefaults.SetString(NSColorToHexString(color, true), LanguageIdentifier + key); + if (sync) NSUserDefaults.StandardUserDefaults.Synchronize(); + } + + #endregion +} diff --git a/Cauldron.Macos/SourceWriter/LanguageFormatCommand.cs b/Cauldron.Macos/SourceWriter/LanguageFormatCommand.cs new file mode 100644 index 0000000..e974a2a --- /dev/null +++ b/Cauldron.Macos/SourceWriter/LanguageFormatCommand.cs @@ -0,0 +1,81 @@ +using Foundation; +using System.Collections.Generic; + +namespace Cauldron.Macos.SourceWriter; + +public class LanguageFormatCommand : NSObject +{ + #region Computed Properties + + /// Gets or sets the title that will appear in the Formatting Menu. + /// The title. + public string Title { get; set; } = ""; + + /// + /// Gets or sets the prefix that will be added to the start of the line (if no Postfix + /// has been defines), or that will be inserted to the start of the current selected text in the + /// document editor. + /// + /// The prefix. + public string Prefix { get; set; } = ""; + + /// + /// Gets or sets the postfix that will added to the end of the selected text in the document + /// editor. If empty (""), the Prefix will be inserted at the start of the line that the + /// cursor is on. + /// + /// The postfix. + public string Postfix { get; set; } = ""; + + /// + /// Gets or sets the sub commands that will be displayed + /// under this item in the Formatting Menu. + /// + /// The sub commands. + public List SubCommands { get; set; } + = new List(); + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public LanguageFormatCommand() { } + + /// + /// Initializes a new instance of the class. + /// + /// The title for the menu item. + public LanguageFormatCommand(string title) + { + this.Title = title; + } + + /// + /// Initializes a new instance of the class. + /// + /// The title for the menu item. + /// The prefix to insert. + public LanguageFormatCommand(string title, string prefix) + { + this.Title = title; + this.Prefix = prefix; + } + + /// + /// Initializes a new instance of the class. + /// + /// The title for the menu item. + /// The prefix to insert. + /// The postfix to insert. + public LanguageFormatCommand(string title, string prefix, string postfix) + { + this.Title = title; + this.Prefix = prefix; + this.Postfix = postfix; + } + + #endregion +} diff --git a/Cauldron.Macos/SourceWriter/LanguageFormats/CSharpDescriptor.cs b/Cauldron.Macos/SourceWriter/LanguageFormats/CSharpDescriptor.cs new file mode 100644 index 0000000..19c17f6 --- /dev/null +++ b/Cauldron.Macos/SourceWriter/LanguageFormats/CSharpDescriptor.cs @@ -0,0 +1,241 @@ +namespace Cauldron.Macos.SourceWriter.LanguageFormats; + +public class CSharpDescriptor : LanguageDescriptor +{ + #region Computed Properties + + /// Gets the language identifier. + /// The language identifier. + public override string LanguageIdentifier { get => "CSharp"; } + + /// Gets or sets the language separators for C# + /// The language separators. + public override char[] LanguageSeparators { get; set; } = new char[] + { + '=', '+', '-', '*', '/', '%', '&', '<', '>', ';', ':', '^', '!', '~', '?', '|', ',', '"', + '\'', '(', ')', '[', ']', '{', '}' + }; + + #endregion + + #region Constructors + + /// Initializes a new instance of the class. + public CSharpDescriptor() { } + + #endregion + + #region Override Methods + + /// Define this instance. + public override void Define() + { + // Call base class + base.Define(); + + // Value Types + // Keywords.Add("", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "")); + Keywords.Add("bool", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "It is used to declare variables to store the Boolean values, true and false.")); + Keywords.Add("byte", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The byte keyword denotes an integral type that stores values between 0 to 255.")); + Keywords.Add("char", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The char keyword is used to declare an instance of the System.Char structure that the .NET Framework uses to represent a Unicode character.")); + Keywords.Add("decimal", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The decimal keyword denotes an integral type that stores values between (-7.9 x 10^28 to 7.9 x 10^28) / (10^(0 to 28)).")); + Keywords.Add("double", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The double keyword signifies a simple type that stores 64-bit floating-point values.")); + Keywords.Add("enum", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The enum keyword is used to declare an enumeration, a distinct type that consists of a set of named constants called the enumerator list.")); + Keywords.Add("float", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The float keyword signifies a simple type that stores 32-bit floating-point values. ")); + Keywords.Add("int", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The int keyword denotes an integral type that stores values between -2,147,483,648 to 2,147,483,647.")); + Keywords.Add("long", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The long keyword denotes an integral type that stores values between –9,223,372,036,854,775,808 to 9,223,372,036,854,775,807.")); + Keywords.Add("sbyte", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The sbyte keyword indicates an integral type that stores values between -128 to 127.")); + Keywords.Add("short", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The short keyword denotes an integral data type that stores values between -32,768 to 32,767.")); + Keywords.Add("struct", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "A struct type is a value type that is typically used to encapsulate small groups of related variables, such as the coordinates of a rectangle or the characteristics of an item in an inventory.")); + Keywords.Add("uint", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The uint keyword signifies an integral type that stores values between 0 to 4,294,967,295.")); + Keywords.Add("ulong", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The ulong keyword denotes an integral type that stores values between 0 to 18,446,744,073,709,551,615.")); + Keywords.Add("ushort", new KeywordDescriptor(KeywordType.ValueType, ValueTypeColor, "The ushort keyword indicates an integral data type that stores values between 0 to 65,535.")); + + // Reference Types + // Keywords.Add("", new KeywordDescriptor(KeywordType.ReferenceType, ReferenceTypeColor, "")); + Keywords.Add("class", new KeywordDescriptor(KeywordType.ReferenceType, ReferenceTypeColor, "Classes are declared using the keyword class.")); + Keywords.Add("delegate", new KeywordDescriptor(KeywordType.ReferenceType, ReferenceTypeColor, "The declaration of a delegate type is similar to a method signature. It has a return value and any number of parameters of any type.")); + Keywords.Add("dynamic", new KeywordDescriptor(KeywordType.ReferenceType, ReferenceTypeColor, "The dynamic type enables the operations in which it occurs to bypass compile-time type checking. Instead, these operations are resolved at run time.")); + Keywords.Add("interface", new KeywordDescriptor(KeywordType.ReferenceType, ReferenceTypeColor, "An interface contains only the signatures of methods, properties, events or indexers. ")); + Keywords.Add("object", new KeywordDescriptor(KeywordType.ReferenceType, ReferenceTypeColor, "In the unified type system of C#, all types, predefined and user-defined, reference types and value types, inherit directly or indirectly from Object. You can assign values of any type to variables of type object. ")); + Keywords.Add("string", new KeywordDescriptor(KeywordType.ReferenceType, ReferenceTypeColor, "The string type represents a sequence of zero or more Unicode characters. string is an alias for String in the .NET Framework.")); + + // Generic Types + // Keywords.Add("", new KeywordDescriptor(KeywordType.Type, TypeColor, "")); + Keywords.Add("void", new KeywordDescriptor(KeywordType.Type, TypeColor, "When used as the return type for a method, void specifies that the method doesn't return a value.")); + Keywords.Add("var", new KeywordDescriptor(KeywordType.Type, TypeColor, "An implicitly typed local variable is strongly typed just as if you had declared the type yourself, but the compiler determines the type.")); + + // Access modifiers + // Keywords.Add("", new KeywordDescriptor(KeywordType.AccessModifier, AccessModifierColor, "")); + Keywords.Add("public", new KeywordDescriptor(KeywordType.AccessModifier, AccessModifierColor, "The public keyword is an access modifier for types and type members. There are no restrictions on accessing public members.")); + Keywords.Add("private", new KeywordDescriptor(KeywordType.AccessModifier, AccessModifierColor, "Private members are accessible only within the body of the class or the struct in which they are declared.")); + Keywords.Add("internal", new KeywordDescriptor(KeywordType.AccessModifier, AccessModifierColor, "Internal types or members are accessible only within files in the same assembly.")); + Keywords.Add("protected", new KeywordDescriptor(KeywordType.AccessModifier, AccessModifierColor, "A protected member is accessible within its class and by derived class instances.")); + + // Modifiers + // Keywords.Add("", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "")); + Keywords.Add("abstract", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "The abstract modifier indicates that the thing being modified has a missing or incomplete implementation.")); + Keywords.Add("async", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "Use the async modifier to specify that a method, lambda expression, or anonymous method is asynchronous. ")); + Keywords.Add("const", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "You use the const keyword to declare a constant field or a constant local. Constant fields and locals aren't variables and may not be modified. ")); + Keywords.Add("event", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "The event keyword is used to declare an event in a publisher class.")); + Keywords.Add("extern", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "The extern modifier is used to declare a method that is implemented externally. ")); + Keywords.Add("in", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "For generic type parameters, the in keyword specifies that the type parameter is contravariant. You can use the in keyword in generic interfaces and delegates.")); + Keywords.Add("override", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "The override modifier is required to extend or modify the abstract or virtual implementation of an inherited method, property, indexer, or event.")); + Keywords.Add("readonly", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "When a field declaration includes a readonly modifier, assignments to the fields introduced by the declaration can only occur as part of the declaration or in a constructor in the same class.")); + Keywords.Add("sealed", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "When applied to a class, the sealed modifier prevents other classes from inheriting from it.")); + Keywords.Add("static", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "Use the static modifier to declare a static member, which belongs to the type itself rather than to a specific object. ")); + Keywords.Add("unsafe", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "The unsafe keyword denotes an unsafe context, which is required for any operation involving pointers.")); + Keywords.Add("virtual", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "The virtual keyword is used to modify a method, property, indexer, or event declaration and allow for it to be overridden in a derived class. ")); + Keywords.Add("volatile", new KeywordDescriptor(KeywordType.Modifier, ModifierColor, "The volatile keyword indicates that a field might be modified by multiple threads that are executing at the same time. ")); + + // Selection Statements + // Keywords.Add("", new KeywordDescriptor(KeywordType.SelectionStatement, SelectionStatementColor, "")); + Keywords.Add("if", new KeywordDescriptor(KeywordType.SelectionStatement, SelectionStatementColor, "An if statement identifies which statement to run based on the value of a Boolean expression.")); + Keywords.Add("else", new KeywordDescriptor(KeywordType.SelectionStatement, SelectionStatementColor, "In an if-else statement, if condition evaluates to true, the then-statement runs. If condition is false, the else-statement runs. ")); + Keywords.Add("switch", new KeywordDescriptor(KeywordType.SelectionStatement, SelectionStatementColor, "The switch statement is a control statement that selects a switch section to execute from a list of candidates.")); + Keywords.Add("case", new KeywordDescriptor(KeywordType.SelectionStatement, SelectionStatementColor, "Each case label specifies a constant value.")); + Keywords.Add("default", new KeywordDescriptor(KeywordType.SelectionStatement, SelectionStatementColor, "f no case label contains a matching value, control is transferred to the default section, if there is one.")); + + // Iteration Statements + // Keywords.Add("", new KeywordDescriptor(KeywordType.IterationStatement, IterationStatementColor, "")); + Keywords.Add("do", new KeywordDescriptor(KeywordType.IterationStatement, IterationStatementColor, "The do statement executes a statement or a block of statements repeatedly until a specified expression evaluates to false. ")); + Keywords.Add("for", new KeywordDescriptor(KeywordType.IterationStatement, IterationStatementColor, "By using a for loop, you can run a statement or a block of statements repeatedly until a specified expression evaluates to false.")); + Keywords.Add("foreach", new KeywordDescriptor(KeywordType.IterationStatement, IterationStatementColor, "The foreach statement repeats a group of embedded statements for each element in an array or an object collection that implements the System.Collections.IEnumerable or System.Collections.Generic.IEnumerable interface. ")); + Keywords.Add("while", new KeywordDescriptor(KeywordType.IterationStatement, IterationStatementColor, "The while statement executes a statement or a block of statements until a specified expression evaluates to false.")); + + // Jump Statements + // Keywords.Add("", new KeywordDescriptor(KeywordType.JumpStatement, JumpStatementColor, "")); + Keywords.Add("break", new KeywordDescriptor(KeywordType.JumpStatement, JumpStatementColor, "The break statement terminates the closest enclosing loop or switch statement in which it appears. Control is passed to the statement that follows the terminated statement, if any.")); + Keywords.Add("continue", new KeywordDescriptor(KeywordType.JumpStatement, JumpStatementColor, "The continue statement passes control to the next iteration of the enclosing while, do, for, or foreach statement in which it appears.")); + Keywords.Add("goto", new KeywordDescriptor(KeywordType.JumpStatement, JumpStatementColor, "The goto statement transfers the program control directly to a labeled statement.")); + Keywords.Add("return", new KeywordDescriptor(KeywordType.JumpStatement, JumpStatementColor, "The return statement terminates execution of the method in which it appears and returns control to the calling method. It can also return an optional value.")); + + // Exception Handling Statements + // Keywords.Add("", new KeywordDescriptor(KeywordType.ExceptionHandlingStatement, ExceptionHandlingColor, "")); + Keywords.Add("throw", new KeywordDescriptor(KeywordType.ExceptionHandlingStatement, ExceptionHandlingColor, "The throw statement is used to signal the occurrence of an anomalous situation (exception) during the program execution.")); + Keywords.Add("try", new KeywordDescriptor(KeywordType.ExceptionHandlingStatement, ExceptionHandlingColor, "The try-catch statement consists of a try block followed by one or more catch clauses, which specify handlers for different exceptions.")); + Keywords.Add("catch", new KeywordDescriptor(KeywordType.ExceptionHandlingStatement, ExceptionHandlingColor, "The try-catch statement consists of a try block followed by one or more catch clauses, which specify handlers for different exceptions.")); + Keywords.Add("finally", new KeywordDescriptor(KeywordType.ExceptionHandlingStatement, ExceptionHandlingColor, "A common usage of catch and finally together is to obtain and use resources in a try block, deal with exceptional circumstances in a catch block, and release the resources in the finally block.")); + + // Statements + // Keywords.Add("", new KeywordDescriptor(KeywordType.Statement, StatementColor, "")); + Keywords.Add("checked", new KeywordDescriptor(KeywordType.Statement, StatementColor, "The checked keyword is used to explicitly enable overflow checking for integral-type arithmetic operations and conversions.")); + Keywords.Add("unchecked", new KeywordDescriptor(KeywordType.Statement, StatementColor, "The unchecked keyword is used to suppress overflow-checking for integral-type arithmetic operations and conversions.")); + Keywords.Add("fixed", new KeywordDescriptor(KeywordType.Statement, StatementColor, "The fixed statement prevents the garbage collector from relocating a movable variable. The fixed statement is only permitted in an unsafe context.")); + Keywords.Add("lock", new KeywordDescriptor(KeywordType.Statement, StatementColor, "The lock keyword marks a statement block as a critical section by obtaining the mutual-exclusion lock for a given object, executing a statement, and then releasing the lock.")); + + // Method Parameters + // Keywords.Add("", new KeywordDescriptor(KeywordType.MethodParameters, MethodParameterColor, "")); + Keywords.Add("params", new KeywordDescriptor(KeywordType.MethodParameters, MethodParameterColor, "By using the params keyword, you can specify a method parameter that takes a variable number of arguments.")); + Keywords.Add("ref", new KeywordDescriptor(KeywordType.MethodParameters, MethodParameterColor, "The ref keyword causes an argument to be passed by reference, not by value.")); + Keywords.Add("out", new KeywordDescriptor(KeywordType.MethodParameters, MethodParameterColor, "The out keyword causes arguments to be passed by reference. This is like the ref keyword, except that ref requires that the variable be initialized before it is passed. ")); + + // Namespace Keywords + // Keywords.Add("", new KeywordDescriptor(KeywordType.NamespaceKeyword, NamespaceColor, "")); + Keywords.Add("namespace", new KeywordDescriptor(KeywordType.NamespaceKeyword, NamespaceColor, "The namespace keyword is used to declare a scope that contains a set of related objects.")); + Keywords.Add("using", new KeywordDescriptor(KeywordType.MethodParameters, NamespaceColor, "Allows the use of types in a namespace so that you do not have to qualify the use of a type in that namespace.")); + + // Operator Keywords + // Keywords.Add("", new KeywordDescriptor(KeywordType.OperatorKeyword, OperatorKeywordColor, "")); + Keywords.Add("as", new KeywordDescriptor(KeywordType.OperatorKeyword, OperatorKeywordColor, "You can use the as operator to perform certain types of conversions between compatible reference types or nullable types.")); + Keywords.Add("await", new KeywordDescriptor(KeywordType.OperatorKeyword, OperatorKeywordColor, "The await operator is applied to a task in an asynchronous method to suspend the execution of the method until the awaited task completes.")); + Keywords.Add("is", new KeywordDescriptor(KeywordType.OperatorKeyword, OperatorKeywordColor, "Checks if an object is compatible with a given type.")); + Keywords.Add("new", new KeywordDescriptor(KeywordType.OperatorKeyword, OperatorKeywordColor, "Used to create objects and invoke constructors.")); + Keywords.Add("sizeof", new KeywordDescriptor(KeywordType.OperatorKeyword, OperatorKeywordColor, "Used to obtain the size in bytes for an unmanaged type.")); + Keywords.Add("typeof", new KeywordDescriptor(KeywordType.OperatorKeyword, OperatorKeywordColor, "Used to obtain the System.Type object for a type.")); + Keywords.Add("true", new KeywordDescriptor(KeywordType.OperatorKeyword, OperatorKeywordColor, "Represents the boolean value true.")); + Keywords.Add("false", new KeywordDescriptor(KeywordType.OperatorKeyword, OperatorKeywordColor, "Represents the boolean value false.")); + Keywords.Add("stackalloc", new KeywordDescriptor(KeywordType.OperatorKeyword, OperatorKeywordColor, "The stackalloc keyword is used in an unsafe code context to allocate a block of memory on the stack.")); + Keywords.Add("nameof", new KeywordDescriptor(KeywordType.OperatorKeyword, OperatorKeywordColor, "Used to obtain the simple (unqualified) string name of a variable, type, or member. ")); + + // Conversion Keywords + // Keywords.Add("", new KeywordDescriptor(KeywordType.ConversionKeyword, ConversionKeywordColor, "")); + Keywords.Add("explicit", new KeywordDescriptor(KeywordType.ConversionKeyword, ConversionKeywordColor, "The explicit keyword declares a user-defined type conversion operator that must be invoked with a cast. ")); + Keywords.Add("implicit", new KeywordDescriptor(KeywordType.ConversionKeyword, ConversionKeywordColor, "The implicit keyword is used to declare an implicit user-defined type conversion operator. ")); + Keywords.Add("operator", new KeywordDescriptor(KeywordType.ConversionKeyword, ConversionKeywordColor, "Use the operator keyword to overload a built-in operator or to provide a user-defined conversion in a class or struct declaration.")); + + // Access Keywords + // Keywords.Add("", new KeywordDescriptor(KeywordType.AccessKeywords, AccessKeywordColor, "")); + Keywords.Add("base", new KeywordDescriptor(KeywordType.AccessKeywords, AccessKeywordColor, "Accesses the members of the base class.")); + Keywords.Add("this", new KeywordDescriptor(KeywordType.AccessKeywords, AccessKeywordColor, "Refers to the current instance of the class.")); + + // Literal Keywords + // Keywords.Add("", new KeywordDescriptor(KeywordType.LiteralKeywords, LiteralKeywordColor, "")); + Keywords.Add("null", new KeywordDescriptor(KeywordType.LiteralKeywords, LiteralKeywordColor, "The null keyword is a literal that represents a null reference, one that does not refer to any object. null is the default value of reference-type variables.")); + + // Contextual Keywords + // Keywords.Add("", new KeywordDescriptor(KeywordType.ContextualKeywords, ContextualKeywordColor, "")); + Keywords.Add("add", new KeywordDescriptor(KeywordType.ContextualKeywords, ContextualKeywordColor, "The add contextual keyword is used to define a custom event accessor that is invoked when client code subscribes to your event.")); + Keywords.Add("get", new KeywordDescriptor(KeywordType.ContextualKeywords, ContextualKeywordColor, "The get keyword defines an accessor method in a property or indexer that retrieves the value of the property or the indexer element.")); + Keywords.Add("global", new KeywordDescriptor(KeywordType.ContextualKeywords, ContextualKeywordColor, "The global contextual keyword, when it comes before the :: operator, refers to the global namespace, which is the default namespace for any C# program and is otherwise unnamed.")); + Keywords.Add("partial", new KeywordDescriptor(KeywordType.ContextualKeywords, ContextualKeywordColor, "Partial type definitions allow for the definition of a class, struct, interface or method to be split into multiple files or definitions.")); + Keywords.Add("remove", new KeywordDescriptor(KeywordType.ContextualKeywords, ContextualKeywordColor, "The remove contextual keyword is used to define a custom event accessor that is invoked when client code unsubscribes from your event.")); + Keywords.Add("set", new KeywordDescriptor(KeywordType.ContextualKeywords, ContextualKeywordColor, "The set keyword defines an accessor method in a property or indexer that assigns the value of the property or the indexer element.")); + Keywords.Add("where", new KeywordDescriptor(KeywordType.ContextualKeywords, ContextualKeywordColor, "In a generic type definition, the where clause is used to specify constraints on the types that can be used as arguments for a type parameter defined in a generic declaration.")); + Keywords.Add("value", new KeywordDescriptor(KeywordType.ContextualKeywords, ContextualKeywordColor, "The contextual keyword value is used in the set accessor in ordinary property declarations. It is similar to an input parameter on a method. ")); + Keywords.Add("yield", new KeywordDescriptor(KeywordType.ContextualKeywords, ContextualKeywordColor, "When you use the yield keyword in a statement, you indicate that the method, operator, or get accessor in which it appears is an iterator. ")); + + // Query Keywords (LINQ) + // Keywords.Add("", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "")); + Keywords.Add("from", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "A query expression must begin with a from clause. Additionally, a query expression can contain sub-queries, which also begin with a from clause. ")); + Keywords.Add("select", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "In a query expression, the select clause specifies the type of values that will be produced when the query is executed.")); + Keywords.Add("group", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "The group clause returns a sequence of IGrouping objects that contain zero or more items that match the key value for the group.")); + Keywords.Add("into", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "The into contextual keyword can be used to create a temporary identifier to store the results of a group, join or select clause into a new identifier.")); + Keywords.Add("orderby", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "In a query expression, the orderby clause causes the returned sequence or subsequence (group) to be sorted in either ascending or descending order.")); + Keywords.Add("join", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "The join clause is useful for associating elements from different source sequences that have no direct relationship in the object model.")); + Keywords.Add("let", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "In a query expression, it is sometimes useful to store the result of a sub-expression in order to use it in subsequent clauses. You can do this with the let keyword.")); + Keywords.Add("ascending", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "The ascending contextual keyword is used in the orderby clause in query expressions to specify that the sort order is from smallest to largest.")); + Keywords.Add("descending", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "The descending contextual keyword is used in the orderby clause in query expressions to specify that the sort order is from largest to smallest.")); + Keywords.Add("on", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "The on contextual keyword is used in the join clause of a query expression to specify the join condition.")); + Keywords.Add("equals", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "The equals contextual keyword is used in a join clause in a query expression to compare the elements of two sequences.")); + Keywords.Add("by", new KeywordDescriptor(KeywordType.QueryKeywords, QueryKeywordColor, "The by contextual keyword is used in the group clause in a query expression to specify how the returned items should be grouped.")); + + // Preprocessor Directive + // Keywords.Add("", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "")); + Keywords.Add("#if", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "When the C# compiler encounters an #if directive, followed eventually by an #endif directive, it will compile the code between the directives only if the specified symbol is defined.")); + Keywords.Add("#else", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "#else lets you create a compound conditional directive, so that, if none of the expressions in the preceding #if or (optional) #elif directives to true, the compiler will evaluate all code between #else and the subsequent #endif.")); + Keywords.Add("#elif", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "#elif lets you create a compound conditional directive. The #elif expression will be evaluated if neither the preceding #if (C# Reference) nor any preceding, optional, #elif directive expressions evaluate to true.")); + Keywords.Add("#endif", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "#endif specifies the end of a conditional directive, which began with the #if directive.")); + Keywords.Add("#define", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "You use #define to define a symbol.")); + Keywords.Add("#undef", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "#undef lets you undefine a symbol, such that, by using the symbol as the expression in a #if directive, the expression will evaluate to false.")); + Keywords.Add("#warning", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "#warning lets you generate a level one warning from a specific location in your code.")); + Keywords.Add("#error", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "#error lets you generate an error from a specific location in your code.")); + Keywords.Add("#line", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "#line lets you modify the compiler's line number and (optionally) the file name output for errors and warnings. This example shows how to report two warnings associated with line numbers.")); + Keywords.Add("#region", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "#region lets you specify a block of code that you can expand or collapse when using the outlining feature.")); + Keywords.Add("#endregion", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "#endregion marks the end of a #region block.")); + Keywords.Add("#pragma", new KeywordDescriptor(KeywordType.PreprocessorDirective, PreprocessorDirectiveColor, "#pragma gives the compiler special instructions for the compilation of the file in which it appears.")); + + // Define formats + Formats.Add(new FormatDescriptor("//", CommentColor)); + Formats.Add(new FormatDescriptor("/*", "*/", CommentColor)); + Formats.Add(new FormatDescriptor("\"", "\"", StringLiteralColor)); + Formats.Add(new FormatDescriptor("'", "'", StringLiteralColor)); + + // Define formatting commands + FormattingCommands.Add(new LanguageFormatCommand("Comment", "//")); + FormattingCommands.Add(new LanguageFormatCommand("Block Comment", "/*", "*/")); + FormattingCommands.Add(new LanguageFormatCommand()); + + var Directives = new LanguageFormatCommand("Preprocessor Directives"); + Directives.SubCommands.Add(new LanguageFormatCommand("If", "#if \n", "\n#endif\n")); + Directives.SubCommands.Add(new LanguageFormatCommand("If..Else", "#if \n", "\n#else\n\n#endif\n")); + Directives.SubCommands.Add(new LanguageFormatCommand("Define", "#define ")); + Directives.SubCommands.Add(new LanguageFormatCommand("Undefine", "#undef")); + Directives.SubCommands.Add(new LanguageFormatCommand("Warning", "#warning")); + Directives.SubCommands.Add(new LanguageFormatCommand("Error", "#error")); + Directives.SubCommands.Add(new LanguageFormatCommand("Line", "#line")); + Directives.SubCommands.Add(new LanguageFormatCommand("Region", "#region \n", "\n#endregion\n")); + Directives.SubCommands.Add(new LanguageFormatCommand("Pragma", "#pragma")); + FormattingCommands.Add(Directives); + } + + /// Formats the passed in string of text for previewing. + /// The string formatted for preview. + /// Text. + public override string FormatForPreview(string text) + { + return "
" + text + "
"; + } + + #endregion +} diff --git a/Cauldron.Macos/SourceWriter/LanguageFormatter.cs b/Cauldron.Macos/SourceWriter/LanguageFormatter.cs new file mode 100644 index 0000000..8edaba2 --- /dev/null +++ b/Cauldron.Macos/SourceWriter/LanguageFormatter.cs @@ -0,0 +1,482 @@ +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 +} diff --git a/Cauldron.Macos/SourceWriter/SourceTextView.cs b/Cauldron.Macos/SourceWriter/SourceTextView.cs new file mode 100644 index 0000000..4447334 --- /dev/null +++ b/Cauldron.Macos/SourceWriter/SourceTextView.cs @@ -0,0 +1,762 @@ +using System; +using AppKit; +using CoreGraphics; +using Foundation; + +namespace Cauldron.Macos.SourceWriter; + +[Register("SourceTextView")] +public class SourceTextView : NSTextView +{ + #region Static Constants + + /// Defines the constant Unicode value of the enter key. + public const int EnterKey = 13; + + /// Defines the constant Unicode value of the tab key. + public const int TabKey = 9; + + /// Defines the constant Unicode value of the shift-tab key. + public const int ShiftTabKey = 25; + + #endregion + + #region Private Variables + + /// The current language formatter used to highlight syntax. + private LanguageFormatter _formatter; + + /// Should the editor auto complete closures. + private bool _completeClosures = true; + + /// Should the editor auto wrap selected text in. + private bool _wrapClosures = true; + + /// + /// Should the edit select the section of text that has just been wrapped in a closure. + /// + private bool _selectAfterWrap = true; + + /// Should the editor provide auto completion of partial words. + private bool _allowAutoComplete = true; + + /// + /// Should the editor auto complete keywords as defined in the current language. + /// + private bool _autoCompleteKeywords = true; + + /// Should the editor use the default words list for auto complete. + private bool _autoCompleteDefaultWords = true; + + /// Should the editor only use default words if the keyword list is empty. + private bool _defaultWordsOnlyIfKeywordsEmpty = true; + + #endregion + + #region Computed Properties + + /// + /// Gets or sets the used to perform + /// syntax highlighting on this NSTextView containing the contents of the document being + /// edited. + /// + /// The for the selected language. + [Export("Formatter")] + public LanguageFormatter Formatter + { + get { return _formatter; } + set + { + WillChangeValue("Formatter"); + _formatter = value; + DidChangeValue("Formatter"); + } + } + + /// + /// Gets or sets a value indicating whether this allows auto complete + /// of partial words. + /// + /// true if allows auto complete; otherwise, false. + [Export("AllowAutoComplete")] + public bool AllowAutoComplete + { + get { return _allowAutoComplete; } + set + { + WillChangeValue("AllowAutoComplete"); + _allowAutoComplete = value; + DidChangeValue("AllowAutoComplete"); + } + } + + /// + /// Gets or sets a value indicating whether this auto completes keywords. + /// + /// true if auto completes keywords; otherwise, false. + [Export("AutoCompleteKeywords")] + public bool AutoCompleteKeywords + { + get { return _autoCompleteKeywords; } + set + { + WillChangeValue("AutoCompleteKeywords"); + _autoCompleteKeywords = value; + DidChangeValue("AutoCompleteKeywords"); + } + } + + /// + /// Gets or sets a value indicating whether this auto completes + /// default words. + /// + /// true if auto complete default words; otherwise, false. + [Export("AutoCompleteDefaultWords")] + public bool AutoCompleteDefaultWords + { + get { return _autoCompleteDefaultWords; } + set + { + WillChangeValue("AutoCompleteDefaultWords"); + _autoCompleteDefaultWords = value; + DidChangeValue("AutoCompleteDefaultWords"); + } + } + + /// + /// Gets or sets a value indicating whether this + /// uses the default words (provided by OS X) only if keywords empty. + /// + /// true if use the default words only if keywords empty; otherwise, false. + [Export("DefaultWordsOnlyIfKeywordsEmpty")] + public bool DefaultWordsOnlyIfKeywordsEmpty + { + get { return _defaultWordsOnlyIfKeywordsEmpty; } + set + { + WillChangeValue("DefaultWordsOnlyIfKeywordsEmpty"); + _defaultWordsOnlyIfKeywordsEmpty = value; + DidChangeValue("DefaultWordsOnlyIfKeywordsEmpty"); + } + } + + /// + /// Gets or sets a value indicating whether this complete closures. + /// + /// true if complete closures; otherwise, false. + [Export("CompleteClosures")] + public bool CompleteClosures + { + get { return _completeClosures; } + set + { + WillChangeValue("CompleteClosures"); + _completeClosures = value; + DidChangeValue("CompleteClosures"); + } + } + + /// + /// Gets or sets a value indicating whether this wrap closures. + /// + /// true if wrap closures; otherwise, false. + [Export("WrapClosures")] + public bool WrapClosures + { + get { return _wrapClosures; } + set + { + WillChangeValue("WrapClosures"); + _wrapClosures = true; + DidChangeValue("WrapClosures"); + } + } + + /// + /// Gets or sets a value indicating whether this selects + /// the text that has just been wrapped in a closure. + /// + /// true if select after wrap; otherwise, false. + [Export("SelectAfterWrap")] + public bool SelectAfterWrap + { + get { return _selectAfterWrap; } + set + { + WillChangeValue("SelectAfterWrap"); + _selectAfterWrap = value; + DidChangeValue("SelectAfterWrap"); + } + } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public SourceTextView() + { + // Init + Initialize(); + } + + /// + /// Initializes a new instance of the class. + /// + /// Frame rect. + public SourceTextView(CGRect frameRect) : base(frameRect) + { + // Init + Initialize(); + } + + /// + /// Initializes a new instance of the class. + /// + /// Frame rect. + /// Container. + public SourceTextView(CGRect frameRect, NSTextContainer container) : base(frameRect, container) + { + // Init + Initialize(); + } + + /// + /// Initializes a new instance of the class. + /// + /// Coder. + public SourceTextView(NSCoder coder) : base(coder) + { + // Init + Initialize(); + } + + /// + /// Initializes a new instance of the class. + /// + /// Handle. + public SourceTextView(IntPtr handle) : base(handle) + { + // Init + Initialize(); + } + + /// + /// Initialize this instance. + /// + private void Initialize() + { + + // Init + this.Delegate = new SourceTextViewDelegate(this); + + } + + #endregion + + #region Private Methods + + /// + /// Calculates the indent level by counting the number of tab characters + /// at the start of the current line. + /// + /// The indent level as the number of tabs. + /// The line of text being processed. + private static int CalculateIndentLevel(string line) + { + int indent = 0; + + // Process all characters in the line + for (int n = 0; n < line.Length; ++n) + { + var code = (int)line[n]; + + // Are we on a tab character? + if (code == TabKey) + { + ++indent; + } + else + { + break; + } + } + + // Return result + return indent; + } + + /// + /// Creates a string of n number of tab characters that will be used to keep + /// the tab level of the current line of text. + /// + /// A string of n tab characters. + /// The number of tab characters to insert in the string. + private static string TabIndent(int indentLevel) + { + string indent = ""; + + // Assemble string + for (int n = 0; n < indentLevel; ++n) + { + indent += (char)TabKey; + } + + // Return indention + return indent; + } + + /// + /// Increases the tab indent on the given section of text. + /// + /// The text with the tab indent increased by one. + /// The text to indent. + private string IncreaseTabIndent(string text) + { + string output = ""; + + // Add first intent + output += (char)TabKey; + for (int n = 0; n < text.Length; ++n) + { + var c = text[n]; + bool found = c == Formatter.Newline + || c == Formatter.LineSeparator + || c == Formatter.ParagraphSeparator; + + // Include char in output + output += c; + + // Increase tab level? + if (found) + { + // Yes + output += (char)TabKey; + } + } + + // Return results + return output; + } + + /// Decreases the tab indent for the given text + /// The text with the tab indent decreased by one. + /// The text to outdent. + private string DecreaseTabIndent(string text) + { + string output = ""; + bool consume = true; + + // Add first intent + for (int n = 0; n < text.Length; ++n) + { + var c = text[n]; + bool found = (c == Formatter.Newline || c == Formatter.LineSeparator || c == Formatter.ParagraphSeparator); + + // Include char in output? + if ((int)c == TabKey && consume) + { + consume = false; + } + else + { + output += c; + } + + // Decrease tab level? + if (found) + { + // Yes + consume = true; + } + } + + // Return results + return output; + } + + #endregion + + #region Public Methods + + /// Indents the currently selected text. + public void IndentText() + { + + // Grab range + var range = Formatter.FindLineBoundries(TextStorage.Value, SelectedRange); + var line = TextStorage.Value.Substring((int)range.Location, (int)range.Length); + + // Increase tab indent + var output = IncreaseTabIndent(line); + + // Reformat section + TextStorage.BeginEditing(); + Replace(range, output); + TextStorage.EndEditing(); + SelectedRange = new NSRange(range.Location, output.Length); + Formatter.HighlightSyntaxRegion(TextStorage.Value, SelectedRange); + } + + /// Outdents the currently selected text. + public void OutdentText() + { + // Grab range + NSRange range = Formatter.FindLineBoundries(TextStorage.Value, SelectedRange); + string line = TextStorage.Value.Substring((int)range.Location, (int)range.Length); + + // Decrease tab indent + string output = DecreaseTabIndent(line); + + // reformat section + TextStorage.BeginEditing(); + Replace(range, output); + TextStorage.EndEditing(); + SelectedRange = new NSRange(range.Location, output.Length); + Formatter.HighlightSyntaxRegion(TextStorage.Value, SelectedRange); + } + + /// Performs the formatting command on the currectly selected range of text. + /// + /// The to apply. + /// + public void PerformFormattingCommand(LanguageFormatCommand command) + { + NSRange range = SelectedRange; + + // Apply to start of line? + if (command.Postfix == "") + { + // Yes, find start + range = Formatter.FindLineBoundries(TextStorage.Value, SelectedRange); + } + + // Yes, get selected text + string line = TextStorage.Value.Substring((int)range.Location, (int)range.Length); + + // Apply command + string output = command.Prefix; + output += line; + output += command.Postfix; + TextStorage.BeginEditing(); + Replace(range, output); + TextStorage.EndEditing(); + Formatter.HighlightSyntaxRegion(TextStorage.Value, range); + } + + #endregion + + #region Override Methods + + /// + /// Look for special keys being pressed and does specific processing based on the key. + /// + /// The event. + public override void KeyDown(NSEvent theEvent) + { + NSRange range; + string line; + int indentLevel = 0; + bool consumeKeystroke = false; + + // Avoid processing if no Formatter has been attached + if (Formatter == null) + return; + + // Trap all errors + try + { + // Get the code of current character + char c = theEvent.Characters[0]; + int charCode = (int)theEvent.Characters[0]; + + // Preprocess based on character code + switch (charCode) + { + case EnterKey: + // Get the tab indent level + range = Formatter.FindLineBoundries(TextStorage.Value, SelectedRange); + line = TextStorage.Value.Substring((int)range.Location, (int)range.Length); + indentLevel = CalculateIndentLevel(line); + break; + case TabKey: + // Is a range selected? + if (SelectedRange.Length > 0) + { + // Increase tab indent over the entire selection + IndentText(); + consumeKeystroke = true; + } + break; + case ShiftTabKey: + // Is a range selected? + if (SelectedRange.Length > 0) + { + // Increase tab indent over the entire selection + OutdentText(); + consumeKeystroke = true; + } + break; + default: + // Are we completing closures + if (CompleteClosures) + { + if (WrapClosures && SelectedRange.Length > 0) + { + // Yes, see if we are starting a closure + foreach (LanguageClosure closure in Formatter.Language.Closures) + { + // Found? + if (closure.StartingCharacter == c) + { + // Yes, get selected text + nint location = SelectedRange.Location; + line = TextStorage.Value.Substring((int)SelectedRange.Location, (int)SelectedRange.Length); + string output = ""; + output += closure.StartingCharacter; + output += line; + output += closure.EndingCharacter; + TextStorage.BeginEditing(); + Replace(SelectedRange, output); + TextStorage.EndEditing(); + if (SelectAfterWrap) + { + SelectedRange = new NSRange(location, output.Length); + } + consumeKeystroke = true; + Formatter.HighlightSyntaxRegion(TextStorage.Value, SelectedRange); + } + } + } + else + { + // Yes, see if we are in a language defined closure + foreach (LanguageClosure closure in Formatter.Language.Closures) + { + // Found? + if (closure.StartingCharacter == c) + { + // Is this a valid location for a completion? + if (Formatter.TrailingCharacterIsWhitespaceOrTerminator(TextStorage.Value, SelectedRange)) + { + // Yes, complete closure + consumeKeystroke = true; + string output = ""; + output += closure.StartingCharacter; + output += closure.EndingCharacter; + TextStorage.BeginEditing(); + InsertText(new NSString(output)); + TextStorage.EndEditing(); + SelectedRange = new NSRange(SelectedRange.Location - 1, 0); + } + } + } + } + } + break; + } + + // Call base to handle event + if (!consumeKeystroke) + base.KeyDown(theEvent); + + // Post process based on character code + switch (charCode) + { + case EnterKey: + // Tab indent the new line to the same level + if (indentLevel > 0) + { + string indent = TabIndent(indentLevel); + TextStorage.BeginEditing(); + InsertText(new NSString(indent)); + TextStorage.EndEditing(); + } + break; + } + } + catch + { + // Call base to process on any error + base.KeyDown(theEvent); + } + + this.Formatter.Reformat(); + + //Console.WriteLine ("Key: {0}", (int)theEvent.Characters[0]); + } + + /// + /// Called when a drag operation is started for this . + /// + /// The entered. + /// Sender. + /// + /// See Apple's drag and drop docs for more details (https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/DragandDrop/DragandDrop.html) + /// + //public override NSDragOperation DraggingEntered(NSDraggingInfo sender) + //{ + // // When we start dragging, inform the system that we will be handling this as + // // a copy/paste + // return NSDragOperation.Copy; + //} + + /// + /// Process any drag operations initialized by the user to this . + /// If one or more files have dragged in, the contents of those files will be copied into the document at the + /// current cursor location. + /// + /// true, if drag operation was performed, false otherwise. + /// The caller that initiated the drag operation. + /// + /// See Apple's drag and drop docs for more details (https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/DragandDrop/DragandDrop.html) + /// + //public override bool PerformDragOperation(NSDraggingInfo sender) + //{ + // // Attempt to read filenames from pasteboard + // var plist = (NSArray)sender.DraggingPasteboard.GetPropertyListForType(NSPasteboard.NSFilenamesType); + + // // Was a list of files returned from Finder? + // if (plist != null) + // { + // // Yes, process list + // for (nuint n = 0; n < plist.Count; ++n) + // { + // // Get the current file + // var path = plist.GetItem(n); + // var url = NSUrl.FromString(path); + // var contents = File.ReadAllText(path); + + // // Insert contents at cursor + // NSRange range = SelectedRange; + // TextStorage.BeginEditing(); + // Replace(range, contents); + // TextStorage.EndEditing(); + + // // Expand range to fully encompass new content and + // // reformat + // range = new NSRange(range.Location, contents.Length); + // range = Formatter.FindLineBoundries(TextStorage.Value, range); + // Formatter.HighlightSyntaxRegion(TextStorage.Value, range); + // } + + // // Inform caller of success + // return true; + // } + // else + // { + // // No, allow base class to handle + // return base.PerformDragOperation(sender); + // } + //} + + /// Reads the selection from pasteboard. + /// + /// true, if the selection was read from the pasteboard, false otherwise. + /// + /// The pasteboard being read. + /// + /// This method is overridden to update the formatting after the user pastes text into the view. + /// + public override bool ReadSelectionFromPasteboard(NSPasteboard pboard) + { + // Console.WriteLine ("Read selection from pasteboard"); + bool result = base.ReadSelectionFromPasteboard(pboard); + Formatter?.Reformat(); + return result; + } + + /// Reads the selection from pasteboard. + /// + /// true, if the selection was read from the pasteboard, false otherwise. + /// + /// The pasteboard being read. + /// The type of data being read from the pasteboard. + /// + /// This method is overridden to update the formatting after the user pastes text into the view. + /// + public override bool ReadSelectionFromPasteboard(NSPasteboard pboard, string type) + { + // Console.WriteLine ("Read selection from pasteboard also"); + var result = base.ReadSelectionFromPasteboard(pboard, type); + Formatter?.Reformat(); + return result; + } + + #endregion + + #region Events + + /// Occurs when source cell clicked. + /// NOTE: This replaces the built-in CellClicked event because we + /// are providing a custom NSTextViewDelegate and it is unavialable. + public event EventHandler SourceCellClicked; + + /// Raises the source cell clicked event. + /// The controller raising the event. + /// Arguments defining the event. + internal void RaiseSourceCellClicked(object sender, NSTextViewClickedEventArgs e) + { + this.SourceCellClicked?.Invoke(sender, e); + } + + /// Occurs when source cell double clicked. + /// NOTE: This replaces the built-in CellDoubleClicked event because we + /// are providing a custom NSTextViewDelegate and it is unavialable. + public event EventHandler SourceCellDoubleClicked; + + /// Raises the source cell double clicked event. + /// The controller raising the event. + /// Arguments defining the event. + internal void RaiseSourceCellDoubleClicked(object sender, NSTextViewDoubleClickEventArgs e) + { + this.SourceCellDoubleClicked?.Invoke(sender, e); + } + + /// Occurs when source cell dragged. + /// + /// NOTE: This replaces the built-in DragCell event because we are providing a custom + /// NSTextViewDelegate and it is unavialable. + /// + public event EventHandler SourceCellDragged; + + /// Raises the source cell dragged event. + /// The controller raising the event. + /// Arguments defining the event. + internal void RaiseSourceCellDragged(object sender, NSTextViewDraggedCellEventArgs e) + { + this.SourceCellDragged?.Invoke(sender, e); + } + + /// Occurs when source selection changed. + /// + /// NOTE: This replaces the built-in DidChangeSelection event because we are providing a + /// custom NSTextViewDelegate and it is unavialable. + /// + public event EventHandler SourceSelectionChanged; + + /// Raises the source selection changed event. + /// The controller raising the event. + /// Arguments defining the event. + internal void RaiseSourceSelectionChanged(object sender, EventArgs e) + { + this.SourceSelectionChanged?.Invoke(sender, e); + } + + /// Occurs when source typing attributes changed. + /// NOTE: This replaces the built-in DidChangeTypingAttributes event because we + /// are providing a custom NSTextViewDelegate and it is unavialable. + public event EventHandler SourceTypingAttributesChanged; + + /// Raises the source typing attributes changed event. + /// The controller raising the event. + /// Arguments defining the event. + internal void RaiseSourceTypingAttributesChanged(object sender, EventArgs e) + { + this.SourceTypingAttributesChanged?.Invoke(sender, e); + } + + #endregion +} diff --git a/Cauldron.Macos/SourceWriter/SourceTextViewDelegate.cs b/Cauldron.Macos/SourceWriter/SourceTextViewDelegate.cs new file mode 100644 index 0000000..4c111d9 --- /dev/null +++ b/Cauldron.Macos/SourceWriter/SourceTextViewDelegate.cs @@ -0,0 +1,183 @@ +using System; +using AppKit; +using CoreGraphics; +using Foundation; +using System.Collections.Generic; + +namespace Cauldron.Macos.SourceWriter +{ + public class SourceTextViewDelegate : NSTextViewDelegate + { + #region Computed Properties + + /// + /// Gets or sets the text editor. + /// + /// The this delegate is attached to. + public SourceTextView TextEditor { get; set; } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// Text editor. + public SourceTextViewDelegate(SourceTextView textEditor) + { + this.TextEditor = textEditor; + } + + #endregion + + #region Override Methods + + /// + /// Based on the user preferences set on the parent , this + /// method returns the available list of partial word completions. + /// + /// The list of word completions that will be presented to the user. + /// The source . + /// + /// A list of default words automatically provided by OS X in the user's language. + /// + /// The cursor location where the partial word exists. + /// + /// The word that should be selected when the list is displayed (usually 0 meaning the first + /// item in the list). Pass -1 for no selected items. + /// + public override string[] GetCompletions(NSTextView textView, string[] words, NSRange charRange, ref nint index) + { + List completions = new(); + + // Is auto complete enabled? + if (TextEditor.AllowAutoComplete) + { + // Use keywords in auto complete? + if (TextEditor.AutoCompleteKeywords) + { + // Yes, grab word being expanded + NSRange range = TextEditor.Formatter.FindWordBoundries( + TextEditor.TextStorage.Value, charRange); + string word = TextEditor.TextStorage.Value.Substring( + (int)range.Location, (int)range.Length); + + // Scan the keywords for the a possible match + foreach (string keyword in TextEditor.Formatter.Language.Keywords.Keys) + { + // Found? + if (keyword.Contains(word)) + { + completions.Add(keyword); + } + } + } + + // Use default words? + if (TextEditor.AutoCompleteDefaultWords) + { + // Only if keywords list is empty? + if (TextEditor.DefaultWordsOnlyIfKeywordsEmpty) + { + if (completions.Count == 0) + { + // No keywords, add defaults + completions.AddRange(words); + } + } + else + { + // No, always include default words + completions.AddRange(words); + } + } + } + + // Return results + return completions.ToArray(); + } + + /// Called when the cell is clicked. + /// The . + /// The cell being acted upon. + /// The onscreen frame of the cell. + /// The index of the character clicked. + /// + /// Because a custom Delegate has been attached to the NSTextView, the normal + /// events will not work so we are using this method to call custom + /// events instead. + /// + public override void CellClicked(NSTextView textView, NSTextAttachmentCell cell, + CGRect cellFrame, nuint charIndex) + { + // Pass through to Text Editor event + TextEditor.RaiseSourceCellClicked(TextEditor, + new NSTextViewClickedEventArgs(cell, cellFrame, charIndex)); + } + + /// Called when the cell is double-clicked. + /// The . + /// The cell being acted upon. + /// The onscreen frame of the cell. + /// The index of the character clicked. + /// + /// Because a custom Delegate has been attached to the NSTextView, the normal + /// events will not work so we are using this method to call custom + /// events instead. + /// + public override void CellDoubleClicked(NSTextView textView, NSTextAttachmentCell cell, + CGRect cellFrame, nuint charIndex) + { + // Pass through to Text Editor event + TextEditor.RaiseSourceCellDoubleClicked(TextEditor, + new NSTextViewDoubleClickEventArgs(cell, cellFrame, charIndex)); + } + + /// Called when the cell is dragged. + /// The . + /// The cell being acted upon. + /// The onscreen frame of the cell. + /// An event defining the drag operation. + /// + /// Because a custom Delegate has been attached to the NSTextView, the normal + /// events will not work so we are using this method to call custom + /// events instead. + /// + //public override void DraggedCell(NSTextView view, NSTextAttachmentCell cell, CGRect rect, + // NSEvent theevent) + //{ + // // Pass through to Text Editor event + // TextEditor.RaiseSourceCellDragged(TextEditor, new NSTextViewDraggedCellEventArgs(cell, rect, theevent)); + //} + + /// Called when the text selection has changed. + /// A notification defining the change. + /// + /// Because a custom Delegate has been attached to the NSTextView, the normal + /// events will not work so we are using this method to call custom + /// events instead. + /// + public override void DidChangeSelection(NSNotification notification) + { + // Pass through to Text Editor event + TextEditor.RaiseSourceSelectionChanged(TextEditor, EventArgs.Empty); + } + + /// Called when the typing attributes has changed. + /// A notification defining the change. + /// + /// Because a custom Delegate has been attached to the NSTextView, the normal + /// events will not work so we are using this method to call custom + /// events instead. + /// + public override void DidChangeTypingAttributes(NSNotification notification) + { + // Pass through to Text Editor event + TextEditor.RaiseSourceTypingAttributesChanged(TextEditor, EventArgs.Empty); + } + + #endregion + } +} +