whole words search/replace/highlight support in editors - close #424

This commit is contained in:
Vadim Lopatin 2017-09-08 12:09:33 +03:00
parent d3469713c6
commit 2a78f1d9d1
1 changed files with 72 additions and 19 deletions

View File

@ -49,6 +49,13 @@ interface EditableContentChangeListener {
void onEditableContentChanged(EditableContent source); void onEditableContentChanged(EditableContent source);
} }
/// Flags used for search / replace / text highlight
enum TextSearchFlag {
CaseSensitive = 1,
WholeWords = 2,
SelectionOnly = 4,
}
/// Editor action codes /// Editor action codes
enum EditorActions : int { enum EditorActions : int {
None = 0, None = 0,
@ -2736,15 +2743,16 @@ class EditBox : EditWidgetBase {
} }
protected dstring _textToHighlight; protected dstring _textToHighlight;
protected bool _textToHighlightCaseSensitive; protected uint _textToHighlightOptions;
/// text pattern to highlight - e.g. for search /// text pattern to highlight - e.g. for search
@property dstring textToHighlight() { @property dstring textToHighlight() {
return _textToHighlight; return _textToHighlight;
} }
/// set text to highlight -- e.g. for search /// set text to highlight -- e.g. for search
void setTextToHighlight(dstring pattern, bool caseSensitive) { void setTextToHighlight(dstring pattern, uint textToHighlightOptions) {
_textToHighlight = pattern; _textToHighlight = pattern;
_textToHighlightCaseSensitive = caseSensitive; _textToHighlightOptions = textToHighlightOptions;
invalidate(); invalidate();
} }
@ -2756,39 +2764,72 @@ class EditBox : EditWidgetBase {
return; return;
ptrdiff_t start = 0; ptrdiff_t start = 0;
import std.string : indexOf, CaseSensitive, Yes, No; import std.string : indexOf, CaseSensitive, Yes, No;
bool caseSensitive = (_textToHighlightOptions & TextSearchFlag.CaseSensitive) != 0;
bool wholeWords = (_textToHighlightOptions & TextSearchFlag.WholeWords) != 0;
bool selectionOnly = (_textToHighlightOptions & TextSearchFlag.SelectionOnly) != 0;
for (;;) { for (;;) {
ptrdiff_t pos = lineText[start .. $].indexOf(_textToHighlight, _textToHighlightCaseSensitive ? Yes.caseSensitive : No.caseSensitive); ptrdiff_t pos = lineText[start .. $].indexOf(_textToHighlight, caseSensitive ? Yes.caseSensitive : No.caseSensitive);
if (pos < 0) if (pos < 0)
break; break;
// found text to highlight // found text to highlight
start += pos; start += pos;
if (!wholeWords || isWholeWord(lineText, start, start + _textToHighlight.length)) {
TextRange r = TextRange(TextPosition(lineIndex, cast(int)start), TextPosition(lineIndex, cast(int)(start + _textToHighlight.length))); TextRange r = TextRange(TextPosition(lineIndex, cast(int)start), TextPosition(lineIndex, cast(int)(start + _textToHighlight.length)));
start += _textToHighlight.length;
uint color = r.isInsideOrNext(caretPos) ? _searchHighlightColorCurrent : _searchHighlightColorOther; uint color = r.isInsideOrNext(caretPos) ? _searchHighlightColorCurrent : _searchHighlightColorOther;
highlightLineRange(buf, lineRect, color, r); highlightLineRange(buf, lineRect, color, r);
} }
start += _textToHighlight.length;
}
} }
/// find all occurences of text pattern in content static bool isWordChar(dchar ch) {
TextRange[] findAll(dstring pattern, bool caseSensitive) { if (ch >= 'a' && ch <= 'z')
return false;
if (ch >= 'A' && ch <= 'Z')
return false;
if (ch == '_')
return false;
return true;
}
static bool isValidWordBound(dchar innerChar, dchar outerChar) {
return isWordChar(innerChar) || isWordChar(outerChar);
}
/// returns true if selected range of string is whole word
static bool isWholeWord(dstring lineText, size_t start, size_t end) {
if (start >= lineText.length || start >= end)
return false;
if (start > 0 && !isValidWordBound(lineText[start], lineText[start - 1]))
return false;
if (end > 0 && end < lineText.length && !isValidWordBound(lineText[end - 1], lineText[end]))
return false;
return true;
}
/// find all occurences of text pattern in content; options = bitset of TextSearchFlag
TextRange[] findAll(dstring pattern, uint options) {
TextRange[] res; TextRange[] res;
res.assumeSafeAppend(); res.assumeSafeAppend();
if (!pattern.length) if (!pattern.length)
return res; return res;
import std.string : indexOf, CaseSensitive, Yes, No; import std.string : indexOf, CaseSensitive, Yes, No;
bool caseSensitive = (options & TextSearchFlag.CaseSensitive) != 0;
bool wholeWords = (options & TextSearchFlag.WholeWords) != 0;
bool selectionOnly = (options & TextSearchFlag.SelectionOnly) != 0;
for (int i = 0; i < _content.length; i++) { for (int i = 0; i < _content.length; i++) {
dstring lineText = _content.line(i); dstring lineText = _content.line(i);
if (lineText.length < pattern.length) if (lineText.length < pattern.length)
continue; continue;
ptrdiff_t start = 0; ptrdiff_t start = 0;
for (;;) { for (;;) {
ptrdiff_t pos = lineText[start .. $].indexOf(_textToHighlight, _textToHighlightCaseSensitive ? Yes.caseSensitive : No.caseSensitive); ptrdiff_t pos = lineText[start .. $].indexOf(pattern, caseSensitive ? Yes.caseSensitive : No.caseSensitive);
if (pos < 0) if (pos < 0)
break; break;
// found text to highlight // found text to highlight
start += pos; start += pos;
TextRange r = TextRange(TextPosition(i, cast(int)start), TextPosition(i, cast(int)(start + _textToHighlight.length))); if (!wholeWords || isWholeWord(lineText, start, start + pattern.length)) {
TextRange r = TextRange(TextPosition(i, cast(int)start), TextPosition(i, cast(int)(start + pattern.length)));
res ~= r; res ~= r;
}
start += _textToHighlight.length; start += _textToHighlight.length;
} }
} }
@ -2796,8 +2837,8 @@ class EditBox : EditWidgetBase {
} }
/// find next occurence of text pattern in content, returns true if found /// find next occurence of text pattern in content, returns true if found
bool findNextPattern(ref TextPosition pos, dstring pattern, bool caseSensitive, int direction) { bool findNextPattern(ref TextPosition pos, dstring pattern, uint searchOptions, int direction) {
TextRange[] all = findAll(pattern, caseSensitive); TextRange[] all = findAll(pattern, searchOptions);
if (!all.length) if (!all.length)
return false; return false;
int currentIndex = -1; int currentIndex = -1;
@ -3424,6 +3465,8 @@ class FindPanel : HorizontalLayout {
_cbWholeWords = childById!ImageCheckButton("cbWholeWords"); _cbWholeWords = childById!ImageCheckButton("cbWholeWords");
_cbSelection = childById!CheckBox("cbSelection"); _cbSelection = childById!CheckBox("cbSelection");
_cbCaseSensitive.checkChange = &onCaseSensitiveCheckChange; _cbCaseSensitive.checkChange = &onCaseSensitiveCheckChange;
_cbWholeWords.checkChange = &onCaseSensitiveCheckChange;
_cbSelection.checkChange = &onCaseSensitiveCheckChange;
if (!replace) if (!replace)
childById("replace").visibility = Visibility.Gone; childById("replace").visibility = Visibility.Gone;
//_edFind = new EditLine("edFind" //_edFind = new EditLine("edFind"
@ -3492,15 +3535,25 @@ class FindPanel : HorizontalLayout {
} }
} }
uint makeSearchFlags() {
uint res = 0;
if (_cbCaseSensitive.checked)
res |= TextSearchFlag.CaseSensitive;
if (_cbWholeWords.checked)
res |= TextSearchFlag.WholeWords;
if (_cbSelection.checked)
res |= TextSearchFlag.SelectionOnly;
return res;
}
bool findNext(bool back) { bool findNext(bool back) {
setDirection(back); setDirection(back);
dstring currentText = _edFind.text; dstring currentText = _edFind.text;
Log.d("findNext text=", currentText, " back=", back); Log.d("findNext text=", currentText, " back=", back);
if (!currentText.length) if (!currentText.length)
return false; return false;
_editor.setTextToHighlight(currentText, _cbCaseSensitive.checked); _editor.setTextToHighlight(currentText, makeSearchFlags);
TextPosition pos = _editor.caretPos; TextPosition pos = _editor.caretPos;
bool res = _editor.findNextPattern(pos, currentText, _cbCaseSensitive.checked, back ? -1 : 1); bool res = _editor.findNextPattern(pos, currentText, makeSearchFlags, back ? -1 : 1);
if (res) { if (res) {
_editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length)); _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length));
_editor.ensureCaretVisible(); _editor.ensureCaretVisible();
@ -3515,9 +3568,9 @@ class FindPanel : HorizontalLayout {
Log.d("replaceOne text=", currentText, " back=", _backDirection, " newText=", newText); Log.d("replaceOne text=", currentText, " back=", _backDirection, " newText=", newText);
if (!currentText.length) if (!currentText.length)
return false; return false;
_editor.setTextToHighlight(currentText, _cbCaseSensitive.checked); _editor.setTextToHighlight(currentText, makeSearchFlags);
TextPosition pos = _editor.caretPos; TextPosition pos = _editor.caretPos;
bool res = _editor.findNextPattern(pos, currentText, _cbCaseSensitive.checked, 0); bool res = _editor.findNextPattern(pos, currentText, makeSearchFlags, 0);
if (res) { if (res) {
_editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length)); _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length));
_editor.replaceSelectionText(newText); _editor.replaceSelectionText(newText);
@ -3554,7 +3607,7 @@ class FindPanel : HorizontalLayout {
void updateHighlight() { void updateHighlight() {
dstring currentText = _edFind.text; dstring currentText = _edFind.text;
Log.d("onFindTextChange.currentText=", currentText); Log.d("onFindTextChange.currentText=", currentText);
_editor.setTextToHighlight(currentText, _cbCaseSensitive.checked); _editor.setTextToHighlight(currentText, makeSearchFlags);
} }
void onFindTextChange(EditableContent source) { void onFindTextChange(EditableContent source) {