From 2c2ef65f89dff72609040da2910a8884ddf86905 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 22 Mar 2009 01:05:59 +0000 Subject: [PATCH] Improved GetNextCaretPosition - placed word borders at line starts and ends. Implemented GetWordBeforeCaret(). git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@3901 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61 --- .../Src/AvalonEditDocumentAdapter.cs | 5 + .../AvalonEdit.AddIn/Src/CodeEditor.cs | 5 +- .../Src/SharpDevelopCompletionWindow.cs | 5 +- .../ICSharpCode.AvalonEdit.Tests.csproj | 1 + .../Utils/CaretNavigationTests.cs | 91 ++++++++++++++ .../CodeCompletion/CompletionWindow.cs | 20 +-- .../Document/ITextSource.cs | 55 +++++++++ .../ICSharpCode.AvalonEdit/Gui/Caret.cs | 4 +- .../Gui/NewLineElementGenerator.cs | 6 +- .../ICSharpCode.AvalonEdit/Gui/VisualLine.cs | 34 ++++++ .../Gui/VisualLineElement.cs | 14 ++- .../Gui/VisualLineText.cs | 44 ++----- .../Gui/WhitespaceElementGenerator.cs | 10 +- .../Utils/TextUtilities.cs | 114 ++++++++++++++++++ .../Project/ICSharpCode.SharpDevelop.csproj | 15 ++- .../RefactoringService/TextEditorDocument.cs | 5 + .../Src/TextEditor/DocumentUtilitites.cs | 86 +++++++++++++ .../Gui/Editor/CodeCompletionBinding.cs | 5 +- .../Project/Src/TextEditor/ITextEditor.cs | 1 - .../Project/Src/Refactoring/IDocument.cs | 1 + 20 files changed, 458 insertions(+), 63 deletions(-) create mode 100644 src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Utils/CaretNavigationTests.cs create mode 100644 src/Main/Base/Project/Src/TextEditor/DocumentUtilitites.cs diff --git a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/AvalonEditDocumentAdapter.cs b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/AvalonEditDocumentAdapter.cs index 990120223e..8540b37e7c 100644 --- a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/AvalonEditDocumentAdapter.cs +++ b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/AvalonEditDocumentAdapter.cs @@ -71,6 +71,11 @@ namespace ICSharpCode.AvalonEdit.AddIn set { document.Text = value; } } + public event EventHandler TextChanged { + add { document.TextChanged += value; } + remove { document.TextChanged -= value; } + } + public IDocumentLine GetLine(int lineNumber) { return new LineAdapter(document.GetLineByNumber(lineNumber)); diff --git a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CodeEditor.cs b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CodeEditor.cs index 93aed1c6e9..25c9d281a7 100644 --- a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CodeEditor.cs +++ b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/CodeEditor.cs @@ -52,9 +52,8 @@ namespace ICSharpCode.AvalonEdit.AddIn if (result == CodeCompletionKeyPressResult.Completed) { if (lastCompletionWindow != null && lastCompletionWindow != oldCompletionWindow) { // a new CompletionWindow was shown, but does not eat the input - // increment the offsets so that they are correct after the text insertion - lastCompletionWindow.StartOffset++; - lastCompletionWindow.EndOffset++; + // tell it to expect the text insertion + lastCompletionWindow.ExpectInsertionBeforeStart = true; } return; } else if (result == CodeCompletionKeyPressResult.EatKey) { diff --git a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/SharpDevelopCompletionWindow.cs b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/SharpDevelopCompletionWindow.cs index 8d104919f8..9edf23dac9 100644 --- a/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/SharpDevelopCompletionWindow.cs +++ b/src/AddIns/DisplayBindings/AvalonEdit.AddIn/Src/SharpDevelopCompletionWindow.cs @@ -38,10 +38,7 @@ namespace ICSharpCode.AvalonEdit.AddIn foreach (char c in e.Text) { switch (itemList.ProcessInput(c)) { case CompletionItemListKeyResult.BeforeStartKey: - if (StartOffset == EndOffset) { - StartOffset++; - EndOffset++; - } + this.ExpectInsertionBeforeStart = true; break; case CompletionItemListKeyResult.NormalKey: break; diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.csproj b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.csproj index 0c73263bde..54fd01f4ba 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.csproj +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/ICSharpCode.AvalonEdit.Tests.csproj @@ -72,6 +72,7 @@ + diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Utils/CaretNavigationTests.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Utils/CaretNavigationTests.cs new file mode 100644 index 0000000000..f86f651032 --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit.Tests/Utils/CaretNavigationTests.cs @@ -0,0 +1,91 @@ +// +// +// +// +// $Revision$ +// + +using ICSharpCode.AvalonEdit.Gui; +using System; +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Utils; +using NUnit.Framework; + +namespace ICSharpCode.AvalonEdit.Tests.Utils +{ + [TestFixture] + public class CaretNavigationTests + { + int GetNextCaretStop(string text, int offset, CaretPositioningMode mode) + { + return TextUtilities.GetNextCaretPosition(new StringTextSource(text), offset, false, mode); + } + + int GetPrevCaretStop(string text, int offset, CaretPositioningMode mode) + { + return TextUtilities.GetNextCaretPosition(new StringTextSource(text), offset, true, mode); + } + + [Test] + public void CaretStopInEmptyString() + { + Assert.AreEqual(0, GetNextCaretStop("", -1, CaretPositioningMode.Normal)); + Assert.AreEqual(-1, GetNextCaretStop("", 0, CaretPositioningMode.Normal)); + Assert.AreEqual(-1, GetPrevCaretStop("", 0, CaretPositioningMode.Normal)); + Assert.AreEqual(0, GetPrevCaretStop("", 1, CaretPositioningMode.Normal)); + + Assert.AreEqual(-1, GetNextCaretStop("", -1, CaretPositioningMode.WordStart)); + Assert.AreEqual(-1, GetNextCaretStop("", -1, CaretPositioningMode.WordBorder)); + Assert.AreEqual(-1, GetPrevCaretStop("", 1, CaretPositioningMode.WordStart)); + Assert.AreEqual(-1, GetPrevCaretStop("", 1, CaretPositioningMode.WordBorder)); + } + + [Test] + public void StartOfDocumentWithWordStart() + { + Assert.AreEqual(0, GetNextCaretStop("word", -1, CaretPositioningMode.Normal)); + Assert.AreEqual(0, GetNextCaretStop("word", -1, CaretPositioningMode.WordStart)); + Assert.AreEqual(0, GetNextCaretStop("word", -1, CaretPositioningMode.WordBorder)); + + Assert.AreEqual(0, GetPrevCaretStop("word", 1, CaretPositioningMode.Normal)); + Assert.AreEqual(0, GetPrevCaretStop("word", 1, CaretPositioningMode.WordStart)); + Assert.AreEqual(0, GetPrevCaretStop("word", 1, CaretPositioningMode.WordBorder)); + } + + [Test] + public void StartOfDocumentNoWordStart() + { + Assert.AreEqual(0, GetNextCaretStop(" word", -1, CaretPositioningMode.Normal)); + Assert.AreEqual(1, GetNextCaretStop(" word", -1, CaretPositioningMode.WordStart)); + Assert.AreEqual(1, GetNextCaretStop(" word", -1, CaretPositioningMode.WordBorder)); + + Assert.AreEqual(0, GetPrevCaretStop(" word", 1, CaretPositioningMode.Normal)); + Assert.AreEqual(-1, GetPrevCaretStop(" word", 1, CaretPositioningMode.WordStart)); + Assert.AreEqual(-1, GetPrevCaretStop(" word", 1, CaretPositioningMode.WordBorder)); + } + + [Test] + public void EndOfDocumentWordBorder() + { + Assert.AreEqual(4, GetNextCaretStop("word", 3, CaretPositioningMode.Normal)); + Assert.AreEqual(-1, GetNextCaretStop("word", 3, CaretPositioningMode.WordStart)); + Assert.AreEqual(4, GetNextCaretStop("word", 3, CaretPositioningMode.WordBorder)); + + Assert.AreEqual(4, GetPrevCaretStop("word", 5, CaretPositioningMode.Normal)); + Assert.AreEqual(0, GetPrevCaretStop("word", 5, CaretPositioningMode.WordStart)); + Assert.AreEqual(4, GetPrevCaretStop("word", 5, CaretPositioningMode.WordBorder)); + } + + [Test] + public void EndOfDocumentNoWordBorder() + { + Assert.AreEqual(4, GetNextCaretStop("txt ", 3, CaretPositioningMode.Normal)); + Assert.AreEqual(-1, GetNextCaretStop("txt ", 3, CaretPositioningMode.WordStart)); + Assert.AreEqual(-1, GetNextCaretStop("txt ", 3, CaretPositioningMode.WordBorder)); + + Assert.AreEqual(4, GetPrevCaretStop("txt ", 5, CaretPositioningMode.Normal)); + Assert.AreEqual(0, GetPrevCaretStop("txt ", 5, CaretPositioningMode.WordStart)); + Assert.AreEqual(3, GetPrevCaretStop("txt ", 5, CaretPositioningMode.WordBorder)); + } + } +} diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/CodeCompletion/CompletionWindow.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/CodeCompletion/CompletionWindow.cs index 5fd5b2bfc9..3621775b38 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/CodeCompletion/CompletionWindow.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/CodeCompletion/CompletionWindow.cs @@ -163,19 +163,21 @@ namespace ICSharpCode.AvalonEdit.CodeCompletion return completionList.ScrollViewer ?? completionList.ListBox ?? (UIElement)completionList; } + /// + /// Gets/sets whether the completion window should expect text insertion at the start offset, + /// which not go into the completion region, but before it. + /// + public bool ExpectInsertionBeforeStart { get; set; } + void textArea_Document_Changing(object sender, DocumentChangeEventArgs e) { - // => startOffset test required so that this startOffset/endOffset are not incremented again - // for BeforeStartKey characters - if (e.Offset >= startOffset && e.Offset <= endOffset) { - endOffset += e.InsertionLength - e.RemovalLength; - } else if (e.Offset == startOffset - 1 && e.InsertionLength == 1 && e.RemovalLength == 0) { - // allow one-character insertion in front of StartOffset. - // this is necessary because for dot-completion, the CompletionWindow is shown before - // the dot is actually inserted. + if (e.Offset == startOffset && e.RemovalLength == 0 && ExpectInsertionBeforeStart) { + startOffset = e.GetNewOffset(startOffset, AnchorMovementType.AfterInsertion); + this.ExpectInsertionBeforeStart = false; } else { - Close(); + startOffset = e.GetNewOffset(startOffset, AnchorMovementType.BeforeInsertion); } + endOffset = e.GetNewOffset(endOffset, AnchorMovementType.AfterInsertion); } /// diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/ITextSource.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/ITextSource.cs index f348a7a6a9..2c1f8cf6a8 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/ITextSource.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Document/ITextSource.cs @@ -54,6 +54,61 @@ namespace ICSharpCode.AvalonEdit.Document string GetText(int offset, int length); } + /// + /// Implements the ITextSource interface by wrapping another TextSource + /// and viewing only a part of the text. + /// + public sealed class TextSourceView : ITextSource + { + readonly ITextSource baseTextSource; + readonly ISegment viewedSegment; + + /// + /// Creates a new TextSourceView object. + /// + /// The base text source. + /// A text segment from the base text source + public TextSourceView(ITextSource baseTextSource, ISegment viewedSegment) + { + if (baseTextSource == null) + throw new ArgumentNullException("baseTextSource"); + if (viewedSegment == null) + throw new ArgumentNullException("viewedSegment"); + this.baseTextSource = baseTextSource; + this.viewedSegment = viewedSegment; + } + + /// + public event EventHandler TextChanged { + add { baseTextSource.TextChanged += value; } + remove { baseTextSource.TextChanged -= value; } + } + + /// + public string Text { + get { + return baseTextSource.GetText(viewedSegment.Offset, viewedSegment.Length); + } + } + + /// + public int TextLength { + get { return viewedSegment.Length; } + } + + /// + public char GetCharAt(int offset) + { + return baseTextSource.GetCharAt(viewedSegment.Offset + offset); + } + + /// + public string GetText(int offset, int length) + { + return baseTextSource.GetText(viewedSegment.Offset + offset, length); + } + } + /// /// Implements the ITextSource interface using a string. /// diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/Caret.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/Caret.cs index 8ca3fe0cc1..7c48089200 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/Caret.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/Caret.cs @@ -242,7 +242,9 @@ namespace ICSharpCode.AvalonEdit.Gui Rect CalcCaretRectangle(VisualLine visualLine) { - RevalidateVisualColumn(visualLine); + if (!visualColumnValid) { + RevalidateVisualColumn(visualLine); + } TextLine textLine = visualLine.GetTextLine(position.VisualColumn); double xPos = textLine.GetDistanceFromCharacterHit(new CharacterHit(position.VisualColumn, 0)); diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/NewLineElementGenerator.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/NewLineElementGenerator.cs index de34962a90..4b80e271b1 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/NewLineElementGenerator.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/NewLineElementGenerator.cs @@ -57,7 +57,7 @@ namespace ICSharpCode.AvalonEdit.Gui return new NewLineTextElement(text); } - class NewLineTextElement : FormattedTextElement + sealed class NewLineTextElement : FormattedTextElement { public NewLineTextElement(FormattedText text) : base(text, 0) { @@ -76,6 +76,10 @@ namespace ICSharpCode.AvalonEdit.Gui return -1; } } + + public override bool HandlesLineBorders { + get { return true; } + } } } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLine.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLine.cs index 7b5e706583..6f2cecd077 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLine.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLine.cs @@ -317,12 +317,31 @@ namespace ICSharpCode.AvalonEdit.Gui /// public int GetNextCaretPosition(int visualColumn, bool backwards, CaretPositioningMode mode) { + if (elements.Count == 0) { + // special handling for empty visual lines: + // even though we don't have any elements, + // there's a single caret stop at visualColumn 0 + if (visualColumn < 0 && !backwards) + return 0; + else if (visualColumn > 0 && backwards) + return 0; + else + return -1; + } + int i; if (backwards) { + // Search Backwards: + // If the last element doesn't handle line borders, return the line end as caret stop + if (visualColumn > this.VisualLength && !elements[elements.Count-1].HandlesLineBorders) { + return this.VisualLength; + } + // skip elements that start after or at visualColumn for (i = elements.Count - 1; i >= 0; i--) { if (elements[i].VisualColumn < visualColumn) break; } + // search last element that has a caret stop for (; i >= 0; i--) { int pos = elements[i].GetNextCaretPosition( Math.Min(visualColumn, elements[i].VisualColumn + elements[i].VisualLength + 1), @@ -330,11 +349,21 @@ namespace ICSharpCode.AvalonEdit.Gui if (pos >= 0) return pos; } + // if we've found nothing, and the first element doesn't handle line borders, + // return the line start as caret stop + if (visualColumn > 0 && !elements[0].HandlesLineBorders) + return 0; } else { + // Search Forwards: + // If the first element doesn't handle line borders, return the line start as caret stop + if (visualColumn < 0 && !elements[0].HandlesLineBorders) + return 0; + // skip elements that end before or at visualColumn for (i = 0; i < elements.Count; i++) { if (elements[i].VisualColumn + elements[i].VisualLength > visualColumn) break; } + // search first element that has a caret stop for (; i < elements.Count; i++) { int pos = elements[i].GetNextCaretPosition( Math.Max(visualColumn, elements[i].VisualColumn - 1), @@ -342,7 +371,12 @@ namespace ICSharpCode.AvalonEdit.Gui if (pos >= 0) return pos; } + // if we've found nothing, and the last element doesn't handle line borders, + // return the line end as caret stop + if (visualColumn < this.VisualLength && !elements[elements.Count-1].HandlesLineBorders) + return this.VisualLength; } + // we've found nothing, return -1 and let the caret search continue in the next line return -1; } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineElement.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineElement.cs index 932f677688..fc1d158a1b 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineElement.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineElement.cs @@ -171,7 +171,8 @@ namespace ICSharpCode.AvalonEdit.Gui /// Whether to stop only at word borders. /// The visual column of the next caret position, or -1 if there is no next caret position. /// - /// In the space between two line elements, usually both of them contain a caret position. + /// In the space between two line elements, it is sufficient that one of them contains a caret position; + /// though in many cases, both of them contain one. /// public virtual int GetNextCaretPosition(int visualColumn, bool backwards, CaretPositioningMode mode) { @@ -191,6 +192,17 @@ namespace ICSharpCode.AvalonEdit.Gui return -1; } + /// + /// Gets whether the implementation handles line borders. + /// If this property returns false, the caller of GetNextCaretPosition should handle the line + /// borders (i.e. place caret stops at the start and end of the line). + /// This property has an effect only for VisualLineElements that are at the start or end of a + /// . + /// + public virtual bool HandlesLineBorders { + get { return false; } + } + /// /// Queries the cursor over the visual line element. /// diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineText.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineText.cs index 04d6e5ec7e..6b5408398e 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineText.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/VisualLineText.cs @@ -5,10 +5,10 @@ // $Revision$ // +using ICSharpCode.AvalonEdit.Utils; using System; using System.Collections.Generic; using System.Windows.Media.TextFormatting; - using ICSharpCode.AvalonEdit.Document; namespace ICSharpCode.AvalonEdit.Gui @@ -94,41 +94,15 @@ namespace ICSharpCode.AvalonEdit.Gui /// public override int GetNextCaretPosition(int visualColumn, bool backwards, CaretPositioningMode mode) { - int nextPos = backwards ? visualColumn - 1 : visualColumn + 1; - if (nextPos >= this.VisualColumn && nextPos <= this.VisualColumn + this.VisualLength) { - if (mode == CaretPositioningMode.WordBorder || mode == CaretPositioningMode.WordStart) { - TextDocument document = parentVisualLine.FirstDocumentLine.Document; - int textOffset = parentVisualLine.FirstDocumentLine.Offset + GetRelativeOffset(nextPos); - if (textOffset > 0 && textOffset < document.TextLength) { - CharClass charBefore = GetCharClass(document.GetCharAt(textOffset - 1)); - CharClass charAfter = GetCharClass(document.GetCharAt(textOffset)); - if (charBefore == charAfter || (charAfter == CharClass.Whitespace && mode == CaretPositioningMode.WordStart)) - return GetNextCaretPosition(nextPos, backwards, mode); - } - } - return nextPos; - } - return -1; - } - - enum CharClass - { - Whitespace, - IdentifierPart, - LineTerminator, - Other - } - - static CharClass GetCharClass(char c) - { - if (c == '\r' || c == '\n') - return CharClass.LineTerminator; - else if (char.IsWhiteSpace(c)) - return CharClass.Whitespace; - else if (char.IsLetterOrDigit(c) || c == '_') - return CharClass.IdentifierPart; + int textOffset = parentVisualLine.FirstDocumentLine.Offset + this.RelativeTextOffset; + TextSourceView view = new TextSourceView( + parentVisualLine.FirstDocumentLine.Document, + new SimpleSegment(textOffset, this.DocumentLength)); + int pos = TextUtilities.GetNextCaretPosition(view, visualColumn - this.VisualColumn, backwards, mode); + if (pos < 0) + return pos; else - return CharClass.Other; + return this.VisualColumn + pos; } } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/WhitespaceElementGenerator.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/WhitespaceElementGenerator.cs index 435ca00d5c..17cf46257f 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/WhitespaceElementGenerator.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Gui/WhitespaceElementGenerator.cs @@ -93,7 +93,7 @@ namespace ICSharpCode.AvalonEdit.Gui } } - class SpaceTextElement : FormattedTextElement + sealed class SpaceTextElement : FormattedTextElement { public SpaceTextElement(FormattedText text) : base(text, 1) { @@ -110,7 +110,7 @@ namespace ICSharpCode.AvalonEdit.Gui } } - class TabTextElement : VisualLineElement + sealed class TabTextElement : VisualLineElement { internal readonly FormattedText text; @@ -121,6 +121,8 @@ namespace ICSharpCode.AvalonEdit.Gui public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context) { + // the TabTextElement consists of two TextRuns: + // first a TabGlyphRun, then TextCharacters '\t' to let WPF handle the tab indentation if (startVisualColumn == this.VisualColumn) return new TabGlyphRun(this, this.TextRunProperties); else if (startVisualColumn == this.VisualColumn + 1) @@ -138,9 +140,9 @@ namespace ICSharpCode.AvalonEdit.Gui } } - class TabGlyphRun : TextEmbeddedObject + sealed class TabGlyphRun : TextEmbeddedObject { - protected readonly TabTextElement element; + readonly TabTextElement element; TextRunProperties properties; public TabGlyphRun(TabTextElement element, TextRunProperties properties) diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/TextUtilities.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/TextUtilities.cs index 4bee7a328b..007d66ac6d 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/TextUtilities.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Utils/TextUtilities.cs @@ -6,6 +6,9 @@ // using System; +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Gui; +using System.ComponentModel; namespace ICSharpCode.AvalonEdit.Utils { @@ -14,5 +17,116 @@ namespace ICSharpCode.AvalonEdit.Utils /// public static class TextUtilities { + /// + /// Gets whether the character is whitespace, part of an identifier, or line terminator. + /// + public static CharacterClass GetCharacterClass(char c) + { + if (c == '\r' || c == '\n') + return CharacterClass.LineTerminator; + else if (char.IsWhiteSpace(c)) + return CharacterClass.Whitespace; + else if (char.IsLetterOrDigit(c) || c == '_') + return CharacterClass.IdentifierPart; + else + return CharacterClass.Other; + } + + /// + /// Gets the next caret position. + /// + /// The text source. + /// The start offset inside the text source. + /// True to look backwards, false to look forwards. + /// The mode for caret positioning. + /// The offset of the next caret position, or -1 if there is no further caret position + /// in the text source. + public static int GetNextCaretPosition(ITextSource textSource, int offset, bool backwards, CaretPositioningMode mode) + { + if (textSource == null) + throw new ArgumentNullException("textSource"); + if (mode != CaretPositioningMode.Normal + && mode != CaretPositioningMode.WordBorder + && mode != CaretPositioningMode.WordStart) + { + throw new ArgumentException("Unsupported CaretPositioningMode: " + mode, "mode"); + } + int textLength = textSource.TextLength; + if (textLength <= 0) { + // empty document? has a normal caret position at 0, though no word borders + if (mode == CaretPositioningMode.Normal) { + if (offset > 0 && backwards) return 0; + if (offset < 0 && !backwards) return 0; + } + return -1; + } + while (true) { + int nextPos = backwards ? offset - 1 : offset + 1; + + // return -1 if there is no further caret position in the text source + // we also need this to handle offset values outside the valid range + if (nextPos < 0 || nextPos > textLength) + return -1; + + // stop at every caret position? we can stop immediately. + if (mode == CaretPositioningMode.Normal) + return nextPos; + // not normal mode? we're looking for word borders... + + // check if we've run against the textSource borders. + // a 'textSource' usually isn't the whole document, but a single VisualLineElement. + if (nextPos == 0) { + // at the document start, there's only a word border + // if the first character is not whitespace + if (!char.IsWhiteSpace(textSource.GetCharAt(0))) + return nextPos; + } else if (nextPos == textLength) { + // at the document end, there's never a word start + if (mode != CaretPositioningMode.WordStart) { + // at the document end, there's only a word border + // if the last character is not whitespace + if (!char.IsWhiteSpace(textSource.GetCharAt(textLength - 1))) + return nextPos; + } + } else { + CharacterClass charBefore = GetCharacterClass(textSource.GetCharAt(nextPos - 1)); + CharacterClass charAfter = GetCharacterClass(textSource.GetCharAt(nextPos)); + if (charBefore != charAfter) { + // this looks like a possible border + + // if we're looking for word starts, check that this is a word start (and not a word end) + // if we're just checking for word borders, accept unconditionally + if (!(mode == CaretPositioningMode.WordStart && (charAfter == CharacterClass.Whitespace || charAfter == CharacterClass.LineTerminator))) { + return nextPos; + } + } + } + // we'll have to continue searching... + offset = nextPos; + } + } + } + + /// + /// Classifies a character as whitespace, line terminator, part of an identifier, or other. + /// + public enum CharacterClass + { + /// + /// The character is not whitespace, line terminator or part of an identifier. + /// + Other, + /// + /// The character is whitespace (but not line terminator). + /// + Whitespace, + /// + /// The character can be part of an identifier (LetterOrDigit or underscore). + /// + IdentifierPart, + /// + /// The character is line terminator (\r or \n). + /// + LineTerminator } } diff --git a/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj b/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj index b01753ae4f..c549c1eab4 100644 --- a/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj +++ b/src/Main/Base/Project/ICSharpCode.SharpDevelop.csproj @@ -512,6 +512,7 @@ + Form @@ -750,9 +751,15 @@ + + {6C55B776-26D4-4DB3-A6AB-87E783B2F3D1} + ICSharpCode.AvalonEdit + True + {2D18BE89-D210-49EB-A9DD-2246FBB3DF6D} ICSharpCode.TextEditor + False {3A9AE6AA-BC07-4A2F-972C-581E3AE2F195} @@ -761,6 +768,7 @@ {35cef10f-2d4c-45f2-9dd1-161e0fec583c} ICSharpCode.Core + False @@ -774,25 +782,28 @@ {7E4A7172-7FF5-48D0-B719-7CD959DD1AC9} ICSharpCode.Core.Presentation - True + False {857CA1A3-FC88-4BE0-AB6A-D1EE772AB288} ICSharpCode.Core.WinForms + False {C3CBC8E3-81D8-4C5B-9941-DCCD12D50B1F} ICSharpCode.SharpDevelop.BuildWorker + False {924EE450-603D-49C1-A8E5-4AFAA31CE6F3} ICSharpCode.SharpDevelop.Dom + False {8035765F-D51F-4A0C-A746-2FD100E19419} ICSharpCode.SharpDevelop.Widgets - True + False diff --git a/src/Main/Base/Project/Src/Services/RefactoringService/TextEditorDocument.cs b/src/Main/Base/Project/Src/Services/RefactoringService/TextEditorDocument.cs index d2bd675168..2e4ddda343 100644 --- a/src/Main/Base/Project/Src/Services/RefactoringService/TextEditorDocument.cs +++ b/src/Main/Base/Project/Src/Services/RefactoringService/TextEditorDocument.cs @@ -72,6 +72,11 @@ namespace ICSharpCode.SharpDevelop.Refactoring set { doc.TextContent = value; } } + public event EventHandler TextChanged { + add { doc.TextContentChanged += value; } + remove { doc.TextContentChanged -= value; } + } + public string GetText(int offset, int length) { return doc.GetText(offset, length); diff --git a/src/Main/Base/Project/Src/TextEditor/DocumentUtilitites.cs b/src/Main/Base/Project/Src/TextEditor/DocumentUtilitites.cs new file mode 100644 index 0000000000..db11199d79 --- /dev/null +++ b/src/Main/Base/Project/Src/TextEditor/DocumentUtilitites.cs @@ -0,0 +1,86 @@ +// +// +// +// +// $Revision$ +// + +using ICSharpCode.AvalonEdit.Gui; +using System; +using ICSharpCode.AvalonEdit.Utils; +using ICSharpCode.SharpDevelop.Dom.Refactoring; + +namespace ICSharpCode.SharpDevelop +{ + /// + /// Extension methods for ITextEditor and IDocument. + /// + public static class DocumentUtilitites + { + /// + /// Gets the word in front of the caret. + /// + public static string GetWordBeforeCaret(this ITextEditor editor) + { + if (editor == null) + throw new ArgumentNullException("editor"); + int endOffset = editor.Caret.Offset; + int startOffset = FindPrevWordStart(editor.Document, endOffset); + if (startOffset < 0) + return string.Empty; + else + return editor.Document.GetText(startOffset, endOffset - startOffset); + } + + /// + /// Finds the first word start in the document before offset. + /// + /// The offset of the word start, or -1 if there is no word start before the specified offset. + public static int FindPrevWordStart(IDocument document, int offset) + { + return TextUtilities.GetNextCaretPosition(GetTextSource(document), offset, true, CaretPositioningMode.WordStart); + } + + #region ITextSource implementation + public static ICSharpCode.AvalonEdit.Document.ITextSource GetTextSource(IDocument document) + { + if (document == null) + throw new ArgumentNullException("document"); + return new DocumentTextSource(document); + } + + sealed class DocumentTextSource : ICSharpCode.AvalonEdit.Document.ITextSource + { + readonly IDocument document; + + public DocumentTextSource(IDocument document) + { + this.document = document; + } + + public event EventHandler TextChanged { + add { document.TextChanged += value; } + remove { document.TextChanged -= value; } + } + + public string Text { + get { return document.Text; } + } + + public int TextLength { + get { return document.TextLength; } + } + + public char GetCharAt(int offset) + { + return document.GetCharAt(offset); + } + + public string GetText(int offset, int length) + { + return document.GetText(offset, length); + } + } + #endregion + } +} diff --git a/src/Main/Base/Project/Src/TextEditor/Gui/Editor/CodeCompletionBinding.cs b/src/Main/Base/Project/Src/TextEditor/Gui/Editor/CodeCompletionBinding.cs index 63d86fe116..b76ec9d94a 100644 --- a/src/Main/Base/Project/Src/TextEditor/Gui/Editor/CodeCompletionBinding.cs +++ b/src/Main/Base/Project/Src/TextEditor/Gui/Editor/CodeCompletionBinding.cs @@ -189,8 +189,9 @@ namespace ICSharpCode.SharpDevelop.DefaultEditor.Gui.Editor case ' ': if (CodeCompletionOptions.KeywordCompletionEnabled) { string word = editor.GetWordBeforeCaret(); - if (word != null) { - return HandleKeyword(editor, word) ? CodeCompletionKeyPressResult.Completed : CodeCompletionKeyPressResult.None; + if (!string.IsNullOrEmpty(word)) { + if (HandleKeyword(editor, word)) + return CodeCompletionKeyPressResult.Completed; } } break; diff --git a/src/Main/Base/Project/Src/TextEditor/ITextEditor.cs b/src/Main/Base/Project/Src/TextEditor/ITextEditor.cs index 28813af114..8e6ebfbf3f 100644 --- a/src/Main/Base/Project/Src/TextEditor/ITextEditor.cs +++ b/src/Main/Base/Project/Src/TextEditor/ITextEditor.cs @@ -43,7 +43,6 @@ namespace ICSharpCode.SharpDevelop [Obsolete] void ShowCompletionWindow(ICSharpCode.TextEditor.Gui.CompletionWindow.ICompletionDataProvider provider, char ch); void ShowCompletionWindow(ICompletionItemList data); - string GetWordBeforeCaret(); } public interface ITextEditorCaret diff --git a/src/Main/ICSharpCode.SharpDevelop.Dom/Project/Src/Refactoring/IDocument.cs b/src/Main/ICSharpCode.SharpDevelop.Dom/Project/Src/Refactoring/IDocument.cs index 2429b42039..40fd2b2ac0 100644 --- a/src/Main/ICSharpCode.SharpDevelop.Dom/Project/Src/Refactoring/IDocument.cs +++ b/src/Main/ICSharpCode.SharpDevelop.Dom/Project/Src/Refactoring/IDocument.cs @@ -20,6 +20,7 @@ namespace ICSharpCode.SharpDevelop.Dom.Refactoring int TextLength { get; } int TotalNumberOfLines { get; } string Text { get; set; } + event EventHandler TextChanged; /// /// Gets the document line with the specified number.