github.com/aretext/aretext@v1.3.0/state/search_test.go (about)

     1  package state
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/stretchr/testify/assert"
     7  	"github.com/stretchr/testify/require"
     8  
     9  	"github.com/aretext/aretext/clipboard"
    10  	"github.com/aretext/aretext/text"
    11  )
    12  
    13  func TestSearchAndCommit(t *testing.T) {
    14  	textTree, err := text.NewTreeFromString("foo bar baz")
    15  	require.NoError(t, err)
    16  	state := NewEditorState(100, 100, nil, nil)
    17  	buffer := state.documentBuffer
    18  	buffer.textTree = textTree
    19  
    20  	// Start a search.
    21  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
    22  	assert.Equal(t, state.inputMode, InputModeSearch)
    23  	assert.Equal(t, buffer.search.query, "")
    24  
    25  	// Enter a search query.
    26  	AppendRuneToSearchQuery(state, 'b')
    27  	assert.Equal(t, "b", buffer.search.query)
    28  	require.NotNil(t, buffer.search.match)
    29  	assert.Equal(t, uint64(4), buffer.search.match.StartPos)
    30  	assert.Equal(t, uint64(5), buffer.search.match.EndPos)
    31  
    32  	AppendRuneToSearchQuery(state, 'a')
    33  	assert.Equal(t, "ba", buffer.search.query)
    34  	require.NotNil(t, buffer.search.match)
    35  	assert.Equal(t, uint64(4), buffer.search.match.StartPos)
    36  	assert.Equal(t, uint64(6), buffer.search.match.EndPos)
    37  
    38  	AppendRuneToSearchQuery(state, 'r')
    39  	assert.Equal(t, "bar", buffer.search.query)
    40  	require.NotNil(t, buffer.search.match)
    41  	assert.Equal(t, uint64(4), buffer.search.match.StartPos)
    42  	assert.Equal(t, uint64(7), buffer.search.match.EndPos)
    43  
    44  	DeleteRuneFromSearchQuery(state)
    45  	assert.Equal(t, "ba", buffer.search.query)
    46  	require.NotNil(t, buffer.search.match)
    47  	assert.Equal(t, uint64(4), buffer.search.match.StartPos)
    48  	assert.Equal(t, uint64(6), buffer.search.match.EndPos)
    49  
    50  	// Commit the search.
    51  	CompleteSearch(state, true)
    52  	assert.Equal(t, state.inputMode, InputModeNormal)
    53  	assert.Equal(t, "ba", buffer.search.query)
    54  	assert.Nil(t, buffer.search.match)
    55  	assert.Equal(t, cursorState{position: 4}, buffer.cursor)
    56  }
    57  
    58  func TestSearchAndAbort(t *testing.T) {
    59  	textTree, err := text.NewTreeFromString("foo bar baz")
    60  	require.NoError(t, err)
    61  	state := NewEditorState(100, 100, nil, nil)
    62  	buffer := state.documentBuffer
    63  	buffer.textTree = textTree
    64  	buffer.search.query = "xyz"
    65  
    66  	// Start a search.
    67  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
    68  	assert.Equal(t, state.inputMode, InputModeSearch)
    69  	assert.Equal(t, buffer.search.query, "")
    70  	assert.Equal(t, buffer.search.prevQuery, "xyz")
    71  
    72  	// Enter a search query.
    73  	AppendRuneToSearchQuery(state, 'b')
    74  	assert.Equal(t, "b", buffer.search.query)
    75  	require.NotNil(t, buffer.search.match)
    76  	assert.Equal(t, uint64(4), buffer.search.match.StartPos)
    77  	assert.Equal(t, uint64(5), buffer.search.match.EndPos)
    78  
    79  	// Abort the search.
    80  	CompleteSearch(state, false)
    81  	assert.Equal(t, state.inputMode, InputModeNormal)
    82  	assert.Equal(t, "xyz", buffer.search.query)
    83  	assert.Nil(t, buffer.search.match)
    84  	assert.Equal(t, cursorState{position: 0}, buffer.cursor)
    85  }
    86  
    87  func TestSearchAndBackspaceEmptyQuery(t *testing.T) {
    88  	textTree, err := text.NewTreeFromString("foo bar baz")
    89  	require.NoError(t, err)
    90  	state := NewEditorState(100, 100, nil, nil)
    91  	buffer := state.documentBuffer
    92  	buffer.textTree = textTree
    93  
    94  	// Start a search.
    95  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
    96  	assert.Equal(t, state.inputMode, InputModeSearch)
    97  	assert.Equal(t, buffer.search.query, "")
    98  
    99  	// Delete from the empty query, equivalent to aborting the search.
   100  	DeleteRuneFromSearchQuery(state)
   101  	assert.Equal(t, state.inputMode, InputModeNormal)
   102  	assert.Equal(t, "", buffer.search.query)
   103  	assert.Nil(t, buffer.search.match)
   104  	assert.Equal(t, cursorState{position: 0}, buffer.cursor)
   105  }
   106  
   107  func TestSearchForwardCursorOnMatch(t *testing.T) {
   108  	textTree, err := text.NewTreeFromString("foo bar foo")
   109  	require.NoError(t, err)
   110  	state := NewEditorState(100, 100, nil, nil)
   111  	buffer := state.documentBuffer
   112  	buffer.textTree = textTree
   113  
   114  	// Enter a search query matching at the cursor's current position.
   115  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   116  	AppendRuneToSearchQuery(state, 'f')
   117  	AppendRuneToSearchQuery(state, 'o')
   118  	AppendRuneToSearchQuery(state, 'o')
   119  	assert.Equal(t, "foo", buffer.search.query)
   120  
   121  	// Expect that to find the match *after* the cursor's position.
   122  	require.NotNil(t, buffer.search.match)
   123  	assert.Equal(t, uint64(8), buffer.search.match.StartPos)
   124  	assert.Equal(t, uint64(11), buffer.search.match.EndPos)
   125  }
   126  
   127  func TestSearchForwardWithWraparoundCursorAtBeginning(t *testing.T) {
   128  	textTree, err := text.NewTreeFromString("abc")
   129  	require.NoError(t, err)
   130  	state := NewEditorState(100, 100, nil, nil)
   131  	buffer := state.documentBuffer
   132  	buffer.textTree = textTree
   133  
   134  	// Enter a search query matching at the cursor's current position.
   135  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   136  	AppendRuneToSearchQuery(state, 'a')
   137  	AppendRuneToSearchQuery(state, 'b')
   138  	assert.Equal(t, "ab", buffer.search.query)
   139  
   140  	// Expect that to match the first position (wraparound back to start)
   141  	require.NotNil(t, buffer.search.match)
   142  	assert.Equal(t, uint64(0), buffer.search.match.StartPos)
   143  	assert.Equal(t, uint64(2), buffer.search.match.EndPos)
   144  }
   145  
   146  func TestSearchCaseSensitivity(t *testing.T) {
   147  	testCases := []struct {
   148  		name             string
   149  		text             string
   150  		query            string
   151  		expectedMatchPos uint64
   152  	}{
   153  		{
   154  			name:             "lowercase query, case-insensitive search",
   155  			text:             "abc Foo foo xyz",
   156  			query:            "foo",
   157  			expectedMatchPos: 4,
   158  		},
   159  		{
   160  			name:             "mixed-case query, case-sensitive search",
   161  			text:             "abc foo Foo xyz",
   162  			query:            "Foo",
   163  			expectedMatchPos: 8,
   164  		},
   165  		{
   166  			name:             "lowercase query, force case-sensitive search",
   167  			text:             "abc Foo foo xyz",
   168  			query:            "foo\\C",
   169  			expectedMatchPos: 8,
   170  		},
   171  		{
   172  			name:             "mixed-case query, force case-insensitive search",
   173  			text:             "abc Foo foo xyz",
   174  			query:            "FOO\\c",
   175  			expectedMatchPos: 4,
   176  		},
   177  	}
   178  
   179  	for _, tc := range testCases {
   180  		t.Run(tc.name, func(t *testing.T) {
   181  			textTree, err := text.NewTreeFromString(tc.text)
   182  			require.NoError(t, err)
   183  			state := NewEditorState(100, 100, nil, nil)
   184  			buffer := state.documentBuffer
   185  			buffer.textTree = textTree
   186  
   187  			StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   188  			for _, r := range tc.query {
   189  				AppendRuneToSearchQuery(state, r)
   190  			}
   191  			CompleteSearch(state, true)
   192  
   193  			assert.Equal(t, cursorState{position: tc.expectedMatchPos}, buffer.cursor)
   194  		})
   195  	}
   196  }
   197  
   198  func TestFindNextMatch(t *testing.T) {
   199  	testCases := []struct {
   200  		name              string
   201  		text              string
   202  		cursorPos         uint64
   203  		query             string
   204  		direction         SearchDirection
   205  		reverse           bool
   206  		expectedCursorPos uint64
   207  	}{
   208  		{
   209  			name:              "empty text",
   210  			text:              "",
   211  			cursorPos:         0,
   212  			query:             "abc",
   213  			direction:         SearchDirectionForward,
   214  			expectedCursorPos: 0,
   215  		},
   216  		{
   217  			name:              "find next after cursor",
   218  			text:              "foo bar baz",
   219  			cursorPos:         1,
   220  			query:             "ba",
   221  			direction:         SearchDirectionForward,
   222  			expectedCursorPos: 4,
   223  		},
   224  		{
   225  			name:              "find next after cursor already on match",
   226  			text:              "foo bar baz",
   227  			cursorPos:         4,
   228  			query:             "ba",
   229  			direction:         SearchDirectionForward,
   230  			expectedCursorPos: 8,
   231  		},
   232  		{
   233  			name:              "find next at end of text, not found in wraparound",
   234  			text:              "foo bar baz",
   235  			cursorPos:         10,
   236  			query:             "xa",
   237  			direction:         SearchDirectionForward,
   238  			expectedCursorPos: 10,
   239  		},
   240  		{
   241  			name:              "find next at end of text, found in wraparound",
   242  			text:              "foo bar baz",
   243  			cursorPos:         10,
   244  			query:             "ba",
   245  			direction:         SearchDirectionForward,
   246  			expectedCursorPos: 4,
   247  		},
   248  		{
   249  			name:              "find next with multi-byte unicode",
   250  			text:              "丂丄丅丆丏 ¢ह€한",
   251  			cursorPos:         0,
   252  			query:             "丅丆",
   253  			direction:         SearchDirectionForward,
   254  			expectedCursorPos: 2,
   255  		},
   256  		{
   257  			name:              "empty text, reverse search",
   258  			text:              "",
   259  			cursorPos:         0,
   260  			query:             "abc",
   261  			expectedCursorPos: 0,
   262  			direction:         SearchDirectionForward,
   263  			reverse:           true,
   264  		},
   265  		{
   266  			name:              "find prev",
   267  			text:              "foo bar baz xyz",
   268  			cursorPos:         14,
   269  			query:             "ba",
   270  			expectedCursorPos: 8,
   271  			direction:         SearchDirectionForward,
   272  			reverse:           true,
   273  		},
   274  		{
   275  			name:              "find prev from current match",
   276  			text:              "foo bar baz xyz",
   277  			cursorPos:         8,
   278  			query:             "ba",
   279  			direction:         SearchDirectionForward,
   280  			expectedCursorPos: 4,
   281  			reverse:           true,
   282  		},
   283  		{
   284  			name:              "find prev from middle of current match",
   285  			text:              "foo bar baz xyz",
   286  			cursorPos:         9,
   287  			query:             "ba",
   288  			direction:         SearchDirectionForward,
   289  			expectedCursorPos: 8,
   290  			reverse:           true,
   291  		},
   292  		{
   293  			name:              "find prev from start of text, not found in wraparound",
   294  			text:              "foo bar baz xyz",
   295  			cursorPos:         0,
   296  			query:             "lm",
   297  			direction:         SearchDirectionForward,
   298  			expectedCursorPos: 0,
   299  			reverse:           true,
   300  		},
   301  		{
   302  			name:              "find prev from start of text, found in wraparound",
   303  			text:              "foo bar baz xyz",
   304  			cursorPos:         0,
   305  			query:             "ba",
   306  			direction:         SearchDirectionForward,
   307  			expectedCursorPos: 8,
   308  			reverse:           true,
   309  		},
   310  		{
   311  			name:              "find prev with multi-byte unicode",
   312  			text:              "丂丄丅丆丏 ¢ह€한",
   313  			cursorPos:         9,
   314  			query:             "丅丆",
   315  			direction:         SearchDirectionForward,
   316  			expectedCursorPos: 2,
   317  			reverse:           true,
   318  		},
   319  		{
   320  			name:              "backward search equivalent to reverse forward search",
   321  			text:              "foo bar baz xyz",
   322  			cursorPos:         14,
   323  			query:             "ba",
   324  			direction:         SearchDirectionBackward,
   325  			expectedCursorPos: 8,
   326  			reverse:           false,
   327  		},
   328  		{
   329  			name:              "reverse backward search equivalent to forward search",
   330  			text:              "foo bar baz xyz",
   331  			cursorPos:         0,
   332  			query:             "ba",
   333  			direction:         SearchDirectionBackward,
   334  			expectedCursorPos: 4,
   335  			reverse:           true,
   336  		},
   337  		{
   338  			name:              "unicode normalization has different offsets",
   339  			text:              "<p>  &amp; © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸</p>\nfoobar",
   340  			cursorPos:         0,
   341  			query:             "foobar",
   342  			direction:         SearchDirectionForward,
   343  			expectedCursorPos: 32,
   344  		},
   345  	}
   346  
   347  	for _, tc := range testCases {
   348  		t.Run(tc.name, func(t *testing.T) {
   349  			textTree, err := text.NewTreeFromString(tc.text)
   350  			require.NoError(t, err)
   351  			state := NewEditorState(100, 100, nil, nil)
   352  			buffer := state.documentBuffer
   353  			buffer.textTree = textTree
   354  			buffer.cursor = cursorState{position: tc.cursorPos}
   355  			buffer.search.query = tc.query
   356  			buffer.search.direction = tc.direction
   357  			FindNextMatch(state, tc.reverse)
   358  			assert.Equal(t, tc.expectedCursorPos, buffer.cursor.position)
   359  		})
   360  	}
   361  }
   362  
   363  func TestSearchWordUnderCursor(t *testing.T) {
   364  	testCases := []struct {
   365  		name          string
   366  		inputText     string
   367  		direction     SearchDirection
   368  		count         uint64
   369  		pos           uint64
   370  		expectedQuery string
   371  		expectedPos   uint64
   372  	}{
   373  		{
   374  			name:          "empty",
   375  			inputText:     "",
   376  			direction:     SearchDirectionForward,
   377  			count:         1,
   378  			pos:           0,
   379  			expectedQuery: "",
   380  			expectedPos:   0,
   381  		},
   382  		{
   383  			name:          "start of word under cursor, search forward",
   384  			inputText:     "foo bar baz bar",
   385  			direction:     SearchDirectionForward,
   386  			count:         1,
   387  			pos:           4,
   388  			expectedQuery: "bar\\C",
   389  			expectedPos:   12,
   390  		},
   391  		{
   392  			name:          "word under cursor, search forward",
   393  			inputText:     "foo bar baz bar",
   394  			direction:     SearchDirectionForward,
   395  			count:         1,
   396  			pos:           5,
   397  			expectedQuery: "bar\\C",
   398  			expectedPos:   12,
   399  		},
   400  		{
   401  			name:          "word under cursor, search backward",
   402  			inputText:     "foo bar baz bar",
   403  			direction:     SearchDirectionForward,
   404  			count:         1,
   405  			pos:           14,
   406  			expectedQuery: "bar\\C",
   407  			expectedPos:   4,
   408  		},
   409  		{
   410  			name:          "whitespace before word",
   411  			inputText:     "foo   bar baz bar",
   412  			direction:     SearchDirectionForward,
   413  			count:         1,
   414  			pos:           3,
   415  			expectedQuery: "bar\\C",
   416  			expectedPos:   6, // differs from vim, which would advance to the next occurrence.
   417  		},
   418  		{
   419  			name:          "whitespace before end of line",
   420  			inputText:     "foo bar   \nbaz",
   421  			direction:     SearchDirectionForward,
   422  			count:         1,
   423  			pos:           9,
   424  			expectedQuery: "baz\\C", // differs from vim, which aborts.
   425  			expectedPos:   11,
   426  		},
   427  		{
   428  			name:          "search forward with count",
   429  			inputText:     "foo bar baz\nxyz\nfoo bar bat",
   430  			direction:     SearchDirectionForward,
   431  			count:         2,
   432  			pos:           1,
   433  			expectedQuery: "foo bar\\C",
   434  			expectedPos:   16,
   435  		},
   436  		{
   437  			name:          "search case sensitive",
   438  			inputText:     "foo bar FOO BAR bar",
   439  			direction:     SearchDirectionForward,
   440  			count:         1,
   441  			pos:           5,
   442  			expectedQuery: "bar\\C",
   443  			expectedPos:   16,
   444  		},
   445  	}
   446  
   447  	for _, tc := range testCases {
   448  		t.Run(tc.name, func(t *testing.T) {
   449  			textTree, err := text.NewTreeFromString(tc.inputText)
   450  			require.NoError(t, err)
   451  			state := NewEditorState(100, 100, nil, nil)
   452  			buffer := state.documentBuffer
   453  			buffer.textTree = textTree
   454  			buffer.cursor.position = tc.pos
   455  
   456  			// Search for the word under the cursor.
   457  			SearchWordUnderCursor(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch, tc.count)
   458  			assert.Equal(t, InputModeNormal, state.inputMode)
   459  			assert.Equal(t, tc.expectedQuery, buffer.search.query)
   460  			assert.Nil(t, buffer.search.match)
   461  			assert.Equal(t, cursorState{position: tc.expectedPos}, buffer.cursor)
   462  		})
   463  	}
   464  }
   465  
   466  func TestSearchForDelete(t *testing.T) {
   467  	testCases := []struct {
   468  		name         string
   469  		inputText    string
   470  		direction    SearchDirection
   471  		pos          uint64
   472  		query        string
   473  		expectedText string
   474  		expectedPos  uint64
   475  	}{
   476  		{
   477  			name:         "empty document",
   478  			inputText:    "",
   479  			direction:    SearchDirectionForward,
   480  			pos:          0,
   481  			query:        "abc",
   482  			expectedText: "",
   483  			expectedPos:  0,
   484  		},
   485  		{
   486  			name:         "no match, forward search",
   487  			inputText:    "abc def",
   488  			direction:    SearchDirectionForward,
   489  			pos:          0,
   490  			query:        "xyz",
   491  			expectedText: "abc def",
   492  			expectedPos:  0,
   493  		},
   494  		{
   495  			name:         "no match, backward search",
   496  			inputText:    "abc def",
   497  			direction:    SearchDirectionForward,
   498  			pos:          6,
   499  			query:        "xyz",
   500  			expectedText: "abc def",
   501  			expectedPos:  6,
   502  		},
   503  		{
   504  			name:         "match, forward search",
   505  			inputText:    "abc def xyz 123 xyz",
   506  			direction:    SearchDirectionForward,
   507  			pos:          2,
   508  			query:        "xyz",
   509  			expectedText: "abxyz 123 xyz",
   510  			expectedPos:  2,
   511  		},
   512  		{
   513  			name:         "match, backward search",
   514  			inputText:    "abc def xyz 123 xyz abc",
   515  			direction:    SearchDirectionBackward,
   516  			pos:          22,
   517  			query:        "xyz",
   518  			expectedText: "abc def xyz 123 xyzc",
   519  			expectedPos:  19,
   520  		},
   521  		{
   522  			name:         "match, forward search, skip match on cursor",
   523  			inputText:    "abc 123 abc 456 abc 789",
   524  			direction:    SearchDirectionForward,
   525  			pos:          0,
   526  			query:        "abc",
   527  			expectedText: "abc 456 abc 789",
   528  			expectedPos:  0,
   529  		},
   530  		{
   531  			name:         "match, forward search, wraparound",
   532  			inputText:    "abc 123 xyz 456",
   533  			direction:    SearchDirectionForward,
   534  			pos:          13,
   535  			query:        "bc",
   536  			expectedText: "a56",
   537  			expectedPos:  1,
   538  		},
   539  		{
   540  			name:         "match, backward search, wraparound",
   541  			inputText:    "abc 123 xyz 456",
   542  			direction:    SearchDirectionBackward,
   543  			pos:          2,
   544  			query:        "yz",
   545  			expectedText: "abyz 456",
   546  			expectedPos:  2,
   547  		},
   548  	}
   549  
   550  	for _, tc := range testCases {
   551  		t.Run(tc.name, func(t *testing.T) {
   552  			textTree, err := text.NewTreeFromString(tc.inputText)
   553  			require.NoError(t, err)
   554  			state := NewEditorState(100, 100, nil, nil)
   555  			buffer := state.documentBuffer
   556  			buffer.textTree = textTree
   557  			buffer.cursor.position = tc.pos
   558  
   559  			// Search for the query, with a complete action to delete to the match.
   560  			StartSearch(state, tc.direction, SearchCompleteDeleteToMatch(clipboard.PageNull))
   561  			for _, r := range tc.query {
   562  				AppendRuneToSearchQuery(state, r)
   563  			}
   564  			CompleteSearch(state, true)
   565  
   566  			assert.Equal(t, InputModeNormal, state.inputMode)
   567  			assert.Equal(t, tc.expectedPos, buffer.cursor.position)
   568  			assert.Equal(t, tc.expectedText, textTree.String())
   569  		})
   570  	}
   571  }
   572  
   573  func TestSearchForDeleteAndRepeatLastAction(t *testing.T) {
   574  	textTree, err := text.NewTreeFromString("abc xyz 123\nabc xyz 123\nabc xyz 123")
   575  	require.NoError(t, err)
   576  	state := NewEditorState(100, 100, nil, nil)
   577  	buffer := state.documentBuffer
   578  	buffer.textTree = textTree
   579  	buffer.cursor.position = 0
   580  
   581  	// Search for the query, with a complete action to delete to the match.
   582  	StartSearch(state, SearchDirectionForward, SearchCompleteDeleteToMatch(clipboard.PageNull))
   583  	for _, r := range "xyz" {
   584  		AppendRuneToSearchQuery(state, r)
   585  	}
   586  	CompleteSearch(state, true)
   587  	assert.Equal(t, InputModeNormal, state.inputMode)
   588  	assert.Equal(t, uint64(0), buffer.cursor.position)
   589  	assert.Equal(t, "xyz 123\nabc xyz 123\nabc xyz 123", textTree.String())
   590  
   591  	// Change the search query. This shouldn't affect the last action macro.
   592  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   593  	for _, r := range "abc" {
   594  		AppendRuneToSearchQuery(state, r)
   595  	}
   596  	CompleteSearch(state, true)
   597  	assert.Equal(t, InputModeNormal, state.inputMode)
   598  	assert.Equal(t, uint64(8), buffer.cursor.position)
   599  
   600  	// Repeat the last action.
   601  	ReplayLastActionMacro(state, 1)
   602  	assert.Equal(t, InputModeNormal, state.inputMode)
   603  	assert.Equal(t, uint64(8), buffer.cursor.position)
   604  	assert.Equal(t, "xyz 123\nxyz 123\nabc xyz 123", textTree.String())
   605  
   606  	// And again!
   607  	ReplayLastActionMacro(state, 1)
   608  	assert.Equal(t, InputModeNormal, state.inputMode)
   609  	assert.Equal(t, uint64(8), buffer.cursor.position)
   610  	assert.Equal(t, "xyz 123\nxyz 123", textTree.String())
   611  }
   612  
   613  func TestSearchForChange(t *testing.T) {
   614  	textTree, err := text.NewTreeFromString("abc xyz 123\nabc xyz 123\nabc xyz 123")
   615  	require.NoError(t, err)
   616  	state := NewEditorState(100, 100, nil, nil)
   617  	buffer := state.documentBuffer
   618  	buffer.textTree = textTree
   619  	buffer.cursor.position = 0
   620  
   621  	// Search for the query, with a complete action to change to the match.
   622  	StartSearch(state, SearchDirectionForward, SearchCompleteChangeToMatch(clipboard.PageNull))
   623  	for _, r := range "xyz" {
   624  		AppendRuneToSearchQuery(state, r)
   625  	}
   626  	CompleteSearch(state, true)
   627  	assert.Equal(t, InputModeInsert, state.inputMode) // Since it's a change, go to insert mode.
   628  	assert.Equal(t, uint64(0), buffer.cursor.position)
   629  	assert.Equal(t, "xyz 123\nabc xyz 123\nabc xyz 123", textTree.String())
   630  }
   631  
   632  func TestSearchForCopy(t *testing.T) {
   633  	testCases := []struct {
   634  		name                  string
   635  		inputText             string
   636  		direction             SearchDirection
   637  		pos                   uint64
   638  		query                 string
   639  		expectedClipboardText string
   640  	}{
   641  		{
   642  			name:                  "empty document",
   643  			inputText:             "",
   644  			direction:             SearchDirectionForward,
   645  			pos:                   0,
   646  			query:                 "abc",
   647  			expectedClipboardText: "",
   648  		},
   649  		{
   650  			name:                  "no match, forward search",
   651  			inputText:             "abc def",
   652  			direction:             SearchDirectionForward,
   653  			pos:                   0,
   654  			query:                 "xyz",
   655  			expectedClipboardText: "",
   656  		},
   657  		{
   658  			name:                  "no match, backward search",
   659  			inputText:             "abc def",
   660  			direction:             SearchDirectionForward,
   661  			pos:                   6,
   662  			query:                 "xyz",
   663  			expectedClipboardText: "",
   664  		},
   665  		{
   666  			name:                  "match, forward search",
   667  			inputText:             "abc def xyz 123 xyz",
   668  			direction:             SearchDirectionForward,
   669  			pos:                   2,
   670  			query:                 "xyz",
   671  			expectedClipboardText: "c def ",
   672  		},
   673  		{
   674  			name:                  "match, backward search",
   675  			inputText:             "abc def xyz 123 xyz abc",
   676  			direction:             SearchDirectionBackward,
   677  			pos:                   22,
   678  			query:                 "xyz",
   679  			expectedClipboardText: " ab",
   680  		},
   681  		{
   682  			name:                  "match, forward search, skip match on cursor",
   683  			inputText:             "abc 123 abc 456 abc 789",
   684  			direction:             SearchDirectionForward,
   685  			pos:                   0,
   686  			query:                 "abc",
   687  			expectedClipboardText: "abc 123 ",
   688  		},
   689  		{
   690  			name:                  "match, forward search, wraparound",
   691  			inputText:             "abc 123 xyz 456",
   692  			direction:             SearchDirectionForward,
   693  			pos:                   13,
   694  			query:                 "bc",
   695  			expectedClipboardText: "",
   696  		},
   697  		{
   698  			name:                  "match, backward search, wraparound",
   699  			inputText:             "abc 123 xyz 456",
   700  			direction:             SearchDirectionBackward,
   701  			pos:                   2,
   702  			query:                 "yz",
   703  			expectedClipboardText: "",
   704  		},
   705  	}
   706  
   707  	for _, tc := range testCases {
   708  		t.Run(tc.name, func(t *testing.T) {
   709  			textTree, err := text.NewTreeFromString(tc.inputText)
   710  			require.NoError(t, err)
   711  			state := NewEditorState(100, 100, nil, nil)
   712  			buffer := state.documentBuffer
   713  			buffer.textTree = textTree
   714  			buffer.cursor.position = tc.pos
   715  
   716  			// Search for the query, with a complete action to copy to the match.
   717  			StartSearch(state, tc.direction, SearchCompleteCopyToMatch(clipboard.PageDefault))
   718  			for _, r := range tc.query {
   719  				AppendRuneToSearchQuery(state, r)
   720  			}
   721  			CompleteSearch(state, true)
   722  
   723  			// Back to normal mode, no change in cursor or document.
   724  			assert.Equal(t, InputModeNormal, state.inputMode)
   725  			assert.Equal(t, tc.pos, buffer.cursor.position)
   726  			assert.Equal(t, tc.inputText, textTree.String())
   727  
   728  			// Check clipboard state.
   729  			page := state.clipboard.Get(clipboard.PageDefault)
   730  			assert.False(t, page.Linewise)
   731  			assert.Equal(t, tc.expectedClipboardText, page.Text)
   732  		})
   733  	}
   734  }
   735  
   736  func TestSetSearchQueryToPrevInHistory(t *testing.T) {
   737  	textTree, err := text.NewTreeFromString("x abc def ghi")
   738  	require.NoError(t, err)
   739  	state := NewEditorState(100, 100, nil, nil)
   740  	buffer := state.documentBuffer
   741  	buffer.textTree = textTree
   742  
   743  	// First search query, aborted.
   744  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   745  	for _, r := range "abc" {
   746  		AppendRuneToSearchQuery(state, r)
   747  	}
   748  	CompleteSearch(state, false)
   749  
   750  	// Second search query, committed.
   751  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   752  	for _, r := range "def" {
   753  		AppendRuneToSearchQuery(state, r)
   754  	}
   755  	CompleteSearch(state, true)
   756  
   757  	// Start a search, go back in history.
   758  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   759  	SetSearchQueryToPrevInHistory(state)
   760  	assert.Equal(t, "def", buffer.search.query)
   761  	require.NotNil(t, buffer.search.match)
   762  	assert.Equal(t, uint64(6), buffer.search.match.StartPos)
   763  
   764  	// Go back in the history again.
   765  	SetSearchQueryToPrevInHistory(state)
   766  	assert.Equal(t, "abc", buffer.search.query)
   767  	require.NotNil(t, buffer.search.match)
   768  	assert.Equal(t, uint64(2), buffer.search.match.StartPos)
   769  
   770  	// Go back in the history, no previous entry so no change.
   771  	SetSearchQueryToPrevInHistory(state)
   772  	assert.Equal(t, "abc", buffer.search.query)
   773  	require.NotNil(t, buffer.search.match)
   774  	assert.Equal(t, uint64(2), buffer.search.match.StartPos)
   775  }
   776  
   777  func TestSetSearchQueryToNextInHistory(t *testing.T) {
   778  	textTree, err := text.NewTreeFromString("x abc def ghi")
   779  	require.NoError(t, err)
   780  	state := NewEditorState(100, 100, nil, nil)
   781  	buffer := state.documentBuffer
   782  	buffer.textTree = textTree
   783  
   784  	// First search query, aborted.
   785  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   786  	for _, r := range "abc" {
   787  		AppendRuneToSearchQuery(state, r)
   788  	}
   789  	CompleteSearch(state, false)
   790  
   791  	// Second search query, committed.
   792  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   793  	for _, r := range "def" {
   794  		AppendRuneToSearchQuery(state, r)
   795  	}
   796  	CompleteSearch(state, true)
   797  
   798  	// Go back to beginning of history.
   799  	SetSearchQueryToPrevInHistory(state)
   800  	SetSearchQueryToPrevInHistory(state)
   801  	assert.Equal(t, "abc", buffer.search.query)
   802  	require.NotNil(t, buffer.search.match)
   803  	assert.Equal(t, uint64(2), buffer.search.match.StartPos)
   804  
   805  	// Go to next in history.
   806  	SetSearchQueryToNextInHistory(state)
   807  	assert.Equal(t, "def", buffer.search.query)
   808  	require.NotNil(t, buffer.search.match)
   809  	assert.Equal(t, uint64(6), buffer.search.match.StartPos)
   810  
   811  	// Forward again. No future entry, so no change.
   812  	SetSearchQueryToNextInHistory(state)
   813  	assert.Equal(t, "def", buffer.search.query)
   814  	require.NotNil(t, buffer.search.match)
   815  	assert.Equal(t, uint64(6), buffer.search.match.StartPos)
   816  }
   817  
   818  func TestSearchQueryToPrevInHistoryThenAppendRunes(t *testing.T) {
   819  	textTree, err := text.NewTreeFromString("x abc def ghi")
   820  	require.NoError(t, err)
   821  	state := NewEditorState(100, 100, nil, nil)
   822  	buffer := state.documentBuffer
   823  	buffer.textTree = textTree
   824  
   825  	// First search query, aborted.
   826  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   827  	for _, r := range "abc" {
   828  		AppendRuneToSearchQuery(state, r)
   829  	}
   830  	CompleteSearch(state, false)
   831  
   832  	// Second search query, committed.
   833  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   834  	for _, r := range "def" {
   835  		AppendRuneToSearchQuery(state, r)
   836  	}
   837  	CompleteSearch(state, true)
   838  
   839  	// Start a search, go back to beginning of history.
   840  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   841  	SetSearchQueryToPrevInHistory(state)
   842  	SetSearchQueryToPrevInHistory(state)
   843  	assert.Equal(t, "abc", buffer.search.query)
   844  	require.NotNil(t, buffer.search.match)
   845  	assert.Equal(t, uint64(2), buffer.search.match.StartPos)
   846  
   847  	// Edit the query by appending runes.
   848  	AppendRuneToSearchQuery(state, 'x')
   849  	AppendRuneToSearchQuery(state, 'y')
   850  	AppendRuneToSearchQuery(state, 'z')
   851  	assert.Equal(t, "abcxyz", buffer.search.query)
   852  	assert.Nil(t, buffer.search.match)
   853  
   854  	// Go back in history, confirm that the edit reset to the last entry.
   855  	SetSearchQueryToPrevInHistory(state)
   856  	assert.Equal(t, "def", buffer.search.query)
   857  	require.NotNil(t, buffer.search.match)
   858  	assert.Equal(t, uint64(6), buffer.search.match.StartPos)
   859  }
   860  
   861  func TestSearchQueryToPrevInHistoryThenDeleteRunes(t *testing.T) {
   862  	textTree, err := text.NewTreeFromString("x abc def ghi")
   863  	require.NoError(t, err)
   864  	state := NewEditorState(100, 100, nil, nil)
   865  	buffer := state.documentBuffer
   866  	buffer.textTree = textTree
   867  
   868  	// First search query, aborted.
   869  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   870  	for _, r := range "abc" {
   871  		AppendRuneToSearchQuery(state, r)
   872  	}
   873  	CompleteSearch(state, false)
   874  
   875  	// Second search query, committed.
   876  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   877  	for _, r := range "def" {
   878  		AppendRuneToSearchQuery(state, r)
   879  	}
   880  	CompleteSearch(state, true)
   881  
   882  	// Start a search, go back to beginning of history.
   883  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   884  	SetSearchQueryToPrevInHistory(state)
   885  	SetSearchQueryToPrevInHistory(state)
   886  	assert.Equal(t, "abc", buffer.search.query)
   887  	require.NotNil(t, buffer.search.match)
   888  	assert.Equal(t, uint64(2), buffer.search.match.StartPos)
   889  
   890  	// Edit the query by deleting runes.
   891  	DeleteRuneFromSearchQuery(state)
   892  	DeleteRuneFromSearchQuery(state)
   893  	assert.Equal(t, "a", buffer.search.query)
   894  	require.NotNil(t, buffer.search.match)
   895  	assert.Equal(t, uint64(2), buffer.search.match.StartPos)
   896  
   897  	// Go back in history, confirm that the edit reset to the last entry.
   898  	SetSearchQueryToPrevInHistory(state)
   899  	assert.Equal(t, "def", buffer.search.query)
   900  	require.NotNil(t, buffer.search.match)
   901  	assert.Equal(t, uint64(6), buffer.search.match.StartPos)
   902  }
   903  
   904  func TestSearchQueryHistoryExcludesEmptyQueries(t *testing.T) {
   905  	textTree, err := text.NewTreeFromString("x abc def ghi")
   906  	require.NoError(t, err)
   907  	state := NewEditorState(100, 100, nil, nil)
   908  	buffer := state.documentBuffer
   909  	buffer.textTree = textTree
   910  
   911  	// First search query.
   912  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   913  	for _, r := range "abc" {
   914  		AppendRuneToSearchQuery(state, r)
   915  	}
   916  	CompleteSearch(state, false)
   917  
   918  	// Several empty search queries, should not be added to history.
   919  	for i := 0; i < 3; i++ {
   920  		StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   921  		CompleteSearch(state, false)
   922  	}
   923  
   924  	// Start a search, back to previous entry.
   925  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   926  	SetSearchQueryToPrevInHistory(state)
   927  	assert.Equal(t, "abc", buffer.search.query)
   928  	require.NotNil(t, buffer.search.match)
   929  	assert.Equal(t, uint64(2), buffer.search.match.StartPos)
   930  }
   931  
   932  func TestSearchQueryHistoryExcludesDuplicateQueries(t *testing.T) {
   933  	textTree, err := text.NewTreeFromString("x abc def ghi")
   934  	require.NoError(t, err)
   935  	state := NewEditorState(100, 100, nil, nil)
   936  	buffer := state.documentBuffer
   937  	buffer.textTree = textTree
   938  
   939  	// First search query.
   940  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   941  	for _, r := range "abc" {
   942  		AppendRuneToSearchQuery(state, r)
   943  	}
   944  	CompleteSearch(state, false)
   945  
   946  	// Second search query.
   947  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   948  	for _, r := range "def" {
   949  		AppendRuneToSearchQuery(state, r)
   950  	}
   951  	CompleteSearch(state, false)
   952  
   953  	// Repeat the query several times.
   954  	for i := 0; i < 3; i++ {
   955  		StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   956  		for _, r := range "def" {
   957  			AppendRuneToSearchQuery(state, r)
   958  		}
   959  		CompleteSearch(state, false)
   960  	}
   961  
   962  	// Start a search, back to previous entry.
   963  	StartSearch(state, SearchDirectionForward, SearchCompleteMoveCursorToMatch)
   964  	SetSearchQueryToPrevInHistory(state)
   965  	assert.Equal(t, "def", buffer.search.query)
   966  	require.NotNil(t, buffer.search.match)
   967  	assert.Equal(t, uint64(6), buffer.search.match.StartPos)
   968  
   969  	// Back again, expect that we're at the first entry (duplicate entries were excluded from history).
   970  	SetSearchQueryToPrevInHistory(state)
   971  	assert.Equal(t, "abc", buffer.search.query)
   972  	require.NotNil(t, buffer.search.match)
   973  	assert.Equal(t, uint64(2), buffer.search.match.StartPos)
   974  }