// Copyright (c) 2013-2021 Bruyère Jean-Philippe // // This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) using System; using Glfw; using Crow.Text; using System.Collections.Generic; using System.Threading.Tasks; using System.Linq; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Collections; using Drawing2D; using System.Diagnostics; namespace Crow { public class Editor : TextBox { XmlSource source; object TokenMutex = new object(); void parse () { XmlSource tmp = new XmlSource(_text); lock(TokenMutex) source = tmp; RegisterForGraphicUpdate(); } protected override void onInitialized(object sender, EventArgs e) { base.onInitialized(sender, e); } ListBox overlay; IList suggestions; volatile bool disableSuggestions; public IList Suggestions { get => suggestions; set { suggestions = value; NotifyValueChangedAuto (suggestions); if (suggestions == null || suggestions.Count == 0) hideOverlay (); else showOverlay (); } } bool suggestionsActive => overlay != null && overlay.IsVisible; Token currentToken; SyntaxNode currentNode; public override void OnTextChanged(object sender, TextChangeEventArgs e) { base.OnTextChanged(sender, e); //Task.Run(()=>parse()); parse(); if (!disableSuggestions && HasFocus) tryGetSuggestions (); //Console.WriteLine ($"{pos}: {suggestionTok.AsString (_text)} {suggestionTok}"); } void showOverlay () { lock (IFace.UpdateMutex) { if (overlay == null) { overlay = IFace.LoadIMLFragment(@" "); overlay.DataSource = this; overlay.Loaded += (sender, arg) => (sender as ListBox).SelectedIndex = 0; } else overlay.IsVisible = true; overlay.RegisterForLayouting(LayoutingType.Sizing); } } void hideOverlay () { if (overlay == null) return; overlay.IsVisible = false; } string[] allWidgetNames = typeof (Widget).Assembly.GetExportedTypes ().Where(t=>typeof(Widget).IsAssignableFrom (t)) .Select (s => s.Name).ToArray (); IEnumerable getAllCrowTypeMembers (string crowTypeName) { Type crowType = IFace.GetWidgetTypeFromName (crowTypeName); return crowType?.GetMembers (BindingFlags.Public | BindingFlags.Instance). Where (m=>((m is PropertyInfo pi && pi.CanWrite) || (m is EventInfo)) && m.GetCustomAttribute() == null); } MemberInfo getCrowTypeMember (string crowTypeName, string memberName) { Type crowType = IFace.GetWidgetTypeFromName (crowTypeName); return crowType.GetMember (memberName, BindingFlags.Public | BindingFlags.Instance).FirstOrDefault (); } void tryGetSuggestions () { if (!currentLoc.HasValue) return; int pos = lines.GetAbsolutePosition (CurrentLoc.Value); currentToken = source.FindTokenIncludingPosition (pos); currentNode = source.FindNodeIncludingPosition (pos); Console.WriteLine ($"Current Token: {currentToken} Current Node: {currentNode}"); if (currentToken.Type == TokenType.ElementOpen) { Suggestions = new List (allWidgetNames); } else if (currentToken.Type == TokenType.ElementName) { Suggestions = allWidgetNames.Where (s => s.StartsWith (currentToken.AsString (_text), StringComparison.OrdinalIgnoreCase)).ToList (); } else if (currentToken.Type == TokenType.EndElementOpen && currentNode is ElementEndTagSyntax eltEndTag) { ElementSyntax es = eltEndTag.Parent as ElementSyntax; if (es?.StartTag != null) Suggestions = new List (new string[] {es.StartTag.NameToken.Value.AsString(_text)}); } else if (currentToken.Type.HasFlag(TokenType.WhiteSpace) && typeof(ElementTagSyntax).IsAssignableFrom(currentNode?.GetType())) { ElementTagSyntax ets = currentNode as ElementTagSyntax; if (ets.NameToken.HasValue) Suggestions = getAllCrowTypeMembers (ets.NameToken.Value.AsString (_text)).ToList (); } else if (currentToken.Type != TokenType.AttributeValueClose && currentNode is AttributeSyntax attribNode) { if (currentNode.Parent is ElementTagSyntax eltTag) { if (eltTag.NameToken.HasValue) { if (currentToken.Type == TokenType.AttributeName) { Suggestions = getAllCrowTypeMembers (eltTag.NameToken.Value.AsString (_text)) .Where (s => s.Name.StartsWith (currentToken.AsString (_text), StringComparison.OrdinalIgnoreCase)).ToList (); } else if (attribNode.NameToken.HasValue) { if (currentToken.Type == TokenType.AttributeValue) { MemberInfo mi = getCrowTypeMember ( eltTag.NameToken.Value.AsString (_text), attribNode.NameToken.Value.AsString (_text)); if (mi is PropertyInfo pi) { if (pi.Name == "Style") Suggestions = IFace.Styling.Keys .Where (s => s.StartsWith (currentToken.AsString (_text), StringComparison.OrdinalIgnoreCase)).ToList (); else if (pi.PropertyType.IsEnum) Suggestions = Enum.GetNames (pi.PropertyType) .Where (s => s.StartsWith (currentToken.AsString (_text), StringComparison.OrdinalIgnoreCase)).ToList (); else if (pi.PropertyType == typeof(bool)) Suggestions = (new string[] {"true", "false"}). Where (s => s.StartsWith (currentToken.AsString (_text), StringComparison.OrdinalIgnoreCase)).ToList (); else if (pi.PropertyType == typeof (Measure)) Suggestions = (new string[] {"Stretched", "Fit"}). Where (s => s.StartsWith (currentToken.AsString (_text), StringComparison.OrdinalIgnoreCase)).ToList (); else if (pi.PropertyType == typeof (Fill)) Suggestions = EnumsNET.Enums.GetValues () .Where (s => s.ToString().StartsWith (currentToken.AsString (_text), StringComparison.OrdinalIgnoreCase)).ToList (); } } else if (currentToken.Type == TokenType.AttributeValueOpen) { MemberInfo mi = getCrowTypeMember ( eltTag.NameToken.Value.AsString (_text), attribNode.NameToken.Value.AsString (_text)); if (mi is PropertyInfo pi) { if (pi.Name == "Style") Suggestions = IFace.Styling.Keys.ToList (); if (pi.PropertyType.IsEnum) Suggestions = Enum.GetNames (pi.PropertyType).ToList (); else if (pi.PropertyType == typeof(bool)) Suggestions = new List (new string[] {"true", "false"}); else if (pi.PropertyType == typeof (Fill)) Suggestions = EnumsNET.Enums.GetValues ().ToList (); else if (pi.PropertyType == typeof (Measure)) Suggestions = new List (new string[] {"Stretched", "Fit"}); } } } } } } else if (currentToken.Type != TokenType.AttributeValueClose && currentToken.Type != TokenType.EmptyElementClosing && currentToken.Type != TokenType.ClosingSign && currentNode is ElementStartTagSyntax eltStartTag) { if (currentToken.Type == TokenType.AttributeName) Suggestions = getAllCrowTypeMembers (eltStartTag.NameToken.Value.AsString (_text)) .Where (s => s.Name.StartsWith (currentToken.AsString (_text), StringComparison.OrdinalIgnoreCase)).ToList (); //else if (currentToken.Type == TokenType.ElementName) // Suggestions = getAllCrowTypeMembers (eltStartTag.NameToken.Value.AsString (_text)).ToList (); } else { /*SyntaxNode curNode = source.FindNodeIncludingPosition (pos); Console.WriteLine ($"Current Node: {curNode}"); if (curNode is ElementStartTagSyntax eltStartTag && (currentToken.Type != TokenType.ClosingSign && currentToken.Type != TokenType.EmptyElementClosing && currentToken.Type != TokenType.Unknown)) { Suggestions = getAllCrowTypeMembers (eltStartTag.NameToken.Value.AsString (_text)).ToList (); } else*/ hideOverlay (); } } void completeToken (string selectedSugg) { if (currentToken.Type.HasFlag(TokenType.WhiteSpace)) { if (typeof(ElementTagSyntax).IsAssignableFrom(currentNode?.GetType())) { ElementTagSyntax ets = currentNode as ElementTagSyntax; if (ets.NameToken.HasValue) update (new TextChange (currentToken.End, 0, selectedSugg + "=\"\""),-1); else update (new TextChange (currentToken.End, 0, selectedSugg + " ")); } else { update (new TextChange (currentToken.End, 0, selectedSugg)); } } else if (currentToken.Type == TokenType.EndElementOpen) { update (new TextChange (currentToken.End, 0, selectedSugg + ">")); } else if (currentToken.Type == TokenType.ElementName) { if (currentNode is ElementEndTagSyntax) update (new TextChange (currentToken.Start, currentToken.Length, selectedSugg + ">")); else update (new TextChange (currentToken.Start, currentToken.Length, selectedSugg + ">"), -1); } else if (currentNode is AttributeSyntax attrib) { if (currentToken.Type == TokenType.AttributeName) { if (attrib.ValueToken.HasValue) { TextChange tc = new TextChange (currentToken.Start, currentToken.Length, selectedSugg); update (tc); SelectionStart = lines.GetLocation (attrib.ValueToken.Value.Start + tc.CharDiff + 1); CurrentLoc = lines.GetLocation (attrib.ValueToken.Value.End + tc.CharDiff - 1); } else { update (new TextChange (currentToken.Start, currentToken.Length, selectedSugg + "=\"\""), -1); } } else { int offset = 1; if (!attrib.ValueCloseToken.HasValue) { selectedSugg += attrib.ValueCloseToken.Value.AsString(_text); offset = 0; } if (currentToken.Type == TokenType.AttributeValueOpen) update (new TextChange (currentToken.End, 0, selectedSugg), offset); else if (currentToken.Type == TokenType.AttributeValue) update (new TextChange (currentToken.Start, currentToken.Length, selectedSugg), offset); } } else if (currentToken.Type == TokenType.ElementOpen) { update (new TextChange (currentToken.End, 0, selectedSugg + " ")); } else update (new TextChange (currentToken.Start, currentToken.Length, selectedSugg)); hideOverlay (); } public override void onMouseDown (object sender, MouseButtonEventArgs e) { hideOverlay (); base.onMouseDown (sender, e); } public override void onMouseEnter(object sender, MouseMoveEventArgs e) { base.onMouseEnter (sender, e); HasFocus = true; } public override void onKeyDown(object sender, KeyEventArgs e) { TextSpan selection = Selection; if (SelectionIsEmpty) { if (suggestionsActive) { switch (e.Key) { case Key.Escape: hideOverlay (); return; case Key.Left: case Key.Right: hideOverlay (); break; case Key.End: case Key.Home: case Key.Down: case Key.Up: case Key.PageDown: case Key.PageUp: overlay.onKeyDown (this, e); return; case Key.Tab: case Key.Enter: case Key.KeypadEnter: string selectedSugg = overlay.SelectedItem is MemberInfo mi ? mi.Name : overlay.SelectedItem?.ToString (); if (selectedSugg == null) break; completeToken (selectedSugg); return; } } else if (e.Key == Key.Space && IFace.Ctrl) { tryGetSuggestions (); return; } } else if (e.Key == Key.Tab && !selection.IsEmpty) { int lineStart = lines.GetLocation (selection.Start).Line; int lineEnd = lines.GetLocation (selection.End).Line; disableSuggestions = true; if (e.Modifiers.HasFlag(Modifier.Shift)) { for (int l = lineStart; l <= lineEnd; l++) { if (Text[lines[l].Start] == '\t') update (new TextChange (lines[l].Start, 1, "")); else if (Char.IsWhiteSpace (Text[lines[l].Start])) { int i = 1; while (i < lines[l].Length && i < Interface.TAB_SIZE && Char.IsWhiteSpace (Text[i])) i++; update (new TextChange (lines[l].Start, i, "")); } } }else{ for (int l = lineStart; l <= lineEnd; l++) update (new TextChange (lines[l].Start, 0, "\t")); } SelectionStart = new CharLocation (lineStart, 0); CurrentLoc = new CharLocation (lineEnd, lines[lineEnd].Length); disableSuggestions = false; return; } base.onKeyDown(sender, e); } protected override void drawContent (IContext gr) { try { lock(TokenMutex) { if (source == null || source.Tokens.Length == 0) { base.drawContent (gr); return; } Rectangle cb = ClientRectangle; fe = gr.FontExtents; double lineHeight = fe.Ascent + fe.Descent; CharLocation selStart = default, selEnd = default; bool selectionNotEmpty = false; //if (HasFocus) { if (currentLoc?.Column < 0) { updateLocation (gr, cb.Width, ref currentLoc); NotifyValueChanged ("CurrentColumn", CurrentColumn); } else updateLocation (gr, cb.Width, ref currentLoc); if (overlay != null && overlay.IsVisible) { Point p = new Point((int)currentLoc.Value.VisualCharXPosition - ScrollX, (int)(lineHeight * (currentLoc.Value.Line + 1) - ScrollY)); if (p.Y < 0 || p.X < 0) hideOverlay (); else { p += ScreenCoordinates (Slot).TopLeft; overlay.Left = p.X; overlay.Top = p.Y; } } if (SelectionStart.HasValue) { updateLocation (gr, cb.Width, ref selectionStart); if (CurrentLoc.Value != SelectionStart.Value) selectionNotEmpty = true; } if (selectionNotEmpty) { if (CurrentLoc.Value.Line < SelectionStart.Value.Line) { selStart = CurrentLoc.Value; selEnd = SelectionStart.Value; } else if (CurrentLoc.Value.Line > SelectionStart.Value.Line) { selStart = SelectionStart.Value; selEnd = CurrentLoc.Value; } else if (CurrentLoc.Value.Column < SelectionStart.Value.Column) { selStart = CurrentLoc.Value; selEnd = SelectionStart.Value; } else { selStart = SelectionStart.Value; selEnd = CurrentLoc.Value; } } //} double spacePixelWidth = gr.TextExtents (" ").XAdvance; int x = 0, y = 0; double pixX = cb.Left; Foreground.SetAsSource (IFace, gr); gr.Translate (-ScrollX, -ScrollY); ReadOnlySpan sourceBytes = source.Source.AsSpan(); Span bytes = stackalloc byte[128]; TextExtents extents; int tokPtr = 0; Token tok = source.Tokens[tokPtr]; bool multilineToken = false; ReadOnlySpan buff = sourceBytes; for (int i = 0; i < lines.Count; i++) { //if (!cancelLinePrint (lineHeight, lineHeight * y, cb.Height)) { if (multilineToken) { if (tok.End < lines[i].End) {//last incomplete line of multiline token buff = sourceBytes.Slice (lines[i].Start, tok.End - lines[i].Start); } else {//print full line buff = sourceBytes.Slice (lines[i].Start, lines[i].Length); } } while (tok.Start < lines[i].End) { if (!multilineToken) { if (tok.End > lines[i].End) {//first line of multiline multilineToken = true; buff = sourceBytes.Slice (tok.Start, lines[i].End - tok.Start); } else buff = sourceBytes.Slice (tok.Start, tok.Length); if (tok.Type.HasFlag (TokenType.Punctuation)) gr.SetSource(Colors.DarkGrey); else if (tok.Type.HasFlag (TokenType.Trivia)) gr.SetSource(Colors.DimGrey); else if (tok.Type == TokenType.ElementName) { gr.SetSource(Colors.Green); }else if (tok.Type == TokenType.AttributeName) { gr.SetSource(Colors.Blue); }else if (tok.Type == TokenType.AttributeValue) { gr.SetSource(Colors.OrangeRed); }else if (tok.Type == TokenType.EqualSign) { gr.SetSource(Colors.Black); }else if (tok.Type == TokenType.PI_Target) { gr.SetSource(Colors.DarkSlateBlue); }else { gr.SetSource(Colors.Red); } } int size = buff.Length * 4 + 1; if (bytes.Length < size) bytes = new byte[size]; int encodedBytes = buff.ToUtf8 (bytes); if (encodedBytes > 0) { bytes[encodedBytes++] = 0; gr.TextExtents (bytes.Slice (0, encodedBytes), out extents); gr.MoveTo (pixX, lineHeight * y + fe.Ascent); gr.ShowText (bytes.Slice (0, encodedBytes)); pixX += extents.XAdvance; x += buff.Length; } if (multilineToken) { if (tok.End < lines[i].End)//last incomplete line of multiline token multilineToken = false; else break; } if (++tokPtr >= source.Tokens.Length) break; tok = source.Tokens[tokPtr]; } if (selectionNotEmpty) { RectangleD lineRect = new RectangleD (cb.X, lineHeight * y + cb.Top, pixX, lineHeight); RectangleD selRect = lineRect; if (i >= selStart.Line && i <= selEnd.Line) { if (selStart.Line == selEnd.Line) { selRect.X = selStart.VisualCharXPosition + cb.X; selRect.Width = selEnd.VisualCharXPosition - selStart.VisualCharXPosition; } else if (i == selStart.Line) { double newX = selStart.VisualCharXPosition + cb.X; selRect.Width -= (newX - selRect.X) - 10.0; selRect.X = newX; } else if (i == selEnd.Line) selRect.Width = selEnd.VisualCharXPosition - selRect.X + cb.X; else selRect.Width += 10.0; buff = sourceBytes.Slice(lines[i].Start, lines[i].Length); int size = buff.Length * 4 + 1; if (bytes.Length < size) bytes = new byte[size]; int encodedBytes = buff.ToUtf8 (bytes); gr.SetSource (SelectionBackground); gr.Rectangle (selRect); if (encodedBytes < 0) gr.Fill (); else { gr.FillPreserve (); gr.Save (); gr.Clip (); gr.SetSource (SelectionForeground); gr.MoveTo (lineRect.X, lineRect.Y + fe.Ascent); gr.ShowText (bytes.Slice (0, encodedBytes)); gr.Restore (); } Foreground.SetAsSource (IFace, gr); } } if (!multilineToken) { if (++tokPtr >= source.Tokens.Length) break; tok = source.Tokens[tokPtr]; } x = 0; pixX = 0; y++; /* } else if (tok2.Type == TokenType.Tabulation) { int spaceRounding = x % tabSize; int spaces = spaceRounding == 0 ? tabSize * tok2.Length : spaceRounding + tabSize * (tok2.Length - 1); x += spaces; pixX += spacePixelWidth * spaces; continue; } else if (tok2.Type == TokenType.WhiteSpace) { x += tok2.Length; pixX += spacePixelWidth * tok2.Length;*/ } //gr.Translate (ScrollX, ScrollY); } } catch { } } } }