Click here to Skip to main content
15,895,667 members
Articles / Desktop Programming / Windows Forms

Storm - the world's best IDE framework for .NET

Rate me:
Please Sign up or sign in to vote.
4.96/5 (82 votes)
4 Feb 2010LGPL311 min read 277.8K   6.5K   340  
Create fast, flexible, and extensible IDE applications easily with Storm - it takes nearly no code at all!
namespace Storm.CodeCompletion
{
	// TODO: make the user able to show lists of some items.
	// For example have an enum where the user can indicate
	// whether he/she wants to use the full list or a custom
	// list, and if the user wants to use a custom lists,
	// load it from a set property and display it instead.

	using System;
	using System.Collections;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Drawing;
	using System.Linq;
	using System.Runtime;
	using System.Runtime.InteropServices;
	using System.Text;
	using System.Windows.Forms;

	using Storm;
	using Storm.CodeCompletion;
	using Storm.TextEditor;
	using Storm.TextEditor.ContentInfo;
	using Storm.TextEditor.Controls;
	using Storm.TextEditor.Controls.Core;
	using Storm.TextEditor.Controls.Core.Globalization;
	using Storm.TextEditor.Controls.Core.Timers;
	using Storm.TextEditor.Controls.IntelliMouse;
	using Storm.TextEditor.Document;
	using Storm.TextEditor.Document.Exporters;
	using Storm.TextEditor.Forms;
	using Storm.TextEditor.Interacting;
	using Storm.TextEditor.Painting;
	using Storm.TextEditor.Parsing;
	using Storm.TextEditor.Parsing.Base;
	using Storm.TextEditor.Parsing.Classes;
	using Storm.TextEditor.Parsing.Language;
	using Storm.TextEditor.Preset;
	using Storm.TextEditor.Preset.Painting;
	using Storm.TextEditor.Preset.TextDraw;
	using Storm.TextEditor.Printing;
	using Storm.TextEditor.Utilities;
	using Storm.TextEditor.Win32;
	using Storm.Win32;

	/// <summary>
	/// Defines how a member in the CodeCompletion should act.
	/// </summary>
	public enum MemberType
	{
		/// <summary>
		/// Act normally - the member is able to be removed.
		/// </summary>
		Normal = 0,

		/// <summary>
		/// The member will be unable to be removed. This can be used for static members.
		/// </summary>
		Unremovable = 1,
	}

	/// <summary>
	/// Defines what the CodeCompletion displays.
	/// </summary>
	public enum ListDisplay
	{
		/// <summary>
		/// The CodeCompletion will display all of its members.
		/// </summary>
		Full = 0,

		/// <summary>
		/// The CodeCompletion will only display a list of given members.
		/// </summary>
		Special = 1,
	}

	/// <summary>
	/// Describes how the code completer should auto complete a word.
	/// </summary>
	public enum CompleteType
	{
		/// <summary>
		/// Complete the word normally.
		/// </summary>
		Normal = 0,

		/// <summary>
		/// Complete the word as if a parenthesis had been typed.
		/// </summary>
		Parenthesis = 1,
	}

	/// <summary>
	/// Class used for bringing CodeCompletion support to the Storm.TextEditor - 
	/// when the user types, the CodeCompletion can suggest what he wants to type 
	/// based on what he already typed.
	/// </summary>
	[ToolboxItem(false)]
	[ToolboxBitmap(typeof(CodeCompletion), "storm16.bmp")]
	[Description("Class used for bringing CodeCompletion support to the Storm.TextEditor - when the user types, the CodeCompletion can suggest what he wants to type based on what he already typed.")]
	public class CodeCompletion
		: Panel
	{
		#region Fields

		private TreeView    _memberAst     = null;
		private ToolTip     _memberTooltip = null;
		private GListBox    _memberList    = null;
		private TextEditor  _textEditor    = null;
		private ListDisplay _listDisplay   = ListDisplay.Full;

		private ListBox.ObjectCollection _items       = null;
		private List<GListBoxItem>       _displayList = null;

		private bool _shouldUpdateList = false;
        private bool _mayAutoComplete  = false;

		private string _typedText      = "";
		private int    _keyCount       = 0;
		private int    _keyCountBounds = 0;

        private GListBoxItem _lastSelectedItem     = null;

		#endregion

		#region Properties

		/// <summary>
		/// Gets or sets the ImageList of the CodeCompletion.
		/// </summary>
		[Browsable(true)]
		[Description("Gets or sets the ImageList of the CodeCompletion.")]
		public ImageList ImageList
		{
			get { return _memberList.ImageList; }
			set { _memberList.ImageList = value; }
		}

		/// <summary>
		/// Gets or sets how the CodeCompletion should display.
		/// </summary>
		[Browsable(false)]
		[Description("Gets or sets how the CodeCompletion should display.")]
		public ListDisplay ListDisplay
		{
			get { return _listDisplay; }
			set
			{
				_listDisplay = value;
				_shouldUpdateList = true;
			}
		}

		/// <summary>
		/// Gets or sets the list of GListBoxItem that the CodeCompletion will display instead of the full list 
		/// when ListDisplay is set to ListDisplay.Special.
		/// </summary>
		[Browsable(false)]
		[Description("Gets or sets the list of GListBoxItem that the CodeCompletion will display instead of the full list when ListDisplay is set to ListDisplay.Special.")]
		public List<GListBoxItem> DisplayList
		{
			get { return _displayList; }
			set
			{
				_displayList = value;
				_shouldUpdateList = true;
			}
		}

		#endregion

        #region Win32

        [DllImport("user32.dll")]
        public static extern bool InvalidateRect(IntPtr hwnd, 
            IntPtr lpRect, bool bErase);

        #endregion

        #region Methods

        #region Public

        /// <summary>
		/// Adds a GListBoxItem to the CodeCompletion.
		/// </summary>
		/// <param name="item">GListBoxItem to add.</param>
		/// <param name="overrideIfExists">If true, overrides existing member(s) with the same Text as the new item.</param>
		/// <returns>Whether the adding of the GListBoxItem was a success.</returns>
		public bool AddMemberItem(GListBoxItem item, bool overrideIfExists)
		{
			bool result = true;
			if (overrideIfExists == true)
			{
				// Loop through all GListBoxItems in the memberList and remove all items
				// with the same Text as the new item.
				foreach (GListBoxItem memberItem in _memberAst.Nodes)
				{
					if (memberItem.Text == item.Text)
						_memberAst.Nodes.Remove(memberItem);
				}

				// Add the item to the memberList.
				_memberAst.Nodes.Add(item);
			}
			else
			{
				// Check if the memberList already contains an item with the same text.
				// If it does, we can't add the item to the memberList.
				foreach (GListBoxItem memberItem in _memberAst.Nodes)
				{
					if (memberItem.Text == item.Text)
					{
						result = false;
						break;
					}
				}

				// Check if the loop through the items found a match.
				if (result == true)
					_memberAst.Nodes.Add(item);
			}

			return result;
		}

		/// <summary>
		/// Removes a specified GListBoxItem from the CodeCompletion.
		/// </summary>
		/// <param name="item">Item to remove.</param>
		public void RemoveMemberItem(GListBoxItem item)
		{
			if (_memberAst.Nodes.Contains(item) == true)
				_memberAst.Nodes.Remove(item);
		}

		/// <summary>
		/// Removes all removable items of the CodeCompletion.
		/// </summary>
		public void RemoveAllMemberItems()
		{
			foreach (GListBoxItem item in _memberAst.Nodes)
			{
				if (item.MemberType == MemberType.Normal)
					_memberAst.Nodes.Remove(item);
			}
		}

		/// <summary>
		/// Sorts all members of the list alphabetically. 
		/// A call to  method is needed when the list should be updated.
		/// </summary>
		public void SortAlphabetically()
		{
			_memberList.Items.Clear();

			GListBoxItem[] items = new GListBoxItem[_memberAst.Nodes.Count + 1];
			int n = 0;

			while ((n < _memberAst.Nodes.Count))
			{
				GListBoxItem memberItem = new GListBoxItem
					(((GListBoxItem)_memberAst.Nodes[n]).Text,
					 ((GListBoxItem)_memberAst.Nodes[n]).ImageIndex,
					 ((GListBoxItem)_memberAst.Nodes[n]).MemberType,
					 ((GListBoxItem)_memberAst.Nodes[n]).Description,
					 ((GListBoxItem)_memberAst.Nodes[n]).Declaration);

				memberItem.Tag = _memberAst.Nodes[n].Tag;
				memberItem.Name = _memberAst.Nodes[n].Name;

				items[n] = memberItem;
				n += 1;
			}

			Array.Sort(items);
			n = 1;

			while ((n < items.Length))
			{
				GListBoxItem item = new GListBoxItem(items[n].Text,
                    items[n].ImageIndex, items[n].MemberType,
                    items[n].Description, items[n].Declaration);

				item.Tag = (string)items[n].Tag;
				_memberList.Items.Add(item);
				_memberList.Invalidate();

				n += 1;
			}
		}

		#endregion

		#region Private

		#region Initialization

		/// <summary>
		/// Initializes the Controls used by the CodeCompletion.
		/// </summary>
		private void InitControls()
		{
			_memberTooltip = new ToolTip();
			_memberList = new GListBox();
			_memberAst = new TreeView();

			#region ListBox

			// Initialize visual fields of the list.
			_memberList.DrawMode = DrawMode.OwnerDrawFixed;
			_memberList.ImageList = null;

			// Initialize screen fields of the list.
			_memberList.Location = new Point(100, 76);
			_memberList.Size = new Size(210, 174);
			_memberList.Dock = DockStyle.Fill;

			// Initialize base settings for the list.
			_memberList.FormattingEnabled = true;
			_memberList.ItemHeight = 17;
			_memberList.Name = "_memberList";
			_memberList.Parent = this;

			// Initialize other fields of the list.
			_memberList.TabIndex = 9;
			_memberList.Visible = true;

			#endregion

			#region AST

			// Initialize visual fields of the TreeView.
			_memberAst.FullRowSelect = true;
			_memberAst.PathSeparator = ".";
			_memberAst.LineColor = Color.FromArgb(109, 109, 111);
           
            // Initialize screen fields of the TreeView.
			_memberAst.Location = new Point(329, 112);
			_memberAst.Size = new Size(355, 216);

			// Initialize other fields of the TreeView.
			_memberAst.Name = "_memberAst";
			_memberAst.TabIndex = 8;
			_memberAst.Visible = false;

			#endregion
			
			// Finally add our Controls so they can be usable.
			Controls.Add(_memberList);
		}

		#endregion

		#region Wrappers

		private Point GetPositionFromCaretIndex()
		{
			return new Point(_textEditor.Caret.Position.X,
							 _textEditor.Caret.Position.Y);
		}

		#endregion

		#region Text/Base EventHandlers

        private void OnKeyDown(object sender, KeyEventArgs e)
        {
			_mayAutoComplete = e.KeyCode == Keys.Space && this.Visible == true;
            if (_mayAutoComplete == true)
            {
                // Space bar has been pressed - AutoComplete.

                this.Hide();
				_memberTooltip.Hide(_textEditor);
				_textEditor.Focus();

				// Don't auto complete if the typed word is already complete.
				if (this.GetLastWord() != (_memberList.SelectedItem as GListBoxItem).Text)
				{
					if (_memberList.SelectedItem != null && (_memberList.SelectedItem as GListBoxItem)
						.CompleteType == CompleteType.Parenthesis)
					{
						this.SelectItem(CompleteType.Parenthesis);
					}
					else
						this.SelectItem(CompleteType.Normal);
				}

				SendKeys.Send(" ");

                _mayAutoComplete = false;
                _typedText = "";
                _keyCount = 0;
                e.Handled = true;
            }
        }

		private void OnListDoubleClick(object sender, EventArgs e)
		{
			if (_memberList.SelectedItem != null)
			{
				// Don't auto complete if the typed word is already complete.
				if (this.GetLastWord() != (_memberList.SelectedItem as GListBoxItem).Text)
				{
					if (_memberList.SelectedItem != null && (_memberList.SelectedItem as GListBoxItem)
						.CompleteType == CompleteType.Parenthesis)
					{
						this.SelectItem(CompleteType.Parenthesis);
					}
					else
						this.SelectItem(CompleteType.Normal);
				}

				this.Hide();
				_memberTooltip.Hide(_textEditor);

				_textEditor.Focus();
			}
		}

		private void OnListItemChanged(object sender, EventArgs e)
		{
			// Show tooltip of the selected item.

			Font font = new Font(_textEditor.FontName, _textEditor.FontSize);
			float fontHeight = font.GetHeight();

			Point point = GetPositionFromCaretIndex();
			point.Y += ((int)fontHeight * _textEditor.Caret.CurrentRow.Index) + ((int)(Math.Ceiling((double)fontHeight + 2)));
			point.X += (100 + Width + 2);

			_memberTooltip.Show((string)((GListBoxItem)_memberList.
				SelectedItem).Description + "\n" + (string)((GListBoxItem)_memberList.
				SelectedItem).Declaration, _textEditor, point.X, point.Y, 6000);

			_textEditor.Focus();
		}

		private void OnTextEditorClick(object sender, EventArgs e)
		{
			this.Hide();
			_memberTooltip.Hide(_textEditor);

			_textEditor.Focus();
		}

		private void OnKeyUp(object sender, KeyEventArgs e)
		{
			if (this.Enabled == true)
			{
				if (_shouldUpdateList == true)
				{
					if (_listDisplay == ListDisplay.Special && _displayList != null)
					{
						_items = _memberList.Items;
						_memberList.Items.Clear();
						_memberAst.Nodes.Clear();
						foreach (GListBoxItem item in _displayList)
						{
							_memberAst.Nodes.Add(item);
							_memberList.Items.Add(item);
						}

						this.SortAlphabetically();
					}
					else
					{
						_memberList.Items.Clear();
						_memberAst.Nodes.Clear();
						foreach (object item in _items)
						{
							_memberAst.Nodes.Add(item as GListBoxItem);
							_memberList.Items.Add(item as GListBoxItem);
						}

						this.SortAlphabetically();
					}

					_shouldUpdateList = false;
				}

				Font font = new Font(_textEditor.FontName, _textEditor.FontSize);
				float fontHeight = (font.GetHeight());

				// LockWindowUpdate to prevent flickering in the TextEditor parent.
				Win32.LockWindowUpdate(this._textEditor.Handle);

				if (e.KeyCode == Keys.Up)
				{
					// The up key moves up our member list, if
					// the list is visible.

					if (this.Visible == true)
					{
						if (_memberList.SelectedIndex > 0)
						{
							_memberList.SelectedIndex -= 1;
							e.Handled = true;
						}
					}

					return;
				}

				if (e.KeyCode == Keys.Down)
				{
					// The up key moves down our member list, if
					// the list is visible.

					if (this.Visible == true)
					{
						if (_memberList.SelectedIndex <
							_memberList.Items.Count - 1)
						{
							_memberList.SelectedIndex += 1;
							e.Handled = true;
						}
					}
					else
					{
						_textEditor.Refresh();
					}

					return;
				}

				if (e.KeyCode == Keys.Oemcomma)
				{
					if (this.Visible == true)
					{
						_typedText = "";
						_keyCount = 0;

						_memberList.Refresh();
						_memberTooltip.Show((string)((GListBoxItem)_memberList.
							SelectedItem).Description + "\n" + (string)((GListBoxItem)_memberList.
							SelectedItem).Declaration, _textEditor, 6000);

						return;
					}
				}

				if (e.KeyCode == Keys.Tab)
				{
					if (this.Visible == true)
					{
						// Tab key has been pressed - AutoComplete.

						this.Hide();
						_memberTooltip.Hide(_textEditor);

						// Don't auto complete if the typed word is already complete.
						if (this.GetLastWord() != (_memberList.SelectedItem as GListBoxItem).Text)
						{
							if (_memberList.SelectedItem != null && (_memberList.SelectedItem as GListBoxItem)
								.CompleteType == CompleteType.Parenthesis)
							{
								this.SelectItem(CompleteType.Parenthesis);
							}
							else
								this.SelectItem(CompleteType.Normal);
						}

						_typedText = "";
						_keyCount = 0;
						e.Handled = true;
						return;
					}
				}

				if (e.KeyCode == Keys.Space)
				{
					if (this.Visible == true)
					{
						this.Hide();
						_memberTooltip.Hide(_textEditor);

						return;
					}
				}

				if (e.KeyCode == Keys.D9 && e.Shift == true)
				{
					// Close bracket key pressed, hide the displayed item tooltip.

					if (this.Visible == true)
					{
						// Don't auto complete if the typed word is already complete.
						if (this.GetLastWord() != (_memberList.SelectedItem as GListBoxItem).Text)
							this.SelectItem(CompleteType.Parenthesis);

						_textEditor.Document.InsertText(")", _textEditor.Caret.Position.X, _textEditor.Caret.Position.Y);
						_textEditor.Caret.MoveRight(false);
					}

					_memberTooltip.Hide(_textEditor);
					this.Hide();

					_keyCount = 0;
					_typedText = "";
					return;
				}

				if (e.KeyCode == Keys.D8 && e.Shift == true)
				{
					if (_memberList.SelectedItem != null && this.Visible == true)
					{
						// Show tooltip of the selected item.

						// Don't auto complete if the typed word is already complete.
						if (this.GetLastWord() != (_memberList.SelectedItem as GListBoxItem).Text)
							this.SelectItem(CompleteType.Parenthesis);

						_textEditor.Document.InsertText("(", _textEditor.Caret.Position.X, _textEditor.Caret.Position.Y);
						_textEditor.Caret.MoveRight(false);

						Point point = GetPositionFromCaretIndex();
						point.Y += ((int)fontHeight * _textEditor.Caret.CurrentRow.Index) + ((int)(Math.Ceiling((double)fontHeight + 2)));
						point.X += (100 + this.Width + 2);

						_memberTooltip.Show((string)((GListBoxItem)_memberList.
							SelectedItem).Description + "\n" + (string)((GListBoxItem)_memberList.
							SelectedItem).Declaration, _textEditor, point.X, point.Y, 6000);

						this.Hide();

						_keyCount = 0;
						_typedText = "";
					}

					return;
				}

				// Keep track of the current character, used
				// for tracking whether to hide the list of members,
				// when the delete button is pressed.
				int i = _textEditor.Selection.SelStart;
				string currentChar = "";

				if (i > 0)
				{
					currentChar = _textEditor.Document.
						Text.Substring(i - 1, 1);
				}

				if ((e.KeyValue > 31 && e.KeyValue < 127 ||
					e.KeyCode == Keys.Back) &&
					e.KeyCode != Keys.Space &&
					e.KeyCode != Keys.Left &&
					e.KeyCode != Keys.Right)
				{
					// Display the member listview if there are any items in it.
					char val = (char)e.KeyValue;
					_typedText = GetLastWord();

					if (_typedText == "")
					{
						// There's obviously no typed text left - hide.
						_memberTooltip.Hide(_textEditor);
						this.Hide();

						// We have to remember to unlock the Window.
						Win32.LockWindowUpdate((IntPtr)0);

						return;
					}

					_keyCount += 1;
					if (_keyCount >= _keyCountBounds)
					{
						_keyCount = 0;

						// Find the position of the caret.
						Point tooltipPoint = GetPositionFromCaretIndex();

						// Setting the position of the tooltip.
						tooltipPoint.Y += ((int)fontHeight * _textEditor.Caret.CurrentRow.Index) + ((int)(Math.Ceiling((double)fontHeight + 2)));
						tooltipPoint.X += (100 + this.Width + 2);

						// Reset the list's position to match the wanted.
						Point point = GetPositionFromCaretIndex();
						point.X += 100;
						point.Y += ((int)fontHeight * _textEditor.Caret.CurrentRow.Index) + ((int)(Math.Ceiling((double)fontHeight + 2)));

						this.Location = point;

						// Letter or number typed, search for it in the list.
						// Loop through all the items in the list, looking
						// for one that starts with the letters typed.
						i = 1;

						bool mayShow = false;
						int toSelect = -1;
						int n = 0;

						while (((n >= _memberList.Items.Count)) == false)
						{
							// Find dots in the itemString.
							// If there's dots, find their
							// index and remove everything
							// before the dot and itself.
							int dotIndex = 0;
							string itemString = _memberList.Items[n].
								ToString().ToLower();

							if (itemString.Contains("."))
							{
								dotIndex = itemString.IndexOf(".");
								itemString = itemString.Substring(dotIndex + 1);
							}

							// We use + 1 because we need to exclude 
							// the dot from the string.
							if (itemString.StartsWith(_typedText.ToLower()))
							{
								toSelect = n;
								mayShow = true;

								// Break out of the loop.
								n = _memberList.Items.Count;
							}

							n += 1;
						}

						if (mayShow == true && toSelect > -1)
						{
							this.Show();
							this.BringToFront();

							_memberList.SelectedIndex = toSelect;
							_memberTooltip.Show((string)((GListBoxItem)_memberList.
								SelectedItem).Description + "\n" + (string)((GListBoxItem)_memberList.
								SelectedItem).Declaration, _textEditor, tooltipPoint.X, tooltipPoint.Y, 6000);
						}
						else
						{
							if (Visible == true)
								this.Hide();
						}
					}

					if (_lastSelectedItem != null &&
						e.KeyCode == Keys.Space)
					{
						_memberList.SelectedItem = _lastSelectedItem;
					}
				}

				Win32.LockWindowUpdate((IntPtr)0);
			}
		}

		#endregion

		#region API

		/// <summary>
		/// Autofills the selected item in the member listbox, by
		/// taking everything before and after the "." in the richtextbox,
		/// and appending the word in the middle.
		/// </summary>
		private void SelectItem(CompleteType completeType)
		{
			if (_memberList.SelectedItem != null)
			{
                // Use a pretty fancy method to remove the already typed text.

                int curN = _typedText.Length;

				if (completeType == CompleteType.Normal)
				{
					while (curN > 0)
					{
						_textEditor.Caret.MoveLeft(true);
						curN--;
					}
				}
				else
				{
					while (curN >= 0)
					{
						_textEditor.Caret.MoveLeft(true);
						curN--;
					}
				}

                _textEditor.Selection.Text = "";

				// Here ends the fancy method. :)
				// Here we will simply insert some text at the caret's position.

				TextPoint pos = _textEditor.Caret.Position;
				_lastSelectedItem = (GListBoxItem)_memberList.SelectedItem;
                _textEditor.Document.InsertText(_lastSelectedItem.Text, pos.X, pos.Y);

				// We will now have another while-loop to make our selection end
				// at the point that the inserted word ends.

				curN = _lastSelectedItem.Text.Length;
				while (curN > 0)
				{
				    _textEditor.Caret.MoveRight(false);
				    curN--;
				}
			}
		}

		/// <summary>
		/// Returns the Word that the Caret is currently at.
		/// </summary>
		/// <returns>The previous word from the Caret position.</returns>
		private string GetLastWord()
		{
			Win32.LockWindowUpdate(_textEditor.Handle);

            string word = "";
            int selStart = _textEditor.Selection.SelStart;

            _textEditor.Selection.SelStart--;
            if (_textEditor.Caret.CurrentWord != null)
                word = _textEditor.Caret.CurrentWord.Text;

            _textEditor.Selection.SelStart = selStart;

			Win32.LockWindowUpdate((IntPtr)0);
			return word;
		}

		#endregion

		#endregion

		#endregion

		/// <summary>
		/// Initializes the CodeCompletion.
		/// </summary>
		/// <param name="textEditor">TextEditor for the CodeCompletion to 
		/// register events for.</param>
		public CodeCompletion(TextEditor textEditor)
		{
			// First set the CodeCompletion TextEditor parent.
			_textEditor = textEditor;

            this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            this.SetStyle(ControlStyles.Selectable, false);
            this.SetStyle(ControlStyles.UserPaint, true);
            this.SetStyle(ControlStyles.ResizeRedraw, true);

			// Initialize our Controls.
			InitControls();

			// Initialize visual setup of the CodeCompletion.
			BackColor = SystemColors.Window;
			Font = new Font("Tahoma", 8.25f, FontStyle.Regular);
            _memberList.Font = Font;

			// EventHandlers.
			_textEditor.KeyUp += OnKeyUp;
            _textEditor.KeyDown += OnKeyDown;
            _textEditor.MouseDown += OnTextEditorClick;

			_memberList.DoubleClick += OnListDoubleClick;
			_memberList.SelectedIndexChanged += OnListItemChanged;

			// We will have to re-focus the TextEditor before we can let 
			// the user go on using it.
			_textEditor.Focus();
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)



Comments and Discussions