Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions examples/ted/FindReplaceDialog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using Terminal.Gui.Views;
using Terminal.Gui.ViewBase;

namespace Ted;

internal sealed class FindReplaceDialog : Dialog
{
private readonly TextField _findTextField;
private readonly TextField _replaceFindTextField;
private readonly TextField _replaceWithTextField;

public FindReplaceDialog (Editor editor, bool selectReplaceTab)
{
ArgumentNullException.ThrowIfNull (editor);

Title = "_Find / Replace";
Width = Dim.Percent (70);
Height = Dim.Percent (50);

string initialSearchText = string.Empty;

if (editor.HasSelection && editor.Document is { } document)
{
int start = Math.Clamp (editor.SelectionStart, 0, document.TextLength);
int end = Math.Clamp (editor.SelectionEnd, start, document.TextLength);
initialSearchText = document.Text.Substring (start, end - start);
}

_findTextField = new () { X = 11, Y = 1, Width = Dim.Fill (2), Text = initialSearchText };
_replaceFindTextField = new () { X = 11, Y = 1, Width = Dim.Fill (2), Text = initialSearchText };
_replaceWithTextField = new () { X = 11, Y = 3, Width = Dim.Fill (2) };

Tabs tabs = new () { Width = Dim.Fill (), Height = Dim.Fill () };
View findTab = BuildFindTab (editor);
View replaceTab = BuildReplaceTab (editor);

tabs.Add (findTab);
tabs.Add (replaceTab);
tabs.Value = selectReplaceTab ? replaceTab : findTab;

AddButton (new Button { Text = "_Close" });
Add (tabs);

if (selectReplaceTab)
{
_replaceFindTextField.SetFocus ();

return;
}

_findTextField.SetFocus ();
}

private View BuildFindTab (Editor editor)
{
View tab = new () { Title = "_Find" };
Button findNextButton = new () { X = 1, Y = 3, Text = "Find _Next" };

tab.Add (
new Label { X = 1, Y = 1, Text = "_Find:" },
_findTextField,
findNextButton);

findNextButton.Accepting += (_, _) => editor.FindNext (_findTextField.Text ?? string.Empty);

return tab;
}

private View BuildReplaceTab (Editor editor)
{
View tab = new () { Title = "_Replace" };

tab.Add (
new Label { X = 1, Y = 1, Text = "_Find:" },
_replaceFindTextField,
new Label { X = 1, Y = 3, Text = "_Replace:" },
_replaceWithTextField);

Button findNextButton = new () { X = 1, Y = 5, Text = "Find _Next" };
Button replaceButton = new () { X = Pos.Right (findNextButton) + 1, Y = 5, Text = "_Replace" };
Button replaceAllButton = new () { X = Pos.Right (replaceButton) + 1, Y = 5, Text = "Replace _All" };

findNextButton.Accepting += (_, _) => editor.FindNext (_replaceFindTextField.Text ?? string.Empty);
replaceButton.Accepting += (_, _) => editor.ReplaceNext (
_replaceFindTextField.Text ?? string.Empty,
_replaceWithTextField.Text ?? string.Empty);
replaceAllButton.Accepting += (_, _) => editor.ReplaceAll (
_replaceFindTextField.Text ?? string.Empty,
_replaceWithTextField.Text ?? string.Empty);

tab.Add (findNextButton, replaceButton, replaceAllButton);

return tab;
}
}
15 changes: 15 additions & 0 deletions examples/ted/TedApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ private Key KeyFor (Command command)

private void Action () { }

private void Find () { ShowFindReplaceDialog (selectReplaceTab: false); }

private void Replace () { ShowFindReplaceDialog (selectReplaceTab: true); }

private void SelectAll () { }

private void Paste () { }
Expand Down Expand Up @@ -344,4 +348,15 @@ private void Quit ()
// TODO: add logic for unsaved changes, confirm quit, etc.
RequestStop ();
}

private void ShowFindReplaceDialog (bool selectReplaceTab)
{
if (App is null)
{
throw new InvalidOperationException ("Cannot show find/replace when Application is not running.");
}

using FindReplaceDialog dialog = new (Editor, selectReplaceTab);
App.Run (dialog);
}
}
183 changes: 183 additions & 0 deletions src/Terminal.Gui.Editor/Editor.FindReplace.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
namespace Terminal.Gui.Views;

public partial class Editor
{
/// <summary>
/// Finds the next match for <paramref name="searchText"/> starting at the current caret (or after the current
/// selection) and selects it.
/// </summary>
/// <returns><see langword="true"/> when a match is found; otherwise <see langword="false"/>.</returns>
public bool FindNext (string searchText, bool matchCase = false, bool wrapAround = true)
{
if (string.IsNullOrEmpty (searchText) || _document is null)
{
return false;
}

int startOffset = HasSelection ? SelectionEnd : _caretOffset;
int matchOffset = FindForwardOffset (searchText, startOffset, matchCase);

if (matchOffset < 0 && wrapAround && startOffset > 0)
{
matchOffset = FindForwardOffset (searchText, 0, matchCase);
}

if (matchOffset < 0)
{
return false;
}

SelectSearchMatch (matchOffset, searchText.Length);

return true;
}

/// <summary>
/// Finds the previous match for <paramref name="searchText"/> before the current caret (or before the current
/// selection start) and selects it.
/// </summary>
/// <returns><see langword="true"/> when a match is found; otherwise <see langword="false"/>.</returns>
public bool FindPrevious (string searchText, bool matchCase = false, bool wrapAround = true)
{
if (string.IsNullOrEmpty (searchText) || _document is null || _document.TextLength == 0)
{
return false;
}

int startOffset = HasSelection ? SelectionStart - 1 : _caretOffset - 1;
int matchOffset = FindBackwardOffset (searchText, startOffset, matchCase);

if (matchOffset < 0 && wrapAround && startOffset < _document.TextLength - 1)
{
matchOffset = FindBackwardOffset (searchText, _document.TextLength - 1, matchCase);
}

if (matchOffset < 0)
{
return false;
}

SelectSearchMatch (matchOffset, searchText.Length);

return true;
}

/// <summary>
/// Replaces the current match (if selected) or finds the next match and replaces it.
/// </summary>
/// <returns><see langword="true"/> when a replacement is made; otherwise <see langword="false"/>.</returns>
public bool ReplaceNext (string searchText, string replacement, bool matchCase = false, bool wrapAround = true)
{
ArgumentNullException.ThrowIfNull (replacement);

if (string.IsNullOrEmpty (searchText) || _document is null)
{
return false;
}

if (!SelectionMatches (searchText, matchCase))
{
if (!FindNext (searchText, matchCase, wrapAround))
{
return false;
}
}

ReplaceSelection (replacement);

return true;
}

/// <summary>
/// Replaces all matches of <paramref name="searchText"/> in the document.
/// </summary>
/// <returns>The number of replacements performed.</returns>
public int ReplaceAll (string searchText, string replacement, bool matchCase = false)
{
ArgumentNullException.ThrowIfNull (replacement);

if (string.IsNullOrEmpty (searchText) || _document is null)
{
return 0;
}

int replacements = 0;
int searchOffset = 0;

while (searchOffset < _document.TextLength)
{
int matchOffset = FindForwardOffset (searchText, searchOffset, matchCase);

if (matchOffset < 0)
{
break;
}

SelectSearchMatch (matchOffset, searchText.Length);
ReplaceSelection (replacement);

searchOffset = matchOffset + replacement.Length;
replacements++;
}

return replacements;
}

private int FindForwardOffset (string searchText, int startOffset, bool matchCase)
{
if (_document is null || startOffset < 0 || startOffset > _document.TextLength)
{
return -1;
}

StringComparison comparison = matchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;

return _document.Text.IndexOf (searchText, startOffset, comparison);
}

private int FindBackwardOffset (string searchText, int startOffset, bool matchCase)
{
if (_document is null || _document.TextLength == 0 || startOffset < 0)
{
return -1;
}

int clampedStart = Math.Clamp (startOffset, 0, _document.TextLength - 1);
StringComparison comparison = matchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;

return _document.Text.LastIndexOf (searchText, clampedStart, comparison);
}

private bool SelectionMatches (string searchText, bool matchCase)
{
if (!HasSelection || _document is null || SelectionLength != searchText.Length)
{
return false;
}

if (SelectionStart < 0 || SelectionLength < 0 || SelectionEnd > _document.TextLength)
{
return false;
}

StringComparison comparison = matchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;

return string.Compare (_document.Text, SelectionStart, searchText, 0, searchText.Length, comparison) == 0;
}

private void SelectSearchMatch (int startOffset, int length)
{
if (_document is null || length <= 0)
{
return;
}

int start = Math.Clamp (startOffset, 0, _document.TextLength);
int end = Math.Clamp (start + length, start, _document.TextLength);

_selectionAnchor = start;
CaretOffset = end;
SelectionChanged?.Invoke (this, EventArgs.Empty);
SetNeedsDraw ();
}
}
16 changes: 16 additions & 0 deletions tests/Terminal.Gui.Editor.IntegrationTests/TedAppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,20 @@ public async Task FileMenu_OpensViaMouse_ClickOnHeader ()

DriverAssert.ContentsContains (fx.Driver, "Open...");
}

[Fact]
public async Task EditMenu_OpensViaKeyboard_AltE_Contains_Find_And_Replace ()
{
await using AppFixture<TedApp> fx = new (() => new TedApp ());

DriverAssert.ContentsDoesNotContain (fx.Driver, "Find...");
DriverAssert.ContentsDoesNotContain (fx.Driver, "Replace...");

InputInjectionOptions options = new () { Mode = InputInjectionMode.Direct };
fx.Injector.InjectKey (Key.E.WithAlt, options);
fx.Render ();

DriverAssert.ContentsContains (fx.Driver, "Find...");
DriverAssert.ContentsContains (fx.Driver, "Replace...");
}
}
Loading