context: 
  using System;
  using System.Collections.Generic;
  using System.IO;
  using System.Runtime.InteropServices;
  using System.Text.RegularExpressions;
  using Mono.Unix;
  
  namespace Tomboy
  {
  	public class NoteRenameWatcher : NoteAddin
  	{
  		bool editing_title;
  		Gtk.TextTag title_tag;
  		HIGMessageDialog title_taken_dialog = null;
  
  		public override void Initialize ()
  		{
  			title_tag = Note.TagTable.Lookup ("note-title");
  		}
  
  		public override void Shutdown ()
  		{
  			// Do nothing.
  		}
  
  		Gtk.TextIter TitleEnd
  		{
  			get {
  				Gtk.TextIter line_end = Buffer.StartIter;
  				line_end.ForwardToLineEnd ();
  				return line_end;
  			}
  		}
  
  		Gtk.TextIter TitleStart
  		{
  			get {
  				return Buffer.StartIter;
  			}
  		}
  
  		public override void OnNoteOpened ()
  		{
  			Buffer.MarkSet += OnMarkSet;
  			Buffer.InsertText += OnInsertText;
  			Buffer.DeleteRange += OnDeleteRange;
  
  			Window.Editor.FocusOutEvent += OnEditorFocusOut;
  
  			// FIXME: Needed because we hide on delete event, and
  			// just hide on accelerator key, so we can't use delete
  			// event.  This means the window will flash if closed
  			// with a name clash.
  			Window.UnmapEvent += OnWindowClosed;
  
  			// Clean up title line
  			Buffer.RemoveAllTags (TitleStart, TitleEnd);
  			Buffer.ApplyTag (title_tag, TitleStart, TitleEnd);
  		}
  
  		void OnEditorFocusOut (object sender, Gtk.FocusOutEventArgs args)
  		{
  			// TODO: Duplicated from Update(); refactor instead
  			if (editing_title) {
  				Changed ();
  				UpdateNoteTitle ();
  				editing_title = false;
  			}
  		}
  
  		// This only gets called on an explicit move, not when typing
  		void OnMarkSet (object sender, Gtk.MarkSetArgs args)
  		{
  			if (args.Mark == Buffer.InsertMark) {
  				Update ();
  			}
  		}
  
  		void OnInsertText (object sender, Gtk.InsertTextArgs args)
  		{
  			Update ();
  
  			Gtk.TextIter end = args.Pos;
  			end.ForwardToLineEnd ();
  
  			// Avoid lingering note-title after a multi-line insert...
  			Buffer.RemoveTag (title_tag, TitleEnd, end);
  			
  			//In the case of large copy and paste operations, show the end of the block
  			this.Window.Editor.ScrollMarkOnscreen (this.Buffer.InsertMark);
  		}
  
  		void OnDeleteRange (object sender, Gtk.DeleteRangeArgs args)
  		{
  			Update ();
  		}
  
  		void Update ()
  		{
  			Gtk.TextIter insert = Buffer.GetIterAtMark (Buffer.InsertMark);
  			Gtk.TextIter selection = Buffer.GetIterAtMark (Buffer.SelectionBound);
  
  			// FIXME: Handle middle-click paste when insert or
  			// selection isn't on line 0, which means we won't know
  			// about the edit.
  
  			if (insert.Line == 0 || selection.Line == 0) {
  				if (!editing_title)
  					editing_title = true;
  				Changed ();
  			} else {
  				if (editing_title) {
  					Changed ();
  					UpdateNoteTitle ();
  					editing_title = false;
  				}
  			}
  		}
  
  		void Changed ()
  		{
  			// Make sure the title line is big and red...
  			Buffer.RemoveAllTags (TitleStart, TitleEnd);
  			Buffer.ApplyTag (title_tag, TitleStart, TitleEnd);
  
  			// NOTE: Use "(Untitled #)" for empty first lines...
  			string title = TitleStart.GetSlice (TitleEnd).Trim ();
  			if (title == string.Empty)
  				title = GetUniqueUntitled ();
  
  			// Only set window title here, to give feedback that we
  			// are indeed changing the title.
  			Window.Title = title;
  		}
  
  		[GLib.ConnectBefore]
  		void OnWindowClosed (object sender, Gtk.UnmapEventArgs args)
  		{
  			if (!editing_title)
  				return;
  
  			if (!UpdateNoteTitle ()) {
  				args.RetVal = true;
  				return;
  			}
  		}
  
  		string GetUniqueUntitled ()
  		{
  			int new_num = Manager.Notes.Count;
  			string temp_title;
  
  			while (true) {
  				temp_title = String.Format (Catalog.GetString ("(Untitled {0})"),
  				                            ++new_num);
  				if (Manager.Find (temp_title) == null)
  					return temp_title;
  			}
  		}
  
  		bool UpdateNoteTitle ()
  		{
  			string title = Window.Title;
  
  			Note existing = Manager.Find (title);
  			if (existing != null && existing != this.Note) {
  				// Present the window in case it got unmapped...
  				// FIXME: Causes flicker.
  				Note.Window.Present ();
  
  				ShowNameClashError (title);
  				return false;
  			}
  
  			Logger.Debug ("Renaming note from {0} to {1}", Note.Title, title);
  			Note.SetTitle (title, true);
  
  			return true;
  		}
  
  		void ShowNameClashError (string title)
  		{
  			// Select text from TitleStart to TitleEnd
  			Buffer.MoveMark (Buffer.SelectionBound, TitleStart);
  			Buffer.MoveMark (Buffer.InsertMark, TitleEnd);
  
  			string message =
  			        String.Format (Catalog.GetString ("A note with the title " +
  			                                          "<b>{0}</b> already exists. " +
  			                                          "Please choose another name " +
  			                                          "for this note before " +
  			                                          "continuing."),
  			                       title);
  
  			/// Only pop open a warning dialog when one isn't already present
  			/// Had to add this check because this method is being called twice.
  			if (title_taken_dialog == null) {
  				title_taken_dialog =
  				        new HIGMessageDialog (Window,
  				                              Gtk.DialogFlags.DestroyWithParent,
  				                              Gtk.MessageType.Warning,
  				                              Gtk.ButtonsType.Ok,
  				                              Catalog.GetString ("Note title taken"),
  				                              message);
  				title_taken_dialog.Modal = true;
  				title_taken_dialog.Response +=
  				delegate (object sender, Gtk.ResponseArgs args) {
  					title_taken_dialog.Destroy ();
  					title_taken_dialog = null;
  				};
  			}
  
  			title_taken_dialog.Present ();
  		}
  	}
  
  	#if FIXED_GTKSPELL
  	public class NoteSpellChecker : NoteAddin
  	{
  		IntPtr obj_ptr = IntPtr.Zero;
  
  		static bool gtkspell_available_tested;
  		static bool gtkspell_available_result;
  
  		[DllImport ("libgtkspell")]
  		static extern IntPtr gtkspell_new_attach (IntPtr text_view,
  			                string locale,
  			                IntPtr error);
  
  		[DllImport ("libgtkspell")]
  		static extern void gtkspell_detach (IntPtr obj);
  
  		static bool DetectGtkSpellAvailable()
  		{
  			try {
  				Gtk.TextView test_view = new Gtk.TextView ();
  				IntPtr test_ptr = gtkspell_new_attach (test_view.Handle,
  				                                       null,
  				                                       IntPtr.Zero);
  				if (test_ptr != IntPtr.Zero)
  					gtkspell_detach (test_ptr);
  				return true;
  			} catch {
  			return false;
  		}
  	}
  
  	public static bool GtkSpellAvailable
  	{
  		get {
  			if (!gtkspell_available_tested) {
  					gtkspell_available_result = DetectGtkSpellAvailable ();
  					gtkspell_available_tested = true;
  				}
  				return gtkspell_available_result;
  			}
  		}
  
  		public NoteSpellChecker ()
  		{
  			if (!GtkSpellAvailable) {
  				throw new Exception();
  			}
  		}
  
  		public override void Initialize ()
  		{
  			// Do nothing.
  		}
  
  		public override void Shutdown ()
  		{
  			// Do nothing.
  		}
  
  		public override void OnNoteOpened ()
  		{
  			Preferences.SettingChanged += OnEnableSpellcheckChanged;
  
  			if ((bool) Preferences.Get (Preferences.ENABLE_SPELLCHECKING)) {
  				Attach ();
  			}
  		}
  
  		void Attach ()
  		{
  			// Make sure we add this tag before attaching, so
  			// gtkspell will use our version.
  			if (Note.TagTable.Lookup ("gtkspell-misspelled") == null) {
  				NoteTag tag = new NoteTag ("gtkspell-misspelled");
  				tag.CanSerialize = false;
  				tag.CanSpellCheck = true;
  				tag.Underline = Pango.Underline.Error;
  				Note.TagTable.Add (tag);
  			}
  
  			Buffer.TagApplied += TagApplied;
  
  			if (obj_ptr == IntPtr.Zero) {
  				obj_ptr = gtkspell_new_attach (Window.Editor.Handle,
  				                               null,
  				                               IntPtr.Zero);
  			}
  		}
  
  		void Detach ()
  		{
  			Buffer.TagApplied -= TagApplied;
  
  			if (obj_ptr != IntPtr.Zero) {
  				gtkspell_detach (obj_ptr);
  				obj_ptr = IntPtr.Zero;
  			}
  		}
  
  		void OnEnableSpellcheckChanged (object sender, NotifyEventArgs args)
  		{
  			if (args.Key != Preferences.ENABLE_SPELLCHECKING)
  				return;
  
  			if ((bool) args.Value) {
  				Attach ();
  			} else {
  				Detach ();
  			}
  		}
  
  		void TagApplied (object sender, Gtk.TagAppliedArgs args)
  		{
  			bool remove = false;
  
  			if (args.Tag.Name == "gtkspell-misspelled") {
  				// Remove misspelled tag for links & title
  				foreach (Gtk.TextTag tag in args.Start.Tags) {
  					if (tag != args.Tag &&
  					                !NoteTagTable.TagIsSpellCheckable (tag)) {
  						remove = true;
  						break;
  					}
  				}
  				// Remove misspelled tag for acronyms (in all caps)
  				if (!args.Start.EndsWord() && System.Char.IsUpper(args.Start.Char, 0)) {
  					Gtk.TextIter char_iter = args.Start;
  					remove = true;
  					while (!char_iter.EndsWord()) {
  						if (Char.IsLower(char_iter.Char, 0)) {
  							remove = false;
  							break;
  						}
  						else {
  							char_iter.ForwardChar();
  						}
  					}
  				}
  			} else if (!NoteTagTable.TagIsSpellCheckable (args.Tag)) {
  				remove = true;
  			}
  
  			if (remove) {
  				Buffer.RemoveTag ("gtkspell-misspelled",
  				                  args.Start,
  				                  args.End);
  			}
  		}
  	}
  	#else
  	// Add in a "dummy" NoteSpellChecker class so that Mono.Addins doesn't
  	// complain at startup.  NoteSpellChecker is specified in Tomboy.addin.xml.
  	public class NoteSpellChecker : NoteAddin
  	{
  		public override void Initialize ()
  		{
  		}
  		
  		public override void Shutdown ()
  		{
  		}
  		
  		public override void OnNoteOpened ()
  		{
  		}
  	}
  	#endif
  
  	public class NoteUrlWatcher : NoteAddin
  	{
  		Gtk.TextMark click_mark;
  
  		const string URL_REGEX =
  			//@"((\b((news|http|https|ftp|file|irc)://|mailto:|(www|ftp)\.|\S*@\S*\.)|(?<=^|\s)/\S+/|(?<=^|\s)~/\S+)\S*\b/?)";
  			    @"("+
  		    		@"(("+
  		    		    @"((?<=(?<starter>("+        // preceded by a starter
  		    			 @"(?<starterA>(\())"+	//	opening parenthesis closed by )
  		    			@"|(?<starterB>(\[))"+	// or	opening bracket	    closed by ]
  		    			@"|(?<starterC>(\{))"+	// or	opening thing	    closed by }
  		    		    @")))"+
  		    		    @"|\b)"+			    //  or at beginning of a word \b
  		    		    @"("+
  		    			@"(news|http|https|ftp|file|irc)://"+    // http:// ...
  		    		        @"|mailto:"+				// or mailto...
  		    			@"|(www|ftp)\."+				// or www...
  		    		        @"|\S*@\S*\."+				// or email adress
  		    		    @")"+
  		    		@")"+
  		    		@"|("+
  		    		    @"((?<=(?<starter>("+        // preceded by a starter
  		    			 @"(?<starterA>(\())"+	//	opening parenthesis closed by )
  		    			@"|(?<starterB>(\[))"+	// or	opening bracket	    closed by ]
  		    			@"|(?<starterC>(\{))"+	// or	opening thing	    closed by }
  		    		    @")))"+
  				    @"|(?<=^|\s))"+
  		    		    @"("+
  			    		    @"(/\S+/)"+				// or starting with '/'
  			    		    @"|(~/\S+?)"+				// or starting with '~/'
  		    		    @")"+
  		    		@"))"+
  		    		@"(?(starter)"+			    //
  		    		    @"(\S(?!("+			    // if starter detected : forbid ender
  									//ignore this comment (for syntax) ([{
  		    			@"(?(starterA)\)|"+				// closing parenthesis
  		    			@"(?(starterB)\]|"+				// closing bracket
  		    			@"(?(starterC)\})))"+				// closing thing
  		    		    @")))*\S?"+
  		    		    @"|\S*/?"+			    // else anything without space
  		    		@")"+
  		    	    @")";
  
  		
  
  		static Regex regex;
  		static bool text_event_connected;
  
  		static NoteUrlWatcher ()
  		{
  			regex = new Regex (URL_REGEX,
  			                   RegexOptions.IgnoreCase | RegexOptions.Compiled);
  			text_event_connected = false;
  		}
  
  		public override void Initialize ()
  		{
  			// Do nothing
  		}
  
  		public override void Shutdown ()
  		{
  			// Do nothing.
  		}
  
  		public override void OnNoteOpened ()
  		{
  			// NOTE: This hack helps avoid multiple URL opens
  			// now that Notes always perform
  			// TagTable sharing.  This is because if the TagTable is
  			// shared, we will connect to the same Tag's event
  			// source each time a note is opened, and get called
  			// multiple times for each button press.  Fixes bug
  			// #305813.
  			if (!text_event_connected) {
  				Note.TagTable.UrlTag.Activated += OnUrlTagActivated;
  				text_event_connected = true;
  			}
  
  			click_mark = Buffer.CreateMark (null, Buffer.StartIter, true);
  
  			Buffer.InsertText += OnInsertText;
  			Buffer.DeleteRange += OnDeleteRange;
  
  			Window.Editor.ButtonPressEvent += OnButtonPress;
  			Window.Editor.PopulatePopup += OnPopulatePopup;
  			Window.Editor.PopupMenu += OnPopupMenu;
  		}
  
  		string GetUrl (Gtk.TextIter start, Gtk.TextIter end)
  		{
  			string url = start.GetSlice (end);
  
  			// FIXME: Needed because the file match is greedy and
  			// eats a leading space.
  			url = url.Trim ();
  
  			// Simple url massaging.  Add to 'http://' to the front
  			// of www.foo.com, 'mailto:' to alex@foo.com, 'file://'
  			// to /home/alex/foo.
  			if (url.StartsWith ("www."))
  				url = "http://" + url;
  			else if (url.StartsWith ("/") &&
  			                url.LastIndexOf ("/") > 1)
  				url = "file://" + url;
  			else if (url.StartsWith ("~/"))
  				url = "file://" +
  				      Path.Combine (Environment.GetEnvironmentVariable ("HOME"),
  				                    url.Substring (2));
  			else if (Regex.IsMatch (url, 
  				@"^(?!(news|mailto|http|https|ftp|file|irc):).+@.{2,}$",
  				RegexOptions.IgnoreCase))
  				url = "mailto:" + url;
  
  			return url;
  		}
  
  		void OpenUrl (string url)
  		{
  			if (url != string.Empty) {
  				Logger.Debug ("Opening url '{0}'...", url);
  				Services.NativeApplication.OpenUrl (url, Note.Window.Screen);
  			}
  		}
  
  		bool OnUrlTagActivated (NoteTag      sender,
  		                        NoteEditor   editor,
  		                        Gtk.TextIter start,
  		                        Gtk.TextIter end)
  		{
  			string url = GetUrl (start, end);
  			try {
  				OpenUrl (url);
  			} catch (GLib.GException e) {
  				GuiUtils.ShowOpeningLocationError (Window, url, e.Message);
  			}
  
  			// Kill the middle button paste...
  			return true;
  		}
  
  		void ApplyUrlToBlock (Gtk.TextIter start, Gtk.TextIter end)
  		{
  			NoteBuffer.GetBlockExtents (ref start,
  			                            ref end,
  			                            256 /* max url length */,
  			                            Note.TagTable.UrlTag);
  
  			Buffer.RemoveTag (Note.TagTable.UrlTag, start, end);
  
  			for (Match match = regex.Match (start.GetSlice (end));
  			                match.Success;
  			                match = match.NextMatch ()) {
  				System.Text.RegularExpressions.Group group = match.Groups [1];
  
  				/*
  				Logger.Log ("Highlighting url: '{0}' at offset {1}",
  				     group,
  				     group.Index);
  				*/
  
  				Gtk.TextIter start_cpy = start;
  				start_cpy.ForwardChars (group.Index);
  
  				end = start_cpy;
  				end.ForwardChars (group.Length);
  
  				Buffer.ApplyTag (Note.TagTable.UrlTag, start_cpy, end);
  			}
  		}
  
  		void OnDeleteRange (object sender, Gtk.DeleteRangeArgs args)
  		{
  			ApplyUrlToBlock (args.Start, args.End);
  		}
  
  		void OnInsertText (object sender, Gtk.InsertTextArgs args)
  		{
  			Gtk.TextIter start = args.Pos;
  			start.BackwardChars (args.NewTextLength);
  
  			ApplyUrlToBlock (start, args.Pos);
  		}
  
  		[GLib.ConnectBefore]
  		void OnButtonPress (object sender, Gtk.ButtonPressEventArgs args)
  		{
  			int x, y;
  
  			Window.Editor.WindowToBufferCoords (Gtk.TextWindowType.Text,
  			                                    (int) args.Event.X,
  			                                    (int) args.Event.Y,
  			                                    out x,
  			                                    out y);
  			Gtk.TextIter click_iter = Window.Editor.GetIterAtLocation (x, y);
  
  			// Move click_mark to click location
  			Buffer.MoveMark (click_mark, click_iter);
  
  			// Continue event processing
  			args.RetVal = false;
  		}
  
  		void OnPopulatePopup (object sender, Gtk.PopulatePopupArgs args)
  		{
  			Gtk.TextIter click_iter = Buffer.GetIterAtMark (click_mark);
  			NoteTag url_tag = Note.TagTable.UrlTag;
  			if (click_iter.HasTag (url_tag) || click_iter.EndsTag (url_tag)) {
  				Gtk.MenuItem item;
  
  				item = new Gtk.SeparatorMenuItem ();
  				item.Show ();
  				args.Menu.Prepend (item);
  
  				item = new Gtk.MenuItem (Catalog.GetString ("_Copy Link Address"));
  				item.Activated += CopyLinkActivate;
  				item.Show ();
  				args.Menu.Prepend (item);
  
  				item = new Gtk.MenuItem (Catalog.GetString ("_Open Link"));
  				item.Activated += OpenLinkActivate;
  				item.Show ();
  				args.Menu.Prepend (item);
  			}
  		}
  
  		// Called via Alt-F10.  Reset click_mark to cursor location.
  		[GLib.ConnectBefore]
  		void OnPopupMenu (object sender, Gtk.PopupMenuArgs args)
  		{
  			Gtk.TextIter click_iter = Buffer.GetIterAtMark (Buffer.InsertMark);
  			Buffer.MoveMark (click_mark, click_iter);
  			args.RetVal = false; // Continue event processing
  		}
  
  		void OpenLinkActivate (object sender, EventArgs args)
  		{
  			Gtk.TextIter click_iter = Buffer.GetIterAtMark (click_mark);
  
  			Gtk.TextIter start, end;
  			NoteTag url_tag = Note.TagTable.UrlTag;
  			url_tag.GetExtents (click_iter, out start, out end);
  
  			OnUrlTagActivated (url_tag, (NoteEditor) Window.Editor, start, end);
  		}
  
  		void CopyLinkActivate (object sender, EventArgs args)
  		{
  			Gtk.TextIter click_iter = Buffer.GetIterAtMark (click_mark);
  
  			Gtk.TextIter start, end;
  			Note.TagTable.UrlTag.GetExtents (click_iter, out start, out end);
  
  			string url = GetUrl (start, end);
  
  			Gtk.Clipboard clip = Window.Editor.GetClipboard (Gdk.Selection.Clipboard);
  			clip.Text = url;
  		}
  	}
  
  	public class NoteLinkWatcher : NoteAddin
  	{
  		static bool text_event_connected;
  
  		public override void Initialize ()
  		{
  			Manager.NoteDeleted += OnNoteDeleted;
  			Manager.NoteAdded += OnNoteAdded;
  			Manager.NoteRenamed += OnNoteRenamed;
  		}
  
  		public override void Shutdown ()
  		{
  			Manager.NoteDeleted -= OnNoteDeleted;
  			Manager.NoteAdded -= OnNoteAdded;
  			Manager.NoteRenamed -= OnNoteRenamed;
  		}
  
  		public override void OnNoteOpened ()
  		{
  			// NOTE: This avoids multiple link opens
  			// now that notes always perform TagTable
  			// sharing.  This is because if the TagTable is shared,
  			// we will connect to the same Tag's event source each
  			// time a note is opened, and get called multiple times
  			// for each button press.  Fixes bug #305813.
  			if (!text_event_connected) {
  				Note.TagTable.LinkTag.Activated += OnLinkTagActivated;
  				Note.TagTable.BrokenLinkTag.Activated += OnLinkTagActivated;
  				text_event_connected = true;
  			}
  
  			Buffer.InsertText += OnInsertText;
  			Buffer.DeleteRange += OnDeleteRange;
  		}
  
  		bool ContainsText (string text)
  		{
  			string body = Note.TextContent.ToLower ();
  			string match = text.ToLower ();
  
  			return body.IndexOf (match) > -1;
  		}
  
  		void OnNoteAdded (object sender, Note added)
  		{
  			if (added == this.Note)
  				return;
  
  			if (!ContainsText (added.Title))
  				return;
  
  			// Highlight previously unlinked text
  			HighlightInBlock (Buffer.StartIter, Buffer.EndIter);
  		}
  
  		void OnNoteDeleted (object sender, Note deleted)
  		{
  			if (deleted == this.Note)
  				return;
  
  			if (!ContainsText (deleted.Title))
  				return;
  
  			string old_title_lower = deleted.Title.ToLower ();
  
  			// Turn all link:internal to link:broken for the deleted note.
  			NoteTag link_tag = Note.TagTable.LinkTag;
  			NoteTag broken_link_tag = Note.TagTable.BrokenLinkTag;
  			TextTagEnumerator enumerator = new TextTagEnumerator (Buffer, link_tag);
  			foreach (TextRange range in enumerator) {
  				if (range.Text.ToLower () != old_title_lower)
  					continue;
  
  				Buffer.RemoveTag (link_tag, range.Start, range.End);
  				Buffer.ApplyTag (broken_link_tag, range.Start, range.End);
  			}
  		}
  
  		void OnNoteRenamed (Note renamed, string old_title)
  		{
  			if (renamed == this.Note)
  				return;
  
  			// Highlight previously unlinked text
  			if (ContainsText (renamed.Title))
  				HighlightNoteInBlock (renamed, Buffer.StartIter, Buffer.EndIter);
  		}
  
  		void DoHighlight (TrieHit hit, Gtk.TextIter start, Gtk.TextIter end)
  		{
  			// Some of these checks should be replaced with fixes to
  			// TitleTrie.FindMatches, probably.
  			if (hit.Value == null) {
  				Logger.Debug ("DoHighlight: null pointer error for '{0}'." , hit.Key);
  				return;
  			}
  			
  			if (Manager.Find(hit.Key) == null) {
  				Logger.Debug ("DoHighlight: '{0}' links to non-existing note." , hit.Key);
  				return;
  			}
  			
  			Note hit_note = (Note) hit.Value;
  
  			if (String.Compare (hit.Key.ToString(), hit_note.Title.ToString(), true ) != 0) { // == 0 if same string  
  				Logger.Debug ("DoHighlight: '{0}' links wrongly to note '{1}'." , hit.Key, hit_note.Title);
  				return;
  			}
  			
  			if (hit_note == this.Note)
  				return;
  
  			Gtk.TextIter title_start = start;
  			title_start.ForwardChars (hit.Start);
  
  			Gtk.TextIter title_end = start;
  			title_end.ForwardChars (hit.End);
  
  			// Only link against whole words/phrases
  			if ((!title_start.StartsWord () && !title_start.StartsSentence ()) ||
  			                (!title_end.EndsWord() && !title_end.EndsSentence()))
  				return;
  
  			// Don't create links inside URLs
  			if (Note.TagTable.HasLinkTag (title_start))
  				return;
  
  			Logger.Debug ("Matching Note title '{0}' at {1}-{2}...",
  			            hit.Key,
  			            hit.Start,
  			            hit.End);
  
  			Buffer.RemoveTag (Note.TagTable.BrokenLinkTag, title_start, title_end);
  			Buffer.ApplyTag (Note.TagTable.LinkTag, title_start, title_end);
  		}
  
  		void HighlightNoteInBlock (Note find_note, Gtk.TextIter start, Gtk.TextIter end)
  		{
  			string buffer_text = start.GetText (end).ToLower();
  			string find_title_lower = find_note.Title.ToLower ();
  			int idx = 0;
  
  			while (true) {
  				idx = buffer_text.IndexOf (find_title_lower, idx);
  				if (idx < 0)
  					break;
  
  				TrieHit hit = new TrieHit (idx,
  				                           idx + find_title_lower.Length,
  				                           find_title_lower,
  				                           find_note);
  				DoHighlight (hit, start, end);
  
  				idx += find_title_lower.Length;
  			}
  		}
  
  		void HighlightInBlock (Gtk.TextIter start, Gtk.TextIter end)
  		{
  			IList<TrieHit> hits = Manager.TitleTrie.FindMatches (start.GetSlice (end));
  			foreach (TrieHit hit in hits) {
  				DoHighlight (hit, start, end);
  			}
  		}
  
  		void UnhighlightInBlock (Gtk.TextIter start, Gtk.TextIter end)
  		{
  			Buffer.RemoveTag (Note.TagTable.LinkTag, start, end);
  		}
  
  		void OnDeleteRange (object sender, Gtk.DeleteRangeArgs args)
  		{
  			Gtk.TextIter start = args.Start;
  			Gtk.TextIter end = args.End;
  
  			NoteBuffer.GetBlockExtents (ref start,
  			                            ref end,
  			                            Manager.TitleTrie.MaxLength,
  			                            Note.TagTable.LinkTag);
  
  			UnhighlightInBlock (start, end);
  			HighlightInBlock (start, end);
  		}
  
  		void OnInsertText (object sender, Gtk.InsertTextArgs args)
  		{
  			Gtk.TextIter start = args.Pos;
  			start.BackwardChars (args.NewTextLength);
  
  			Gtk.TextIter end = args.Pos;
  
  			NoteBuffer.GetBlockExtents (ref start,
  			                            ref end,
  			                            Manager.TitleTrie.MaxLength,
  			                            Note.TagTable.LinkTag);
  
  			UnhighlightInBlock (start, end);
  			HighlightInBlock (start, end);
  		}
  
  		bool OpenOrCreateLink (Gtk.TextIter start, Gtk.TextIter end)
  		{
  			string link_name = start.GetText (end);
  			Note link = Manager.Find (link_name);
  
  			if (link == null) {
  				Logger.Debug ("Creating note '{0}'...", link_name);
  				try {
  					link = Manager.Create (link_name);
  				} catch {
  				// Fail silently.
  			}
  		}
  
  		// FIXME: We used to also check here for (link != this.Note), but
  		// somehow this was causing problems receiving clicks for the
  		// wrong instance of a note (see bug #413234).  Since a
  		// link:internal tag is never applied around text that's the same
  		// as the current note's title, it's safe to omit this check and
  		// also works around the bug.
  		if (link != null) {
  				Logger.Debug ("Opening note '{0}' on click...", link_name);
  				link.Window.Present ();
  				return true;
  			}
  
  			return false;
  		}
  
  		bool OnLinkTagActivated (NoteTag      sender,
  		                         NoteEditor   editor,
  		                         Gtk.TextIter start,
  		                         Gtk.TextIter end)
  		{
  			return OpenOrCreateLink (start, end);
  		}
  	}
  
  	public class NoteWikiWatcher : NoteAddin
  	{
  		Gtk.TextTag broken_link_tag;
  
  		// NOTE: \p{Lu} is Unicode uppercase and \p{Ll} is lowercase.
  		const string WIKIWORD_REGEX = @"\b((\p{Lu}+[\p{Ll}0-9]+){2}([\p{Lu}\p{Ll}0-9])*)\b";
  
  		static Regex regex;
  
  		static NoteWikiWatcher ()
  		{
  			regex = new Regex (WIKIWORD_REGEX, RegexOptions.Compiled);
  		}
  
  		public override void Initialize ()
  		{
  			broken_link_tag = Note.TagTable.Lookup ("link:broken");
  		}
  
  		public override void Shutdown ()
  		{
  			// Do nothing.
  		}
  
  		public override void OnNoteOpened ()
  		{
  			if ((bool) Preferences.Get (Preferences.ENABLE_WIKIWORDS)) {
  				Buffer.InsertText += OnInsertText;
  				Buffer.DeleteRange += OnDeleteRange;
  			}
  			Preferences.SettingChanged += OnEnableWikiwordsChanged;
  		}
  
  		void OnEnableWikiwordsChanged (object sender, NotifyEventArgs args)
  		{
  			if (args.Key != Preferences.ENABLE_WIKIWORDS)
  				return;
  
  			if ((bool) args.Value) {
  				Buffer.InsertText += OnInsertText;
  				Buffer.DeleteRange += OnDeleteRange;
  			} else {
  				Buffer.InsertText -= OnInsertText;
  				Buffer.DeleteRange -= OnDeleteRange;
  			}
  		}
  
  		void ApplyWikiwordToBlock (Gtk.TextIter start, Gtk.TextIter end)
  		{
  			NoteBuffer.GetBlockExtents (ref start,
  			                            ref end,
  
gt: 			                            80 /* max wiki name */,