521 lines
14 KiB
C#
521 lines
14 KiB
C#
|
using System;
|
|||
|
using System.Collections.Generic;
|
|||
|
using System.ComponentModel;
|
|||
|
using System.Data;
|
|||
|
using System.Drawing;
|
|||
|
using System.Text;
|
|||
|
using System.Windows.Forms;
|
|||
|
using ICSharpCode.TextEditor.Document;
|
|||
|
using ICSharpCode.TextEditor;
|
|||
|
using System.Diagnostics;
|
|||
|
using System.IO;
|
|||
|
|
|||
|
namespace XmlPad
|
|||
|
{
|
|||
|
public partial class FindAndReplaceForm : Form
|
|||
|
{
|
|||
|
public FindAndReplaceForm()
|
|||
|
{
|
|||
|
InitializeComponent();
|
|||
|
_search = new TextEditorSearcher();
|
|||
|
}
|
|||
|
|
|||
|
TextEditorSearcher _search;
|
|||
|
TextEditorControl _editor;
|
|||
|
TextEditorControl Editor
|
|||
|
{
|
|||
|
get { return _editor; }
|
|||
|
set
|
|||
|
{
|
|||
|
_editor = value;
|
|||
|
_search.Document = _editor.Document;
|
|||
|
UpdateTitleBar();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private void UpdateTitleBar()
|
|||
|
{
|
|||
|
string text = ReplaceMode ? "查找和替换" : "查找";
|
|||
|
if (_editor != null && _editor.FileName != null)
|
|||
|
text += " - " + Path.GetFileName(_editor.FileName);
|
|||
|
if (_search.HasScanRegion)
|
|||
|
text += " (只可选)";
|
|||
|
this.Text = text;
|
|||
|
}
|
|||
|
|
|||
|
public void ShowFor(TextEditorControl editor, bool replaceMode)
|
|||
|
{
|
|||
|
Editor = editor;
|
|||
|
|
|||
|
_search.ClearScanRegion();
|
|||
|
SelectionManager sm = editor.ActiveTextAreaControl.SelectionManager;
|
|||
|
if (sm.HasSomethingSelected && sm.SelectionCollection.Count == 1)
|
|||
|
{
|
|||
|
ISelection sel = sm.SelectionCollection[0];
|
|||
|
if (sel.StartPosition.Y == sel.EndPosition.Y)
|
|||
|
txtLookFor.Text = sm.SelectedText;
|
|||
|
else
|
|||
|
_search.SetScanRegion(sel);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
// Get the current word that the caret is on
|
|||
|
Caret caret = editor.ActiveTextAreaControl.Caret;
|
|||
|
int start = TextUtilities.FindWordStart(editor.Document, caret.Offset);
|
|||
|
int endAt = TextUtilities.FindWordEnd(editor.Document, caret.Offset);
|
|||
|
txtLookFor.Text = editor.Document.GetText(start, endAt - start);
|
|||
|
}
|
|||
|
|
|||
|
ReplaceMode = replaceMode;
|
|||
|
|
|||
|
this.Owner = (Form)editor.TopLevelControl;
|
|||
|
this.Show();
|
|||
|
|
|||
|
txtLookFor.SelectAll();
|
|||
|
txtLookFor.Focus();
|
|||
|
}
|
|||
|
|
|||
|
public bool ReplaceMode
|
|||
|
{
|
|||
|
get { return txtReplaceWith.Visible; }
|
|||
|
set
|
|||
|
{
|
|||
|
btnReplace.Visible = btnReplaceAll.Visible = value;
|
|||
|
lblReplaceWith.Visible = txtReplaceWith.Visible = value;
|
|||
|
btnFindAllHighlightAll.Visible = !value;
|
|||
|
this.AcceptButton = value ? btnReplace : btnFindNext;
|
|||
|
UpdateTitleBar();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private void BtnFindPrevious_Click(object sender, EventArgs e)
|
|||
|
{
|
|||
|
FindNext(false, true, "内容没有找到!");
|
|||
|
}
|
|||
|
private void BtnFindNext_Click(object sender, EventArgs e)
|
|||
|
{
|
|||
|
FindNext(false, false, "内容没有找到");
|
|||
|
}
|
|||
|
|
|||
|
public bool _lastSearchWasBackward = false;
|
|||
|
public bool _lastSearchLoopedAround;
|
|||
|
|
|||
|
public TextRange FindNext(bool viaF3, bool searchBackward, string messageIfNotFound)
|
|||
|
{
|
|||
|
if (string.IsNullOrEmpty(txtLookFor.Text))
|
|||
|
{
|
|||
|
MessageBox.Show("没有指定要查找的内容!", "提示");
|
|||
|
return null;
|
|||
|
}
|
|||
|
_lastSearchWasBackward = searchBackward;
|
|||
|
_search.LookFor = txtLookFor.Text;
|
|||
|
_search.MatchCase = chkMatchCase.Checked;
|
|||
|
_search.MatchWholeWordOnly = chkMatchWholeWord.Checked;
|
|||
|
|
|||
|
Caret caret = _editor.ActiveTextAreaControl.Caret;
|
|||
|
if (viaF3 && _search.HasScanRegion && !Globals.IsInRange(caret.Offset, _search.BeginOffset, _search.EndOffset))
|
|||
|
{
|
|||
|
// user moved outside of the originally selected region
|
|||
|
_search.ClearScanRegion();
|
|||
|
UpdateTitleBar();
|
|||
|
}
|
|||
|
|
|||
|
int startFrom = caret.Offset - (searchBackward ? 1 : 0);
|
|||
|
TextRange range = _search.FindNext(startFrom, searchBackward, out _lastSearchLoopedAround);
|
|||
|
if (range != null)
|
|||
|
SelectResult(range);
|
|||
|
else if (messageIfNotFound != null)
|
|||
|
MessageBox.Show(messageIfNotFound, "提示");
|
|||
|
return range;
|
|||
|
}
|
|||
|
|
|||
|
private void SelectResult(TextRange range)
|
|||
|
{
|
|||
|
TextLocation p1 = _editor.Document.OffsetToPosition(range.Offset);
|
|||
|
TextLocation p2 = _editor.Document.OffsetToPosition(range.Offset + range.Length);
|
|||
|
_editor.ActiveTextAreaControl.SelectionManager.SetSelection(p1, p2);
|
|||
|
//_editor.ActiveTextAreaControl.ScrollTo(p1.Line, p1.Column);
|
|||
|
// Also move the caret to the end of the selection, because when the user
|
|||
|
// presses F3, the caret is where we start searching next time.
|
|||
|
_editor.ActiveTextAreaControl.Caret.Position =
|
|||
|
_editor.Document.OffsetToPosition(range.Offset + range.Length);
|
|||
|
}
|
|||
|
|
|||
|
Dictionary<TextEditorControl, HighlightGroup> _highlightGroups = new Dictionary<TextEditorControl, HighlightGroup>();
|
|||
|
|
|||
|
private void btnHighlightAll_Click(object sender, EventArgs e)
|
|||
|
{
|
|||
|
if (!_highlightGroups.ContainsKey(_editor))
|
|||
|
_highlightGroups[_editor] = new HighlightGroup(_editor);
|
|||
|
HighlightGroup group = _highlightGroups[_editor];
|
|||
|
|
|||
|
if (string.IsNullOrEmpty(LookFor))
|
|||
|
// Clear highlights
|
|||
|
group.ClearMarkers();
|
|||
|
else
|
|||
|
{
|
|||
|
_search.LookFor = txtLookFor.Text;
|
|||
|
_search.MatchCase = chkMatchCase.Checked;
|
|||
|
_search.MatchWholeWordOnly = chkMatchWholeWord.Checked;
|
|||
|
|
|||
|
bool looped = false;
|
|||
|
int offset = 0, count = 0;
|
|||
|
for (; ; )
|
|||
|
{
|
|||
|
TextRange range = _search.FindNext(offset, false, out looped);
|
|||
|
if (range == null || looped)
|
|||
|
break;
|
|||
|
offset = range.Offset + range.Length;
|
|||
|
count++;
|
|||
|
|
|||
|
TextMarker m = new TextMarker(range.Offset, range.Length,
|
|||
|
TextMarkerType.SolidBlock, Color.Yellow, Color.Black);
|
|||
|
group.AddMarker(m);
|
|||
|
}
|
|||
|
if (count == 0)
|
|||
|
MessageBox.Show("没有找到你要查找的内容", "提示");
|
|||
|
else
|
|||
|
Close();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private void FindAndReplaceForm_FormClosing(object sender, FormClosingEventArgs e)
|
|||
|
{ // Prevent dispose, as this form can be re-used
|
|||
|
if (e.CloseReason != CloseReason.FormOwnerClosing)
|
|||
|
{
|
|||
|
if (this.Owner != null)
|
|||
|
this.Owner.Select(); // prevent another app from being activated instead
|
|||
|
|
|||
|
e.Cancel = true;
|
|||
|
Hide();
|
|||
|
|
|||
|
// Discard search region
|
|||
|
_search.ClearScanRegion();
|
|||
|
_editor.Refresh(); // must repaint manually
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private void btnCancel_Click(object sender, EventArgs e)
|
|||
|
{
|
|||
|
Close();
|
|||
|
}
|
|||
|
|
|||
|
private void btnReplace_Click(object sender, EventArgs e)
|
|||
|
{
|
|||
|
SelectionManager sm = _editor.ActiveTextAreaControl.SelectionManager;
|
|||
|
if (string.Equals(sm.SelectedText, txtLookFor.Text, StringComparison.OrdinalIgnoreCase))
|
|||
|
InsertText(txtReplaceWith.Text);
|
|||
|
FindNext(false, _lastSearchWasBackward, "内容没有找到!");
|
|||
|
}
|
|||
|
|
|||
|
private void btnReplaceAll_Click(object sender, EventArgs e)
|
|||
|
{
|
|||
|
int count = 0;
|
|||
|
// BUG FIX: if the replacement string contains the original search string
|
|||
|
// (e.g. replace "red" with "very red") we must avoid looping around and
|
|||
|
// replacing forever! To fix, start replacing at beginning of region (by
|
|||
|
// moving the caret) and stop as soon as we loop around.
|
|||
|
_editor.ActiveTextAreaControl.Caret.Position =
|
|||
|
_editor.Document.OffsetToPosition(_search.BeginOffset);
|
|||
|
|
|||
|
//_editor.Document.UndoStack.StartUndoGroup();
|
|||
|
//try
|
|||
|
//{
|
|||
|
lock (_editor.Document.UndoStack)
|
|||
|
{
|
|||
|
while (FindNext(false, false, null) != null)
|
|||
|
{
|
|||
|
if (_lastSearchLoopedAround)
|
|||
|
break;
|
|||
|
|
|||
|
// Replace
|
|||
|
count++;
|
|||
|
InsertText(txtReplaceWith.Text);
|
|||
|
}
|
|||
|
}
|
|||
|
//}
|
|||
|
//finally
|
|||
|
//{
|
|||
|
// _editor.Document.UndoStack.EndUndoGroup();
|
|||
|
//}
|
|||
|
if (count == 0)
|
|||
|
MessageBox.Show(string.Format("没有找到目标内容: {0}!", txtLookFor.Text), "提示");
|
|||
|
else
|
|||
|
{
|
|||
|
MessageBox.Show(string.Format("替换了 {0} 处内容!", count), "提示");
|
|||
|
Close();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private void InsertText(string text)
|
|||
|
{
|
|||
|
TextArea textArea = _editor.ActiveTextAreaControl.TextArea;
|
|||
|
//textArea.Document.UndoStack.StartUndoGroup();
|
|||
|
//try
|
|||
|
//{
|
|||
|
lock (textArea.Document.UndoStack)
|
|||
|
{
|
|||
|
if (textArea.SelectionManager.HasSomethingSelected)
|
|||
|
{
|
|||
|
textArea.Caret.Position = textArea.SelectionManager.SelectionCollection[0].StartPosition;
|
|||
|
textArea.SelectionManager.RemoveSelectedText();
|
|||
|
}
|
|||
|
textArea.InsertString(text);
|
|||
|
}
|
|||
|
//}
|
|||
|
//finally
|
|||
|
//{
|
|||
|
// textArea.Document.UndoStack.EndUndoGroup();
|
|||
|
//}
|
|||
|
}
|
|||
|
|
|||
|
public string LookFor { get { return txtLookFor.Text; } }
|
|||
|
}
|
|||
|
|
|||
|
public class TextRange : AbstractSegment
|
|||
|
{
|
|||
|
IDocument _document;
|
|||
|
public TextRange(IDocument document, int offset, int length)
|
|||
|
{
|
|||
|
_document = document;
|
|||
|
this.offset = offset;
|
|||
|
this.length = length;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>This class finds occurrances of a search string in a text
|
|||
|
/// editor's IDocument... it's like Find box without a GUI.</summary>
|
|||
|
public class TextEditorSearcher : IDisposable
|
|||
|
{
|
|||
|
IDocument _document;
|
|||
|
public IDocument Document
|
|||
|
{
|
|||
|
get { return _document; }
|
|||
|
set
|
|||
|
{
|
|||
|
if (_document != value)
|
|||
|
{
|
|||
|
ClearScanRegion();
|
|||
|
_document = value;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// I would have used the TextAnchor class to represent the beginning and
|
|||
|
// end of the region to scan while automatically adjusting to changes in
|
|||
|
// the document--but for some reason it is sealed and its constructor is
|
|||
|
// internal. Instead I use a TextMarker, which is perhaps even better as
|
|||
|
// it gives me the opportunity to highlight the region. Note that all the
|
|||
|
// markers and coloring information is associated with the text document,
|
|||
|
// not the editor control, so TextEditorSearcher doesn't need a reference
|
|||
|
// to the TextEditorControl. After adding the marker to the document, we
|
|||
|
// must remember to remove it when it is no longer needed.
|
|||
|
TextMarker _region = null;
|
|||
|
/// <summary>Sets the region to search. The region is updated
|
|||
|
/// automatically as the document changes.</summary>
|
|||
|
public void SetScanRegion(ISelection sel)
|
|||
|
{
|
|||
|
SetScanRegion(sel.Offset, sel.Length);
|
|||
|
}
|
|||
|
/// <summary>Sets the region to search. The region is updated
|
|||
|
/// automatically as the document changes.</summary>
|
|||
|
public void SetScanRegion(int offset, int length)
|
|||
|
{
|
|||
|
Color bkgColor = _document.HighlightingStrategy.GetColorFor("Default").BackgroundColor;
|
|||
|
_region = new TextMarker(offset, length, TextMarkerType.SolidBlock,
|
|||
|
Globals.HalfMix(bkgColor, Color.FromArgb(160, 160, 160)));
|
|||
|
_document.MarkerStrategy.AddMarker(_region);
|
|||
|
}
|
|||
|
public bool HasScanRegion
|
|||
|
{
|
|||
|
get { return _region != null; }
|
|||
|
}
|
|||
|
public void ClearScanRegion()
|
|||
|
{
|
|||
|
if (_region != null)
|
|||
|
{
|
|||
|
_document.MarkerStrategy.RemoveMarker(_region);
|
|||
|
_region = null;
|
|||
|
}
|
|||
|
}
|
|||
|
public void Dispose() { ClearScanRegion(); GC.SuppressFinalize(this); }
|
|||
|
~TextEditorSearcher() { Dispose(); }
|
|||
|
|
|||
|
/// <summary>Begins the start offset for searching</summary>
|
|||
|
public int BeginOffset
|
|||
|
{
|
|||
|
get
|
|||
|
{
|
|||
|
if (_region != null)
|
|||
|
return _region.Offset;
|
|||
|
else
|
|||
|
return 0;
|
|||
|
}
|
|||
|
}
|
|||
|
/// <summary>Begins the end offset for searching</summary>
|
|||
|
public int EndOffset
|
|||
|
{
|
|||
|
get
|
|||
|
{
|
|||
|
if (_region != null)
|
|||
|
return _region.EndOffset;
|
|||
|
else
|
|||
|
return _document.TextLength;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public bool MatchCase;
|
|||
|
|
|||
|
public bool MatchWholeWordOnly;
|
|||
|
|
|||
|
string _lookFor;
|
|||
|
string _lookFor2; // uppercase in case-insensitive mode
|
|||
|
public string LookFor
|
|||
|
{
|
|||
|
get { return _lookFor; }
|
|||
|
set { _lookFor = value; }
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>Finds next instance of LookFor, according to the search rules
|
|||
|
/// (MatchCase, MatchWholeWordOnly).</summary>
|
|||
|
/// <param name="beginAtOffset">Offset in Document at which to begin the search</param>
|
|||
|
/// <remarks>If there is a match at beginAtOffset precisely, it will be returned.</remarks>
|
|||
|
/// <returns>Region of document that matches the search string</returns>
|
|||
|
public TextRange FindNext(int beginAtOffset, bool searchBackward, out bool loopedAround)
|
|||
|
{
|
|||
|
Debug.Assert(!string.IsNullOrEmpty(_lookFor));
|
|||
|
loopedAround = false;
|
|||
|
|
|||
|
int startAt = BeginOffset, endAt = EndOffset;
|
|||
|
int curOffs = Globals.InRange(beginAtOffset, startAt, endAt);
|
|||
|
|
|||
|
_lookFor2 = MatchCase ? _lookFor : _lookFor.ToUpperInvariant();
|
|||
|
|
|||
|
TextRange result;
|
|||
|
if (searchBackward)
|
|||
|
{
|
|||
|
result = FindNextIn(startAt, curOffs, true);
|
|||
|
if (result == null)
|
|||
|
{
|
|||
|
loopedAround = true;
|
|||
|
result = FindNextIn(curOffs, endAt, true);
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
result = FindNextIn(curOffs, endAt, false);
|
|||
|
if (result == null)
|
|||
|
{
|
|||
|
loopedAround = true;
|
|||
|
result = FindNextIn(startAt, curOffs, false);
|
|||
|
}
|
|||
|
}
|
|||
|
return result;
|
|||
|
}
|
|||
|
|
|||
|
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
|
|||
|
public delegate TResult Func<T, TResult>(T arg);
|
|||
|
|
|||
|
private TextRange FindNextIn(int offset1, int offset2, bool searchBackward)
|
|||
|
{
|
|||
|
Debug.Assert(offset2 >= offset1);
|
|||
|
offset2 -= _lookFor.Length;
|
|||
|
|
|||
|
// Make behavior decisions before starting search loop
|
|||
|
Func<char, char, bool> matchFirstCh;
|
|||
|
Func<int, bool> matchWord;
|
|||
|
if (MatchCase)
|
|||
|
matchFirstCh = delegate(char lookFor, char c)
|
|||
|
{
|
|||
|
return lookFor == c;
|
|||
|
};
|
|||
|
else
|
|||
|
matchFirstCh = delegate(char lookFor, char c)
|
|||
|
{
|
|||
|
return lookFor == Char.ToUpperInvariant(c);
|
|||
|
};
|
|||
|
|
|||
|
if (MatchWholeWordOnly)
|
|||
|
matchWord = IsWholeWordMatch;
|
|||
|
else
|
|||
|
matchWord = IsPartWordMatch;
|
|||
|
|
|||
|
// Search
|
|||
|
char lookForCh = _lookFor2[0];
|
|||
|
if (searchBackward)
|
|||
|
{
|
|||
|
for (int offset = offset2; offset >= offset1; offset--)
|
|||
|
{
|
|||
|
if (matchFirstCh(lookForCh, _document.GetCharAt(offset))
|
|||
|
&& matchWord(offset))
|
|||
|
return new TextRange(_document, offset, _lookFor.Length);
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
for (int offset = offset1; offset <= offset2; offset++)
|
|||
|
{
|
|||
|
if (matchFirstCh(lookForCh, _document.GetCharAt(offset))
|
|||
|
&& matchWord(offset))
|
|||
|
return new TextRange(_document, offset, _lookFor.Length);
|
|||
|
}
|
|||
|
}
|
|||
|
return null;
|
|||
|
}
|
|||
|
private bool IsWholeWordMatch(int offset)
|
|||
|
{
|
|||
|
if (IsWordBoundary(offset) && IsWordBoundary(offset + _lookFor.Length))
|
|||
|
return IsPartWordMatch(offset);
|
|||
|
else
|
|||
|
return false;
|
|||
|
}
|
|||
|
private bool IsWordBoundary(int offset)
|
|||
|
{
|
|||
|
return offset <= 0 || offset >= _document.TextLength ||
|
|||
|
!IsAlphaNumeric(offset - 1) || !IsAlphaNumeric(offset);
|
|||
|
}
|
|||
|
private bool IsAlphaNumeric(int offset)
|
|||
|
{
|
|||
|
char c = _document.GetCharAt(offset);
|
|||
|
return Char.IsLetterOrDigit(c) || c == '_';
|
|||
|
}
|
|||
|
private bool IsPartWordMatch(int offset)
|
|||
|
{
|
|||
|
string substr = _document.GetText(offset, _lookFor.Length);
|
|||
|
if (!MatchCase)
|
|||
|
substr = substr.ToUpperInvariant();
|
|||
|
return substr == _lookFor2;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/// <summary>Bundles a group of markers together so that they can be cleared
|
|||
|
/// together.</summary>
|
|||
|
public class HighlightGroup : IDisposable
|
|||
|
{
|
|||
|
List<TextMarker> _markers = new List<TextMarker>();
|
|||
|
TextEditorControl _editor;
|
|||
|
IDocument _document;
|
|||
|
public HighlightGroup(TextEditorControl editor)
|
|||
|
{
|
|||
|
_editor = editor;
|
|||
|
_document = editor.Document;
|
|||
|
}
|
|||
|
public void AddMarker(TextMarker marker)
|
|||
|
{
|
|||
|
_markers.Add(marker);
|
|||
|
_document.MarkerStrategy.AddMarker(marker);
|
|||
|
}
|
|||
|
public void ClearMarkers()
|
|||
|
{
|
|||
|
foreach (TextMarker m in _markers)
|
|||
|
_document.MarkerStrategy.RemoveMarker(m);
|
|||
|
_markers.Clear();
|
|||
|
_editor.Refresh();
|
|||
|
}
|
|||
|
public void Dispose() { ClearMarkers(); GC.SuppressFinalize(this); }
|
|||
|
~HighlightGroup() { Dispose(); }
|
|||
|
|
|||
|
public IList<TextMarker> Markers { get { return _markers.AsReadOnly(); } }
|
|||
|
}
|
|||
|
}
|