using System;
using System.Timers;
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;
private LineNumberRuler LineNumberRuler;
#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)
{
Initialize();
}
public SourceTextView(ObjCRuntime.NativeHandle handle) : base(handle)
{
Initialize();
}
/// Initialize this instance.
private void Initialize()
{
this.Delegate = new SourceTextViewDelegate(this);
this.UsesAdaptiveColorMappingForDarkAppearance = true;
}
public override void AwakeFromNib()
{
base.AwakeFromNib();
this.LineNumberRuler = new LineNumberRuler(this);
this.EnclosingScrollView.VerticalRulerView = this.LineNumberRuler;
this.EnclosingScrollView.HasVerticalRuler = true;
this.EnclosingScrollView.RulersVisible = true;
this.PostsFrameChangedNotifications = true;
NSView.Notifications.ObserveBoundsChanged((_, _) => this.DrawGutter());
this.OnTextChanged += (_, _) => this.DrawGutter();
}
[Export("drawGutter")]
public void DrawGutter()
{
if (this.LineNumberRuler is not null)
this.LineNumberRuler.NeedsDisplay = true;
}
#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
private Timer InputTimoutTimer { get; set; }
///
/// The amount of time with no user input after which will be run
///
public TimeSpan InputTimeoutInterval { get; set; } = new TimeSpan(0, 0, 1);
///
/// An event triggered when the user has stopped typing for a period of time defined by
///
///
public event ElapsedEventHandler OnFinishedTyping;
/// Triggered when the value in the textbox is changed
public event EventHandler OnTextChanged;
///
/// 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();
this.OnTextChanged.Invoke(this, null);
this.InputTimoutTimer?.Stop();
this.InputTimoutTimer?.Close();
this.InputTimoutTimer = new(this.InputTimeoutInterval)
{
AutoReset = false
};
this.InputTimoutTimer.Elapsed += this.OnFinishedTyping;
this.InputTimoutTimer.Start();
}
///
/// 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();
this.OnTextChanged?.Invoke(this, null);
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();
this.OnTextChanged?.Invoke(this, null);
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
}