瀏覽代碼

Autocomplete for TextView (#1406)

* Added basic autocomplete style dropdown (not working properly yet)

* Autocomplete basically working but rough around the edges

* Changed to Lists and added CloseKey

* Fixed test, made autocomplete equal length

* Added scrolling through autocomplete list

* Made Accept autocomplete do delete and replace instead of append to support caps changes

* Changed Autocomplete ColorScheme to cyan

* Fixed autocomplete render location when TextView is scrolled

* Fixed scrolling and overspill rendering

* Added wordwrap option to SyntaxHighlighting Scenario

* Moved Autocomplete to be member property of TextView

* Made Suggestions a readonly collection and enabled Autocomplete in Editor Scenario

* Added ClipOrPad tests

* Fixed bad merge

* Delayed init of ColorScheme on Autocomplete until needed

* Changed ColorScheme to match Menu bar
Thomas Nind 3 年之前
父節點
當前提交
d60aed79e4

+ 305 - 0
Terminal.Gui/Core/Autocomplete.cs

@@ -0,0 +1,305 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using Rune = System.Rune;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Renders an overlay on another view at a given point that allows selecting
+	/// from a range of 'autocomplete' options.
+	/// </summary>
+	public class Autocomplete {
+
+		/// <summary>
+		/// The maximum width of the autocomplete dropdown
+		/// </summary>
+		public int MaxWidth { get; set; } = 10;
+
+		/// <summary>
+		/// The maximum number of visible rows in the autocomplete dropdown to render
+		/// </summary>
+		public int MaxHeight { get; set; } = 6;
+
+		/// <summary>
+		/// True if the autocomplete should be considered open and visible
+		/// </summary>
+		protected bool Visible { get; set; } = true;
+
+		/// <summary>
+		/// The strings that form the current list of suggestions to render
+		/// based on what the user has typed so far.
+		/// </summary>
+		public ReadOnlyCollection<string> Suggestions { get; protected set; } = new ReadOnlyCollection<string>(new string[0]);
+
+		/// <summary>
+		/// The full set of all strings that can be suggested.
+		/// </summary>
+		/// <returns></returns>
+		public List<string> AllSuggestions { get; set; } = new List<string>();
+
+		/// <summary>
+		/// The currently selected index into <see cref="Suggestions"/> that the user has highlighted
+		/// </summary>
+		public int SelectedIdx { get; set; }
+
+		/// <summary>
+		/// When more suggestions are available than can be rendered the user
+		/// can scroll down the dropdown list.  This indicates how far down they
+		/// have gone
+		/// </summary>
+		public int ScrollOffset {get;set;}
+
+		/// <summary>
+		/// The colors to use to render the overlay.  Accessing this property before
+		/// the Application has been initialised will cause an error
+		/// </summary>
+		public ColorScheme ColorScheme { 
+			get
+			{
+				if(colorScheme == null)
+				{
+					colorScheme = Colors.Menu;
+				}
+				return colorScheme;
+			}
+		 	set
+			{
+				ColorScheme = value;
+			}
+		}
+
+		private ColorScheme colorScheme;
+
+		/// <summary>
+		/// The key that the user must press to accept the currently selected autocomplete suggestion
+		/// </summary>
+		public Key SelectionKey { get; set; } = Key.Enter;
+
+		/// <summary>
+		/// The key that the user can press to close the currently popped autocomplete menu
+		/// </summary>
+		public Key CloseKey {get;set;} = Key.Esc;
+
+		/// <summary>
+		/// Renders the autocomplete dialog inside the given <paramref name="view"/> at the
+		/// given point.
+		/// </summary>
+		/// <param name="view">The view the overlay should be rendered into</param>
+		/// <param name="renderAt"></param>
+		public void RenderOverlay (View view, Point renderAt)
+		{
+			if (!Visible || !view.HasFocus || Suggestions.Count == 0) {
+				return;
+			}
+
+			view.Move (renderAt.X, renderAt.Y);
+
+			// don't overspill vertically
+			var height = Math.Min(view.Bounds.Height - renderAt.Y,MaxHeight);
+
+			var toRender = Suggestions.Skip(ScrollOffset).Take(height).ToArray();
+
+			if(toRender.Length == 0)
+			{
+				return;
+			}
+
+			var width = Math.Min(MaxWidth,toRender.Max(s=>s.Length));
+
+			// don't overspill horizontally
+			width = Math.Min(view.Bounds.Width - renderAt.X ,width);
+
+			for(int i=0;i<toRender.Length; i++) {
+
+				if(i ==  SelectedIdx - ScrollOffset) {
+					Application.Driver.SetAttribute (ColorScheme.Focus);
+				}
+				else {
+					Application.Driver.SetAttribute (ColorScheme.Normal);
+				}
+
+				view.Move (renderAt.X, renderAt.Y+i);
+
+				var text = TextFormatter.ClipOrPad(toRender[i],width);
+
+				Application.Driver.AddStr (text );
+			}
+		}
+
+		/// <summary>
+		/// Updates <see cref="SelectedIdx"/> to be a valid index within <see cref="Suggestions"/>
+		/// </summary>
+		public void EnsureSelectedIdxIsValid()
+		{				
+			SelectedIdx = Math.Max (0,Math.Min (Suggestions.Count - 1, SelectedIdx));
+			
+			// if user moved selection up off top of current scroll window
+			if(SelectedIdx < ScrollOffset)
+			{
+				ScrollOffset = SelectedIdx;
+			}
+
+			// if user moved selection down past bottom of current scroll window
+			while(SelectedIdx >= ScrollOffset + MaxHeight ){
+				ScrollOffset++;
+			}
+		}
+
+		/// <summary>
+		/// Handle key events before <paramref name="hostControl"/> e.g. to make key events like
+		/// up/down apply to the autocomplete control instead of changing the cursor position in 
+		/// the underlying text view.
+		/// </summary>
+		/// <param name="hostControl"></param>
+		/// <param name="kb"></param>
+		/// <returns></returns>
+		public bool ProcessKey (TextView hostControl, KeyEvent kb)
+		{
+			if(IsWordChar((char)kb.Key))
+			{
+				Visible = true;
+			}
+
+			if(!Visible || Suggestions.Count == 0) {
+				return false;
+			}
+
+			if (kb.Key == Key.CursorDown) {
+				SelectedIdx++;
+				EnsureSelectedIdxIsValid();
+				hostControl.SetNeedsDisplay ();
+				return true;
+			}
+
+			if (kb.Key == Key.CursorUp) {
+				SelectedIdx--;
+				EnsureSelectedIdxIsValid();
+				hostControl.SetNeedsDisplay ();
+				return true;
+			}
+
+			if(kb.Key == SelectionKey && SelectedIdx >=0 && SelectedIdx < Suggestions.Count) {
+
+				var accepted = Suggestions [SelectedIdx];
+								
+				var typedSoFar = GetCurrentWord (hostControl) ?? "";
+				
+				if(typedSoFar.Length < accepted.Length) {
+
+					// delete the text
+					for(int i=0;i<typedSoFar.Length;i++)
+					{
+						hostControl.DeleteTextBackwards();
+					}
+
+					hostControl.InsertText (accepted);
+					return true;
+				}
+
+				return false;
+			}
+
+			if(kb.Key == CloseKey)
+			{
+				ClearSuggestions ();
+				Visible = false;
+				hostControl.SetNeedsDisplay();
+				return true;
+			}
+
+			return false;
+		}
+
+		/// <summary>
+		/// Clears <see cref="Suggestions"/>
+		/// </summary>
+		public void ClearSuggestions ()
+		{
+			Suggestions = Enumerable.Empty<string> ().ToList ().AsReadOnly ();
+		}
+
+
+		/// <summary>
+		/// Populates <see cref="Suggestions"/> with all strings in <see cref="AllSuggestions"/> that
+		/// match with the current cursor position/text in the <paramref name="hostControl"/>
+		/// </summary>
+		/// <param name="hostControl">The text view that you want suggestions for</param>
+		public void GenerateSuggestions (TextView hostControl)
+		{
+			// if there is nothing to pick from
+			if(AllSuggestions.Count == 0) {
+				ClearSuggestions ();
+				return;
+			}
+
+			var currentWord = GetCurrentWord (hostControl); 
+
+			if(string.IsNullOrWhiteSpace(currentWord)) {
+				ClearSuggestions ();
+			}
+			else {
+				Suggestions = AllSuggestions.Where (o => 
+				o.StartsWith (currentWord, StringComparison.CurrentCultureIgnoreCase) &&
+				!o.Equals(currentWord,StringComparison.CurrentCultureIgnoreCase)
+				).ToList ().AsReadOnly();
+
+				EnsureSelectedIdxIsValid();
+			}
+		}
+
+		private string GetCurrentWord (TextView hostControl)
+		{
+			var currentLine = hostControl.GetCurrentLine ();
+			var cursorPosition = Math.Min (hostControl.CurrentColumn, currentLine.Count);
+			return IdxToWord (currentLine, cursorPosition);
+		}
+
+		private string IdxToWord (List<Rune> line, int idx)
+		{
+			StringBuilder sb = new StringBuilder ();
+
+			// do not generate suggestions if the cursor is positioned in the middle of a word
+			bool areMidWord;
+
+			if(idx == line.Count) {
+				// the cursor positioned at the very end of the line
+				areMidWord = false;
+			}
+			else {
+				// we are in the middle of a word if the cursor is over a letter/number
+				areMidWord = IsWordChar (line [idx]);
+			}
+
+			// if we are in the middle of a word then there is no way to autocomplete that word
+			if(areMidWord) {
+				return null;
+			}
+
+			// we are at the end of a word.  Work out what has been typed so far
+			while(idx-- > 0) {
+
+				if(IsWordChar(line [idx])) {
+					sb.Insert(0,(char)line [idx]);
+				}
+				else {
+					break;
+				}
+			}
+			return sb.ToString ();
+		}
+
+		/// <summary>
+		/// Return true if the given symbol should be considered part of a word
+		/// and can be contained in matches.  Base behaviour is to use <see cref="char.IsLetterOrDigit(char)"/>
+		/// </summary>
+		/// <param name="rune"></param>
+		/// <returns></returns>
+		public virtual bool IsWordChar (Rune rune)
+		{
+			return Char.IsLetterOrDigit ((char)rune);
+		}
+	}
+}

+ 1 - 18
Terminal.Gui/Core/Graphs/Annotations.cs

@@ -187,7 +187,7 @@ namespace Terminal.Gui.Graphs {
 				// add the text
 				graph.Move (x + 1, y + linesDrawn);
 
-				string str = TruncateOrPad (entry.Item2, availableWidth - 1);
+				string str = TextFormatter.ClipOrPad (entry.Item2, availableWidth - 1);
 				Application.Driver.AddStr (str);
 
 				linesDrawn++;
@@ -199,23 +199,6 @@ namespace Terminal.Gui.Graphs {
 			}
 		}
 
-		private string TruncateOrPad (string text, int width)
-		{
-			if (string.IsNullOrEmpty (text))
-				return text;
-
-			// if value is not wide enough
-			if (text.Sum (c => Rune.ColumnWidth (c)) < width) {
-
-				// pad it out with spaces to the given alignment
-				int toPad = width - (text.Sum (c => Rune.ColumnWidth (c)));
-
-				return text + new string (' ', toPad);
-			}
-
-			// value is too wide
-			return new string (text.TakeWhile (c => (width -= Rune.ColumnWidth (c)) >= 0).ToArray ());
-		}
 
 		/// <summary>
 		/// Adds an entry into the legend.  Duplicate entries are permissable

+ 27 - 0
Terminal.Gui/Core/TextFormatter.cs

@@ -386,6 +386,33 @@ namespace Terminal.Gui {
 			return ustring.Make (runes);
 		}
 
+
+		/// <summary>
+		/// Adds trailing whitespace or truncates <paramref name="text"/>
+		/// so that it fits exactly <paramref name="width"/> console units.
+		/// Note that some unicode characters take 2+ columns
+		/// </summary>
+		/// <param name="text"></param>
+		/// <param name="width"></param>
+		/// <returns></returns>
+		public static string ClipOrPad (string text, int width)
+		{
+			if (string.IsNullOrEmpty (text))
+				return text;
+
+			// if value is not wide enough
+			if (text.Sum (c => Rune.ColumnWidth (c)) < width) {
+
+				// pad it out with spaces to the given alignment
+				int toPad = width - (text.Sum (c => Rune.ColumnWidth (c)));
+
+				return text + new string (' ', toPad);
+			}
+
+			// value is too wide
+			return new string (text.TakeWhile (c => (width -= Rune.ColumnWidth (c)) >= 0).ToArray ());
+		}
+
 		/// <summary>
 		/// Formats the provided text to fit within the width provided using word wrapping.
 		/// </summary>

+ 56 - 2
Terminal.Gui/Views/TextView.cs

@@ -906,6 +906,12 @@ namespace Terminal.Gui {
 		/// </summary>
 		public event Action TextChanged;
 
+		/// <summary>
+		/// Provides autocomplete context menu based on suggestions at the current cursor
+		/// position.  Populate <see cref="Autocomplete.AllSuggestions"/> to enable this feature
+		/// </summary>
+		public Autocomplete Autocomplete { get; protected set; } = new Autocomplete ();
+
 #if false
 		/// <summary>
 		///   Changed event, raised when the text has clicked.
@@ -1779,6 +1785,15 @@ namespace Terminal.Gui {
 			}
 
 			PositionCursor ();
+
+			// draw autocomplete
+			Autocomplete.GenerateSuggestions (this);
+
+			var renderAt = new Point (
+				CursorPosition.X - LeftColumn,
+			(CursorPosition.Y + 1) - TopRow);
+
+			Autocomplete.RenderOverlay (this, renderAt);
 		}
 
 		///<inheritdoc/>
@@ -1799,6 +1814,30 @@ namespace Terminal.Gui {
 			Clipboard.Contents += text;
 		}
 
+
+		/// <summary>
+		/// Inserts the given <paramref name="toAdd"/> text at the current cursor position
+		/// exactly as if the user had just typed it
+		/// </summary>
+		/// <param name="toAdd">Text to add</param>
+		public void InsertText (string toAdd)
+		{
+			foreach(var ch in toAdd) {
+
+				Key key;
+
+				try {
+					key = (Key)ch;
+				} catch (Exception) {
+
+					throw new ArgumentException($"Cannot insert character '{ch}' because it does not map to a Key");
+				}
+				
+
+				InsertText (new KeyEvent () { Key = key });
+			}
+		}
+		
 		void Insert (Rune rune)
 		{
 			var line = GetCurrentLine ();
@@ -1843,7 +1882,13 @@ namespace Terminal.Gui {
 			return ustring.Make (encoded);
 		}
 
-		List<Rune> GetCurrentLine () => model.GetLine (currentRow);
+		/// <summary>
+		/// Returns the characters on the current line (where the cursor is positioned).
+		/// Use <see cref="CurrentColumn"/> to determine the position of the cursor within
+		/// that line
+		/// </summary>
+		/// <returns></returns>
+		public List<Rune> GetCurrentLine () => model.GetLine (currentRow);
 
 		void InsertText (ustring text)
 		{
@@ -2026,6 +2071,11 @@ namespace Terminal.Gui {
 				}
 			}
 
+			// Give autocomplete first opportunity to respond to key presses
+			if (Autocomplete.ProcessKey (this, kb)) {
+				return true;
+			}
+
 			// Handle some state here - whether the last command was a kill
 			// operation and the column tracking (up/down)
 			switch (kb.Key) {
@@ -2598,7 +2648,11 @@ namespace Terminal.Gui {
 			return false;
 		}
 
-		bool DeleteTextBackwards ()
+		/// <summary>
+		/// Deletes a single character from the position of the cursor
+		/// </summary>
+		/// <returns></returns>
+		public bool DeleteTextBackwards ()
 		{
 			if (currentColumn > 0) {
 				// Delete backwards 

+ 27 - 0
UICatalog/Scenarios/Editor.cs

@@ -2,6 +2,8 @@
 using System.Collections.Generic;
 using System.Text;
 using Terminal.Gui;
+using System.Linq;
+using System.Text.RegularExpressions;
 
 namespace UICatalog {
 	[ScenarioMetadata (Name: "Editor", Description: "A Terminal.Gui Text Editor via TextView")]
@@ -95,6 +97,7 @@ namespace UICatalog {
 				}),
 				new MenuBarItem ("Forma_t", new MenuItem [] {
 					CreateWrapChecked (),
+          CreateAutocomplete(),
 					CreateAllowsTabChecked ()
 				}),
 				new MenuBarItem ("_Responder", new MenuItem [] {
@@ -473,6 +476,30 @@ namespace UICatalog {
 			return item;
 		}
 
+		private MenuItem CreateAutocomplete()
+		{
+			var auto = new MenuItem ();
+			auto.Title = "Autocomplete";
+			auto.CheckType |= MenuItemCheckStyle.Checked;
+			auto.Checked = false;
+			auto.Action += () => {
+				if(auto.Checked = !auto.Checked) {
+					// setup autocomplete with all words currently in the editor
+					_textView.Autocomplete.AllSuggestions = 
+					
+					Regex.Matches(_textView.Text.ToString(),"\\w+")
+					.Select(s=>s.Value)
+					.Distinct ().ToList ();
+				}
+				else {
+					_textView.Autocomplete.AllSuggestions.Clear ();
+
+				}
+			};
+
+			return auto;
+		}
+
 		private MenuItem CreateAllowsTabChecked ()
 		{
 			var item = new MenuItem {

+ 74 - 41
UICatalog/Scenarios/SyntaxHighlighting.cs

@@ -1,13 +1,9 @@
 
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Linq;
-using System.Text;
 using System.Text.RegularExpressions;
-using System.Threading.Tasks;
 using Terminal.Gui;
-using static UICatalog.Scenario;
 using Attribute = Terminal.Gui.Attribute;
 
 namespace UICatalog.Scenarios {
@@ -15,47 +11,55 @@ namespace UICatalog.Scenarios {
 	[ScenarioCategory ("Controls")]
 	class SyntaxHighlighting : Scenario {
 
-			public override void Setup ()
-			{
-				Win.Title = this.GetName ();
-				Win.Y = 1; // menu
-				Win.Height = Dim.Fill (1); // status bar
-				Top.LayoutSubviews ();
-
-				var menu = new MenuBar (new MenuBarItem [] {
-				new MenuBarItem ("_File", new MenuItem [] {
-					new MenuItem ("_Quit", "", () => Quit()),
-				})
-				});
-				Top.Add (menu);
-
-				var textView = new SqlTextView () {
-					X = 0,
-					Y = 0,
-					Width = Dim.Fill (),
-					Height = Dim.Fill (1),
-				};
-
-				textView.Init();
-
-				textView.Text = "SELECT TOP 100 * \nfrom\n MyDb.dbo.Biochemistry;";
-				
-				Win.Add (textView);
+		SqlTextView textView;
+		MenuItem miWrap;
+
+		public override void Setup ()
+		{
+			Win.Title = this.GetName ();
+			Win.Y = 1; // menu
+			Win.Height = Dim.Fill (1); // status bar
+			Top.LayoutSubviews ();
+
+			var menu = new MenuBar (new MenuBarItem [] {
+			new MenuBarItem ("_File", new MenuItem [] {
+				miWrap =  new MenuItem ("_Word Wrap", "", () => WordWrap()){CheckType = MenuItemCheckStyle.Checked},
+				new MenuItem ("_Quit", "", () => Quit()),
+			})
+			});
+			Top.Add (menu);
 
-				var statusBar = new StatusBar (new StatusItem [] {
-				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
+			textView = new SqlTextView () {
+				X = 0,
+				Y = 0,
+				Width = Dim.Fill (),
+				Height = Dim.Fill (1),
+			};
 
+			textView.Init();
+
+			textView.Text = "SELECT TOP 100 * \nfrom\n MyDb.dbo.Biochemistry;";
+
+			Win.Add (textView);
+
+			var statusBar = new StatusBar (new StatusItem [] {
+				new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()),
 			});
 
 
-				Top.Add (statusBar);
-			}
+			Top.Add (statusBar);
+		}
 
+		private void WordWrap ()
+		{
+			miWrap.Checked = !miWrap.Checked;
+			textView.WordWrap = miWrap.Checked;
+		}
 
-			private void Quit ()
-			{
-				Application.RequestStop ();
-			}
+		private void Quit ()
+		{
+			Application.RequestStop ();
+		}
 
 		private class SqlTextView : TextView{
 
@@ -65,13 +69,41 @@ namespace UICatalog.Scenarios {
 			private Attribute magenta;
 
 
-			public void Init()
+		public void Init()
 			{
 				keywords.Add("select");
 				keywords.Add("distinct");
 				keywords.Add("top");
 				keywords.Add("from");
-				keywords.Add ("create");
+				keywords.Add("create");
+				keywords.Add("CIPHER");
+				keywords.Add("CLASS_ORIGIN");
+				keywords.Add("CLIENT");
+				keywords.Add("CLOSE");
+				keywords.Add("COALESCE");
+				keywords.Add("CODE");
+				keywords.Add("COLUMNS");
+				keywords.Add("COLUMN_FORMAT");
+				keywords.Add("COLUMN_NAME");
+				keywords.Add("COMMENT");
+				keywords.Add("COMMIT");
+				keywords.Add("COMPACT");
+				keywords.Add("COMPLETION");
+				keywords.Add("COMPRESSED");
+				keywords.Add("COMPRESSION");
+				keywords.Add("CONCURRENT");
+				keywords.Add("CONNECT");
+				keywords.Add("CONNECTION");
+				keywords.Add("CONSISTENT");
+				keywords.Add("CONSTRAINT_CATALOG");
+				keywords.Add("CONSTRAINT_SCHEMA");
+				keywords.Add("CONSTRAINT_NAME");
+				keywords.Add("CONTAINS");
+				keywords.Add("CONTEXT");
+				keywords.Add("CONTRIBUTORS");
+				keywords.Add("COPY");
+				keywords.Add("CPU");
+				keywords.Add("CURSOR_NAME");
 				keywords.Add ("primary");
 				keywords.Add ("key");
 				keywords.Add ("insert");
@@ -96,7 +128,6 @@ namespace UICatalog.Scenarios {
 				keywords.Add ("is");
 				keywords.Add ("drop");
 				keywords.Add ("database");
-				keywords.Add ("column");
 				keywords.Add ("table");
 				keywords.Add ("having");
 				keywords.Add ("in");
@@ -105,6 +136,8 @@ namespace UICatalog.Scenarios {
 				keywords.Add ("union");
 				keywords.Add ("exists");
 
+				Autocomplete.AllSuggestions = keywords.ToList();
+
 				magenta = Driver.MakeAttribute (Color.Magenta, Color.Black);
 				blue = Driver.MakeAttribute (Color.Cyan, Color.Black);
 				white = Driver.MakeAttribute (Color.White, Color.Black);

+ 29 - 0
UnitTests/AutocompleteTests.cs

@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Terminal.Gui;
+using Xunit;
+
+namespace UnitTests {
+	public class AutocompleteTests {
+
+		[Fact][AutoInitShutdown]
+		public void Test_GenerateSuggestions_Simple()
+		{
+			var ac = new Autocomplete ();
+			ac.AllSuggestions = new List<string> { "fish","const","Cobble"};
+
+			var tv = new TextView ();
+			tv.InsertText ("co");
+
+			ac.GenerateSuggestions (tv);
+
+			Assert.Equal (2, ac.Suggestions.Count);
+			Assert.Equal ("const", ac.Suggestions[0]);
+			Assert.Equal ("Cobble", ac.Suggestions[1]);
+
+		}
+	}
+}

+ 13 - 0
UnitTests/TextFormatterTests.cs

@@ -2538,6 +2538,19 @@ namespace Terminal.Gui.Core {
 			Application.Shutdown ();
 		}
 
+		[Fact]
+		public void TestClipOrPad_ShortWord()
+		{
+			// word is short but we want it to fill 6 so it should be padded
+			Assert.Equal ("fff   ", TextFormatter.ClipOrPad ("fff", 6));
+		}
+
+		[Fact]
+		public void TestClipOrPad_LongWord ()
+		{
+			// word is long but we want it to fill 3 space only
+			Assert.Equal ("123", TextFormatter.ClipOrPad ("123456789", 3));
+		}
 		[Fact]
 		public void Draw_Vertical_Throws_IndexOutOfRangeException_With_Negative_Bounds ()
 		{