aboutsummaryrefslogtreecommitdiffstats
path: root/Software/Visual_Studio/Tango.SharedUI/Controls/ScriptEditorControl.xaml.cs
blob: 3fe87c7e2f371bdb97aea274d135140a032b070e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
// Generated by the protocol buffer compiler.  DO NOT EDIT!
// source: DebugDistributor.proto
#pragma warning disable 1591, 0612, 3021
#region Designer generated code

using pb = global::Google.Protobuf;
using pbc = global::Google.Protobuf.Collections;
using pbr = global::Google.Protobuf.Reflection;
using scg = global::System.Collections.Generic;
namespace Tango.PMR.Debugging {

  /// <summary>Holder for reflection information generated from DebugDistributor.proto</summary>
  public static partial class DebugDistributorReflection {

    #region Descriptor
    /// <summary>File descriptor for DebugDistributor.proto</summary>
    public static pbr::FileDescriptor Descriptor {
      get { return descriptor; }
    }
    private static pbr::FileDescriptor descriptor;

    static DebugDistributorReflection() {
      byte[] descriptorData = global::System.Convert.FromBase64String(
          string.Concat(
            "ChZEZWJ1Z0Rpc3RyaWJ1dG9yLnByb3RvEhNUYW5nby5QTVIuRGVidWdnaW5n",
            "KoUCChBEZWJ1Z0Rpc3RyaWJ1dG9yEhEKDUNvbnRyb2xGaWx0ZXIQABIRCg1I",
            "ZWF0ZXJzRmlsdGVyEAESDQoJSm9iRmlsdGVyEAISDwoLV2FzdGVGaWx0ZXIQ",
            "AxIPCgtBbGFybUZpbHRlchAEEhUKEURpYWdub3N0aWNzRmlsdGVyEAUSDQoJ",
            "SURTRmlsdGVyEAYSEAoMVGhyZWFkRmlsdGVyEAcSEQoNR2VuZXJhbEZpbHRl",
            "chAIEg4KCkNvbW1GaWx0ZXIQCRIOCgpGUEdBRmlsdGVyEAoSDgoKTG9hZEZp",
            "bHRlchALEg4KCkluaXRGaWx0ZXIQDBIPCgtNYWludEZpbHRlchANQh8KHWNv",
            "bS50d2luZS50YW5nby5wbXIuZGVidWdnaW5nYgZwcm90bzM="));
      descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
          new pbr::FileDescriptor[] { },
          new pbr::GeneratedClrTypeInfo(new[] {typeof(global::Tango.PMR.Debugging.DebugDistributor), }, null));
    }
    #endregion

  }
  #region Enums
  public enum DebugDistributor {
    [pbr::OriginalName("ControlFilter")] ControlFilter = 0,
    [pbr::OriginalName("HeatersFilter")] HeatersFilter = 1,
    [pbr::OriginalName("JobFilter")] JobFilter = 2,
    [pbr::OriginalName("WasteFilter")] WasteFilter = 3,
    [pbr::OriginalName("AlarmFilter")] AlarmFilter = 4,
    [pbr::OriginalName("DiagnosticsFilter")] DiagnosticsFilter = 5,
    [pbr::OriginalName("IDSFilter")] <
using ICSharpCode.AvalonEdit.CodeCompletion;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Editing;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Xml;
using Tango.Core.Commands;
using Tango.Scripting;
using Tango.SharedUI;
using Tango.SharedUI.Helpers;

namespace Tango.SharedUI.Controls
{
    /// <summary>
    /// Represents a C# script editor control.
    /// </summary>
    /// <seealso cref="System.Windows.Controls.UserControl" />
    /// <seealso cref="System.Windows.Markup.IComponentConnector" />
    public partial class ScriptEditorControl : UserControl
    {
        #region Completion

        /// <summary>
        /// Represents an auto complete item.
        /// </summary>
        /// <seealso cref="ICSharpCode.AvalonEdit.CodeCompletion.ICompletionData" />
        internal class CompletionData : ICompletionData
        {
            private String _description;

            /// <summary>
            /// Gets or sets the icon source.
            /// </summary>
            public BitmapSource Source { get; set; }

            /// <summary>
            /// Initializes a new instance of the <see cref="CompletionData"/> class.
            /// </summary>
            /// <param name="text">The text.</param>
            /// <param name="description">The description.</param>
            public CompletionData(string text, String description)
            {
                this.Text = text;
                _description = description;
            }

            /// <summary>
            /// Gets the image.
            /// </summary>
            public System.Windows.Media.ImageSource Image
            {
                get { return Source; }
            }

            /// <summary>
            /// Gets the text. This property is used to filter the list of visible elements.
            /// </summary>
            public string Text { get; private set; }

            // Use this property if you want to show a fancy UIElement in the drop down list.
            public object Content
            {
                get { return this.Text; }
            }

            /// <summary>
            /// Gets the description.
            /// </summary>
            public object Description
            {
                get { return _description; }
            }

            /// <summary>
            /// Gets the priority. This property is used in the selection logic. You can use it to prefer selecting those items
            /// which the user is accessing most frequently.
            /// </summary>
            public double Priority { get { return 0; } }

            /// <summary>
            /// Perform the completion.
            /// </summary>
            /// <param name="textArea">The text area on which completion is performed.</param>
            /// <param name="completionSegment">The text segment that was used by the completion window if
            /// the user types (segment between CompletionWindow.StartOffset and CompletionWindow.EndOffset).</param>
            /// <param name="insertionRequestEventArgs">The EventArgs used for the insertion request.
            /// These can be TextCompositionEventArgs, KeyEventArgs, MouseEventArgs, depending on how
            /// the insertion was triggered.</param>
            public void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs)
            {
                textArea.Document.Replace(completionSegment, this.Text);
            }

            /// <summary>
            /// Returns a <see cref="System.String" /> that represents this instance.
            /// </summary>
            /// <returns>
            /// A <see cref="System.String" /> that represents this instance.
            /// </returns>
            public override string ToString()
            {
                return Text;
            }
        }

        #endregion

        private CompletionWindow completionWindow; //Holds the auto-complete window instance.

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ScriptEditorControl"/> class.
        /// </summary>
        public ScriptEditorControl()
        {
            InitializeComponent();

            textEditor.TextArea.IndentationStrategy = new ICSharpCode.AvalonEdit.Indentation.CSharp.CSharpIndentationStrategy();
            textEditor.TextArea.TextEntering += textEditor_TextArea_TextEntering;
            textEditor.TextArea.TextEntered += textEditor_TextArea_TextEntered;

            this.Loaded += ScriptEditorControl_Loaded;
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Handles the TextEntered event of the textEditor_TextArea control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="TextCompositionEventArgs"/> instance containing the event data.</param>
        private void textEditor_TextArea_TextEntered(object sender, TextCompositionEventArgs e)
        {
            if (e.Text == ".")
            {
                String keyword = textEditor.TextArea.GetJustCurrentWord();

                if (keyword != null)
                {
                    completionWindow = new CompletionWindow(textEditor.TextArea);
                    completionWindow.WindowStyle = WindowStyle.None;
                    completionWindow.AllowsTransparency = true;
                    completionWindow.ResizeMode = ResizeMode.NoResize;

                    IList<ICompletionData> data = completionWindow.CompletionList.CompletionData;

                    bool ok = false;

                    List<KeyValuePair<String, Type>> types = new List<KeyValuePair<String, Type>>();

                    types.AddRange(IntellisenseTypes);


                    if (IntellisenseTypes != null)
                    {
                        ScriptParser parser = new ScriptParser();

                        try
                        {
                            var variables = parser.ParseScript(textEditor.Text);

                            foreach (var v in variables)
                            {
                                var hT = IntellisenseTypes.SingleOrDefault(x => x.Key == v.Type);

                                if (hT.Value != null)
                                {
                                    types.Add(new KeyValuePair<string, Type>(v.Name, hT.Value));
                                }
                            }

                        }
                        catch { }
                    }

                    KeyValuePair<String, Type> type = types.LastOrDefault(x => keyword == x.Key);

                    if (type.Key != null)
                    {
                        ok = true;
                        FillType(type.Value, data);
                    }

                    if (ok)
                    {
                        completionWindow.Show();
                        completionWindow.Closed += delegate
                        {
                            completionWindow = null;
                        };
                    }
                }
            }
        }

        /// <summary>
        /// Handles the TextEntering event of the textEditor_TextArea control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="TextCompositionEventArgs"/> instance containing the event data.</param>
        private void textEditor_TextArea_TextEntering(object sender, TextCompositionEventArgs e)
        {
            if (e.Text.Length > 0 && completionWindow != null)
            {
                if (!char.IsLetterOrDigit(e.Text[0]))
                {
                    // Whenever a non-letter is typed while the completion window is open,
                    // insert the currently selected element.
                    completionWindow.CompletionList.RequestInsertion(e);
                }
            }
        }

        /// <summary>
        /// Handles the TextChanged event of the textEditor control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
        private void textEditor_TextChanged(object sender, EventArgs e)
        {
            Text = textEditor.Text;
        }

        private void ScriptEditorControl_Loaded(object sender, RoutedEventArgs e)
        {
            if (HighlightTypes != null)
            {
                Stream xshd_stream = typeof(ScriptEditorControl).Assembly.GetManifestResourceStream("Tango.SharedUI.CSharp-Mode.xshd");

                String text = String.Empty;

                using (StreamReader reader = new StreamReader(xshd_stream))
                {
                    text = reader.ReadToEnd();
                }

                String code = String.Empty;



                foreach (var name in HighlightTypes.Select(x => x.Key))
                {
                    code += String.Format("<Word>{0}</Word>", name) + Environment.NewLine;
                }

                text = text.Replace("@CUSTOM_TYPES@", code);

                using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(text)))
                {
                    XmlTextReader xshd_reader = new XmlTextReader(ms);
                    textEditor.SyntaxHighlighting = ICSharpCode.AvalonEdit.Highlighting.Xshd.HighlightingLoader.Load(xshd_reader, ICSharpCode.AvalonEdit.Highlighting.HighlightingManager.Instance);
                    xshd_reader.Close();
                }
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Fills the type.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="data">The data.</param>
        private void FillType(Type type, IList<ICompletionData> data)
        {
            List<CompletionData> items = new List<CompletionData>();

            foreach (var method in type.GetMethods().Where(x => x.IsPublic && !x.IsSpecialName))
            {
                String desc = method.ReturnType.Name + " " + method.Name + "(" + String.Join(", ", method.GetParameters().Select(x => x.ParameterType.Name + " " + x.Name).ToArray()) + ")";
                items.Add(new CompletionData(method.Name, desc) { Source = ResourceHelper.GetImageFromResources("Images/pubmethod.gif") });
            }
            foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                String desc = property.PropertyType.Name + " " + property.Name;
                items.Add(new CompletionData(property.Name, desc) { Source = ResourceHelper.GetImageFromResources("Images/pubproperty.gif") });
            }
            foreach (var ev in type.GetEvents(BindingFlags.Instance | BindingFlags.Public))
            {
                try
                {
                    String desc = ev.Name + " " + "(" + String.Join(", ", ev.EventHandlerType.GetMethod("Invoke").GetParameters().Select(x => x.ParameterType.Name + " " + x.Name).ToArray()) + ")";
                    items.Add(new CompletionData(ev.Name, desc) { Source = ResourceHelper.GetImageFromResources("Images/pubevent.gif") });
                }
                catch { }
            }

            foreach (var item in items.OrderBy(x => x.Text))
            {
                data.Add(item);
            }
        }

        /// <summary>
        /// Fills the assembly.
        /// </summary>
        /// <param name="asm">The asm.</param>
        /// <param name="data">The data.</param>
        private void FillAssembly(Assembly asm, IList<ICompletionData> data)
        {
            var q = from t in asm.GetTypes()
                    where t.IsClass
                    select t;

            foreach (var type in q)
            {
                data.Add(new CompletionData(type.Name, "Class") { Source = ResourceHelper.GetImageFromResources("Images/pubclass.gif") });
            }
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the text.
        /// </summary>
        public String Text
        {
            get { return (String)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }
        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register("Text", typeof(String), typeof(ScriptEditorControl), new PropertyMetadata(null, (d, e) => (d as ScriptEditorControl).OnTextChanged()));

        /// <summary>
        /// Gets or sets the highlight types.
        /// </summary>
        public ObservableCollection<KeyValuePair<string, Type>> HighlightTypes
        {
            get { return (ObservableCollection<KeyValuePair<string, Type>>)GetValue(HighlightTypesProperty); }
            set { SetValue(HighlightTypesProperty, value); }
        }
        public static readonly DependencyProperty HighlightTypesProperty =
            DependencyProperty.Register("HighlightTypes", typeof(ObservableCollection<KeyValuePair<string, Type>>), typeof(ScriptEditorControl), new PropertyMetadata(null));

        /// <summary>
        /// Gets or sets the intellisense types.
        /// </summary>
        public ObservableCollection<KeyValuePair<String,Type>> IntellisenseTypes
        {
            get { return (ObservableCollection<KeyValuePair<String,Type>>)GetValue(IntellisenseTypesProperty); }
            set { SetValue(IntellisenseTypesProperty, value); }
        }
        public static readonly DependencyProperty IntellisenseTypesProperty =
            DependencyProperty.Register("IntellisenseTypes", typeof(ObservableCollection<KeyValuePair<String,Type>>), typeof(ScriptEditorControl), new PropertyMetadata(null));


        #endregion

        #region Virtual Methods

        /// <summary>
        /// Called when the text has changed.
        /// </summary>
        protected virtual void OnTextChanged()
        {
            if (textEditor.Text != Text)
            {
                textEditor.Text = Text;
            }
        }

        /// <summary>
        /// Called when the insert script command has changed.
        /// </summary>
        protected virtual void OnInsertScriptCommandChanged()
        {
            if (InsertSnippetCommand != null)
            {
                InsertSnippetCommand.Executed += (x, snippet) =>
                {
                    textEditor.Document.Insert(textEditor.TextArea.Caret.Offset, snippet);
                };
            }
        }

        #endregion

        #region Commands

        /// <summary>
        /// Gets or sets the run command.
        /// </summary>
        public RelayCommand RunCommand
        {
            get { return (RelayCommand)GetValue(RunCommandProperty); }
            set { SetValue(RunCommandProperty, value); }
        }
        public static readonly DependencyProperty RunCommandProperty =
            DependencyProperty.Register("RunCommand", typeof(RelayCommand), typeof(ScriptEditorControl), new PropertyMetadata(null));

        /// <summary>
        /// Gets or sets the stop command.
        /// </summary>
        public RelayCommand StopCommand
        {
            get { return (RelayCommand)GetValue(StopCommandProperty); }
            set { SetValue(StopCommandProperty, value); }
        }
        public static readonly DependencyProperty StopCommandProperty =
            DependencyProperty.Register("StopCommand", typeof(RelayCommand), typeof(ScriptEditorControl), new PropertyMetadata(null));

        /// <summary>
        /// Gets or sets the save command.
        /// </summary>
        public RelayCommand SaveCommand
        {
            get { return (RelayCommand)GetValue(SaveCommandProperty); }
            set { SetValue(SaveCommandProperty, value); }
        }
        public static readonly DependencyProperty SaveCommandProperty =
            DependencyProperty.Register("SaveCommand", typeof(RelayCommand), typeof(ScriptEditorControl), new PropertyMetadata(null));

        /// <summary>
        /// Gets or sets the insert snippet command.
        /// </summary>
        public RelayCommand<String> InsertSnippetCommand
        {
            get { return (RelayCommand<String>)GetValue(InsertSnippetCommandProperty); }
            set { SetValue(InsertSnippetCommandProperty, value); }
        }
        public static readonly DependencyProperty InsertSnippetCommandProperty =
            DependencyProperty.Register("InsertSnippetCommand", typeof(RelayCommand<String>), typeof(ScriptEditorControl), new PropertyMetadata(null, (d, e) => (d as ScriptEditorControl).OnInsertScriptCommandChanged()));

        #endregion
    }

    internal static class DocumentUtils
    {
        private static Regex _wordRegex = new Regex(@"[^\W\d][\w]*(?<=\w)", RegexOptions.Compiled);

        public static string GetJustCurrentWord(this TextArea textArea)
        {
            try
            {
                DocumentLine line = textArea.Document.GetLineByNumber(textArea.Caret.Line);
                if (line.Length == 0)
                    return null;

                int lineCaretPosition = textArea.Caret.Offset - line.Offset;
                String l = textArea.Document.GetText(line);

                String trimmed = l.Remove(lineCaretPosition, l.Length - lineCaretPosition);

                return SplitToWords(trimmed).LastOrDefault(x => !String.IsNullOrWhiteSpace(x));
            }
            catch
            {
                return null;
            }
        }

        public static List<String> SplitToWords(String text)
        {
            text = text.Replace(".", " ");
            text = text.Replace("(", " ");
            text = text.Replace(")", " ");
            text = text.Replace(",", " ");
            var punctuation = text.Where(Char.IsPunctuation).Distinct().ToArray();
            var words = text.Split().Select(x => x.Trim(punctuation));
            return words.ToList();
        }
    }
}