github.com/aretext/aretext@v1.3.0/state/edit_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/locate"
    11  	"github.com/aretext/aretext/selection"
    12  	"github.com/aretext/aretext/text"
    13  )
    14  
    15  func TestInsertRune(t *testing.T) {
    16  	testCases := []struct {
    17  		name           string
    18  		inputString    string
    19  		initialCursor  cursorState
    20  		insertRune     rune
    21  		expectedCursor cursorState
    22  		expectedText   string
    23  	}{
    24  		{
    25  			name:           "insert into empty string",
    26  			inputString:    "",
    27  			initialCursor:  cursorState{position: 0},
    28  			insertRune:     'x',
    29  			expectedCursor: cursorState{position: 1},
    30  			expectedText:   "x",
    31  		},
    32  		{
    33  			name:           "insert in middle of string",
    34  			inputString:    "abcd",
    35  			initialCursor:  cursorState{position: 1},
    36  			insertRune:     'x',
    37  			expectedCursor: cursorState{position: 2},
    38  			expectedText:   "axbcd",
    39  		},
    40  		{
    41  			name:           "insert at end of string",
    42  			inputString:    "abcd",
    43  			initialCursor:  cursorState{position: 4},
    44  			insertRune:     'x',
    45  			expectedCursor: cursorState{position: 5},
    46  			expectedText:   "abcdx",
    47  		},
    48  	}
    49  
    50  	for _, tc := range testCases {
    51  		t.Run(tc.name, func(t *testing.T) {
    52  			textTree, err := text.NewTreeFromString(tc.inputString)
    53  			require.NoError(t, err)
    54  			state := NewEditorState(100, 100, nil, nil)
    55  			state.documentBuffer.textTree = textTree
    56  			state.documentBuffer.cursor = tc.initialCursor
    57  			InsertRune(state, tc.insertRune)
    58  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
    59  			assert.Equal(t, tc.expectedText, textTree.String())
    60  		})
    61  	}
    62  }
    63  
    64  func TestInsertText(t *testing.T) {
    65  	testCases := []struct {
    66  		name           string
    67  		inputString    string
    68  		initialCursor  cursorState
    69  		insertText     string
    70  		expectedCursor cursorState
    71  		expectedText   string
    72  	}{
    73  		{
    74  			name:           "empty",
    75  			initialCursor:  cursorState{position: 0},
    76  			inputString:    "",
    77  			insertText:     "",
    78  			expectedCursor: cursorState{position: 0},
    79  			expectedText:   "",
    80  		},
    81  		{
    82  			name:           "ascii",
    83  			initialCursor:  cursorState{position: 1},
    84  			inputString:    "abc",
    85  			insertText:     "xyz",
    86  			expectedCursor: cursorState{position: 4},
    87  			expectedText:   "axyzbc",
    88  		},
    89  		{
    90  			name:           "non-ascii unicode with multi-byte runes",
    91  			initialCursor:  cursorState{position: 1},
    92  			inputString:    "abc",
    93  			insertText:     "丂丄丅丆丏 ¢ह€한",
    94  			expectedCursor: cursorState{position: 11},
    95  			expectedText:   "a丂丄丅丆丏 ¢ह€한bc",
    96  		},
    97  	}
    98  
    99  	for _, tc := range testCases {
   100  		t.Run(tc.name, func(t *testing.T) {
   101  			textTree, err := text.NewTreeFromString(tc.inputString)
   102  			require.NoError(t, err)
   103  			state := NewEditorState(100, 100, nil, nil)
   104  			state.documentBuffer.textTree = textTree
   105  			state.documentBuffer.cursor = tc.initialCursor
   106  			InsertText(state, tc.insertText)
   107  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
   108  			assert.Equal(t, tc.expectedText, textTree.String())
   109  		})
   110  	}
   111  }
   112  
   113  func TestDeleteToPos(t *testing.T) {
   114  	testCases := []struct {
   115  		name              string
   116  		inputString       string
   117  		initialCursor     cursorState
   118  		locator           func(LocatorParams) uint64
   119  		expectedCursor    cursorState
   120  		expectedText      string
   121  		expectedClipboard clipboard.PageContent
   122  	}{
   123  		{
   124  			name:          "delete from empty string",
   125  			inputString:   "",
   126  			initialCursor: cursorState{position: 0},
   127  			locator: func(params LocatorParams) uint64 {
   128  				return locate.NextCharInLine(params.TextTree, 1, true, params.CursorPos)
   129  			},
   130  			expectedCursor: cursorState{position: 0},
   131  			expectedText:   "",
   132  		},
   133  		{
   134  			name:          "delete next character at start of string",
   135  			inputString:   "abcd",
   136  			initialCursor: cursorState{position: 0},
   137  			locator: func(params LocatorParams) uint64 {
   138  				return locate.NextCharInLine(params.TextTree, 1, true, params.CursorPos)
   139  			},
   140  			expectedCursor:    cursorState{position: 0},
   141  			expectedText:      "bcd",
   142  			expectedClipboard: clipboard.PageContent{Text: "a"},
   143  		},
   144  		{
   145  			name:          "delete from end of text",
   146  			inputString:   "abcd",
   147  			initialCursor: cursorState{position: 3},
   148  			locator: func(params LocatorParams) uint64 {
   149  				return locate.NextCharInLine(params.TextTree, 1, true, params.CursorPos)
   150  			},
   151  			expectedCursor:    cursorState{position: 3},
   152  			expectedText:      "abc",
   153  			expectedClipboard: clipboard.PageContent{Text: "d"},
   154  		},
   155  		{
   156  			name:          "delete multiple characters",
   157  			inputString:   "abcd",
   158  			initialCursor: cursorState{position: 1},
   159  			locator: func(params LocatorParams) uint64 {
   160  				return locate.NextCharInLine(params.TextTree, 10, true, params.CursorPos)
   161  			},
   162  			expectedCursor:    cursorState{position: 1},
   163  			expectedText:      "a",
   164  			expectedClipboard: clipboard.PageContent{Text: "bcd"},
   165  		},
   166  	}
   167  
   168  	for _, tc := range testCases {
   169  		t.Run(tc.name, func(t *testing.T) {
   170  			textTree, err := text.NewTreeFromString(tc.inputString)
   171  			require.NoError(t, err)
   172  			state := NewEditorState(100, 100, nil, nil)
   173  			state.documentBuffer.textTree = textTree
   174  			state.documentBuffer.cursor = tc.initialCursor
   175  			DeleteToPos(state, tc.locator, clipboard.PageDefault)
   176  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
   177  			assert.Equal(t, tc.expectedText, textTree.String())
   178  			assert.Equal(t, tc.expectedClipboard, state.clipboard.Get(clipboard.PageDefault))
   179  		})
   180  	}
   181  }
   182  
   183  func TestInsertNewline(t *testing.T) {
   184  	testCases := []struct {
   185  		name              string
   186  		inputString       string
   187  		autoIndent        bool
   188  		cursorPos         uint64
   189  		tabExpand         bool
   190  		expectedCursorPos uint64
   191  		expectedText      string
   192  	}{
   193  		{
   194  			name:              "empty document, autoindent disabled",
   195  			inputString:       "",
   196  			cursorPos:         0,
   197  			expectedCursorPos: 1,
   198  			expectedText:      "\n",
   199  		},
   200  		{
   201  			name:              "single line, autoindent disabled, no indentation",
   202  			inputString:       "abcd",
   203  			cursorPos:         2,
   204  			expectedCursorPos: 3,
   205  			expectedText:      "ab\ncd",
   206  		},
   207  		{
   208  			name:              "single line, autoindent disabled, with indentation",
   209  			inputString:       "\tabcd",
   210  			cursorPos:         3,
   211  			expectedCursorPos: 4,
   212  			expectedText:      "\tab\ncd",
   213  		},
   214  		{
   215  			name:              "single line, autoindent enabled, no indentation",
   216  			inputString:       "abcd",
   217  			autoIndent:        true,
   218  			cursorPos:         2,
   219  			expectedCursorPos: 3,
   220  			expectedText:      "ab\ncd",
   221  		},
   222  		{
   223  			name:              "single line, autoindent enabled, tab indentation",
   224  			inputString:       "\tabcd",
   225  			autoIndent:        true,
   226  			cursorPos:         3,
   227  			expectedCursorPos: 5,
   228  			expectedText:      "\tab\n\tcd",
   229  		},
   230  		{
   231  			name:              "single line, autoindent enabled, space indentation",
   232  			inputString:       "    abcd",
   233  			autoIndent:        true,
   234  			cursorPos:         6,
   235  			expectedCursorPos: 8,
   236  			expectedText:      "    ab\n\tcd",
   237  		},
   238  		{
   239  			name:              "single line, autoindent enabled, mixed tabs and spaces aligned indentation",
   240  			inputString:       " \tabcd",
   241  			autoIndent:        true,
   242  			cursorPos:         4,
   243  			expectedCursorPos: 6,
   244  			expectedText:      " \tab\n\tcd",
   245  		},
   246  		{
   247  			name:              "single line, autoindent enabled, mixed tabs and spaces misaligned indentation",
   248  			inputString:       "\t abcd",
   249  			autoIndent:        true,
   250  			cursorPos:         4,
   251  			expectedCursorPos: 7,
   252  			expectedText:      "\t ab\n\t cd",
   253  		},
   254  		{
   255  			name:              "expand tab inserts spaces",
   256  			inputString:       "    abcd",
   257  			autoIndent:        true,
   258  			tabExpand:         true,
   259  			cursorPos:         8,
   260  			expectedCursorPos: 13,
   261  			expectedText:      "    abcd\n    ",
   262  		},
   263  		{
   264  			name:              "dedent if extra whitespace at end of current line",
   265  			inputString:       "    abcd        xyz",
   266  			autoIndent:        true,
   267  			tabExpand:         true,
   268  			cursorPos:         8,
   269  			expectedCursorPos: 13,
   270  			expectedText:      "    abcd\n    xyz",
   271  		},
   272  	}
   273  
   274  	for _, tc := range testCases {
   275  		t.Run(tc.name, func(t *testing.T) {
   276  			textTree, err := text.NewTreeFromString(tc.inputString)
   277  			require.NoError(t, err)
   278  			state := NewEditorState(100, 100, nil, nil)
   279  			state.documentBuffer.textTree = textTree
   280  			state.documentBuffer.cursor = cursorState{position: tc.cursorPos}
   281  			state.documentBuffer.autoIndent = tc.autoIndent
   282  			state.documentBuffer.tabSize = 4
   283  			state.documentBuffer.tabExpand = tc.tabExpand
   284  			InsertNewline(state)
   285  			assert.Equal(t, cursorState{position: tc.expectedCursorPos}, state.documentBuffer.cursor)
   286  			assert.Equal(t, tc.expectedText, textTree.String())
   287  		})
   288  	}
   289  }
   290  
   291  func TestClearAutoIndentWhitespaceLine(t *testing.T) {
   292  	testCases := []struct {
   293  		name              string
   294  		inputString       string
   295  		cursorPos         uint64
   296  		targetLinePos     uint64
   297  		expectedText      string
   298  		expectedCursorPos uint64
   299  	}{
   300  		{
   301  			name:              "empty",
   302  			inputString:       "",
   303  			cursorPos:         0,
   304  			targetLinePos:     0,
   305  			expectedText:      "",
   306  			expectedCursorPos: 0,
   307  		},
   308  		{
   309  			name:              "line with non-whitespace chars",
   310  			inputString:       "    abc",
   311  			cursorPos:         0,
   312  			targetLinePos:     0,
   313  			expectedText:      "    abc",
   314  			expectedCursorPos: 0,
   315  		},
   316  		{
   317  			name:              "line with only spaces",
   318  			inputString:       "    ",
   319  			cursorPos:         1,
   320  			targetLinePos:     0,
   321  			expectedText:      "",
   322  			expectedCursorPos: 0,
   323  		},
   324  		{
   325  			name:              "line with only tabs",
   326  			inputString:       "\t\t",
   327  			cursorPos:         1,
   328  			targetLinePos:     0,
   329  			expectedText:      "",
   330  			expectedCursorPos: 0,
   331  		},
   332  		{
   333  			name:              "cursor after target line",
   334  			inputString:       "    ab\n    \n    cd",
   335  			cursorPos:         17,
   336  			targetLinePos:     7,
   337  			expectedText:      "    ab\n\n    cd",
   338  			expectedCursorPos: 13,
   339  		},
   340  		{
   341  			name:              "cursor before target line",
   342  			inputString:       "    ab\n    \n    cd",
   343  			cursorPos:         5,
   344  			targetLinePos:     7,
   345  			expectedText:      "    ab\n\n    cd",
   346  			expectedCursorPos: 5,
   347  		},
   348  		{
   349  			name:              "cursor on target line",
   350  			inputString:       "    ab\n    \n    cd",
   351  			cursorPos:         9,
   352  			targetLinePos:     7,
   353  			expectedText:      "    ab\n\n    cd",
   354  			expectedCursorPos: 7,
   355  		},
   356  	}
   357  
   358  	for _, tc := range testCases {
   359  		t.Run(tc.name, func(t *testing.T) {
   360  			textTree, err := text.NewTreeFromString(tc.inputString)
   361  			require.NoError(t, err)
   362  			state := NewEditorState(100, 100, nil, nil)
   363  			state.documentBuffer.textTree = textTree
   364  			state.documentBuffer.cursor = cursorState{position: tc.cursorPos}
   365  			state.documentBuffer.autoIndent = true
   366  			ClearAutoIndentWhitespaceLine(state, func(p LocatorParams) uint64 {
   367  				return locate.StartOfLineAtPos(p.TextTree, tc.targetLinePos)
   368  			})
   369  			assert.Equal(t, cursorState{position: tc.expectedCursorPos}, state.documentBuffer.cursor)
   370  			assert.Equal(t, tc.expectedText, textTree.String())
   371  		})
   372  	}
   373  }
   374  
   375  func TestInsertTab(t *testing.T) {
   376  	testCases := []struct {
   377  		name           string
   378  		inputString    string
   379  		initialCursor  cursorState
   380  		expectedText   string
   381  		expectedCursor cursorState
   382  		tabExpand      bool
   383  	}{
   384  		{
   385  			name:           "insert tab, no expand",
   386  			inputString:    "abcd",
   387  			initialCursor:  cursorState{position: 2},
   388  			expectedText:   "ab\tcd",
   389  			expectedCursor: cursorState{position: 3},
   390  		},
   391  		{
   392  			name:           "insert tab, expand full width",
   393  			tabExpand:      true,
   394  			inputString:    "abcd",
   395  			initialCursor:  cursorState{position: 0},
   396  			expectedText:   "    abcd",
   397  			expectedCursor: cursorState{position: 4},
   398  		},
   399  		{
   400  			name:           "insert tab, partial width",
   401  			tabExpand:      true,
   402  			inputString:    "abcd",
   403  			initialCursor:  cursorState{position: 2},
   404  			expectedText:   "ab  cd",
   405  			expectedCursor: cursorState{position: 4},
   406  		},
   407  		{
   408  			name:           "insert tab, expand with mixed tabs/spaces",
   409  			tabExpand:      true,
   410  			inputString:    "\t\tab",
   411  			initialCursor:  cursorState{position: 2},
   412  			expectedText:   "\t\t    ab",
   413  			expectedCursor: cursorState{position: 6},
   414  		},
   415  	}
   416  
   417  	for _, tc := range testCases {
   418  		t.Run(tc.name, func(t *testing.T) {
   419  			textTree, err := text.NewTreeFromString(tc.inputString)
   420  			require.NoError(t, err)
   421  			state := NewEditorState(100, 100, nil, nil)
   422  			state.documentBuffer.textTree = textTree
   423  			state.documentBuffer.cursor = tc.initialCursor
   424  			state.documentBuffer.tabSize = 4
   425  			state.documentBuffer.tabExpand = tc.tabExpand
   426  			InsertTab(state)
   427  			assert.Equal(t, tc.expectedText, textTree.String())
   428  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
   429  		})
   430  	}
   431  }
   432  
   433  func TestDeleteLines(t *testing.T) {
   434  	testCases := []struct {
   435  		name                       string
   436  		inputString                string
   437  		initialCursor              cursorState
   438  		targetLineLocator          func(LocatorParams) uint64
   439  		abortIfTargetIsCurrentLine bool
   440  		replaceWithEmptyLine       bool
   441  		expectedCursor             cursorState
   442  		expectedText               string
   443  		expectedClipboard          clipboard.PageContent
   444  	}{
   445  		{
   446  			name:          "empty",
   447  			inputString:   "",
   448  			initialCursor: cursorState{position: 0},
   449  			targetLineLocator: func(params LocatorParams) uint64 {
   450  				return locate.StartOfLineBelow(params.TextTree, 1, params.CursorPos)
   451  			},
   452  			expectedCursor: cursorState{position: 0},
   453  			expectedText:   "",
   454  		},
   455  		{
   456  			name:          "delete single line",
   457  			inputString:   "abcd",
   458  			initialCursor: cursorState{position: 2},
   459  			targetLineLocator: func(params LocatorParams) uint64 {
   460  				return params.CursorPos
   461  			},
   462  			expectedCursor: cursorState{position: 0},
   463  			expectedText:   "",
   464  			expectedClipboard: clipboard.PageContent{
   465  				Text:     "abcd",
   466  				Linewise: true,
   467  			},
   468  		},
   469  		{
   470  			name:          "delete single line, abort if same line",
   471  			inputString:   "abcd",
   472  			initialCursor: cursorState{position: 2},
   473  			targetLineLocator: func(params LocatorParams) uint64 {
   474  				return params.CursorPos
   475  			},
   476  			abortIfTargetIsCurrentLine: true,
   477  			expectedCursor:             cursorState{position: 2},
   478  			expectedText:               "abcd",
   479  		},
   480  		{
   481  			name:          "delete single line, first line",
   482  			inputString:   "abcd\nefgh\nijk",
   483  			initialCursor: cursorState{position: 2},
   484  			targetLineLocator: func(params LocatorParams) uint64 {
   485  				return params.CursorPos
   486  			},
   487  			expectedCursor: cursorState{position: 0},
   488  			expectedText:   "efgh\nijk",
   489  			expectedClipboard: clipboard.PageContent{
   490  				Text:     "abcd",
   491  				Linewise: true,
   492  			},
   493  		},
   494  		{
   495  			name:          "delete single line, interior line",
   496  			inputString:   "abcd\nefgh\nijk",
   497  			initialCursor: cursorState{position: 6},
   498  			targetLineLocator: func(params LocatorParams) uint64 {
   499  				return params.CursorPos
   500  			},
   501  			expectedCursor: cursorState{position: 5},
   502  			expectedText:   "abcd\nijk",
   503  			expectedClipboard: clipboard.PageContent{
   504  				Text:     "efgh",
   505  				Linewise: true,
   506  			},
   507  		},
   508  		{
   509  			name:          "delete single line, last line",
   510  			inputString:   "abcd\nefgh\nijk",
   511  			initialCursor: cursorState{position: 12},
   512  			targetLineLocator: func(params LocatorParams) uint64 {
   513  				return params.CursorPos
   514  			},
   515  			expectedCursor: cursorState{position: 5},
   516  			expectedText:   "abcd\nefgh",
   517  			expectedClipboard: clipboard.PageContent{
   518  				Text:     "ijk",
   519  				Linewise: true,
   520  			},
   521  		},
   522  		{
   523  			name:          "delete empty line",
   524  			inputString:   "abcd\n\nefgh",
   525  			initialCursor: cursorState{position: 5},
   526  			targetLineLocator: func(params LocatorParams) uint64 {
   527  				return params.CursorPos
   528  			},
   529  			expectedCursor: cursorState{position: 5},
   530  			expectedText:   "abcd\nefgh",
   531  			expectedClipboard: clipboard.PageContent{
   532  				Text:     "",
   533  				Linewise: true,
   534  			},
   535  		},
   536  		{
   537  			name:          "delete multiple lines down",
   538  			inputString:   "abcd\nefgh\nijk\nlmnop",
   539  			initialCursor: cursorState{position: 0},
   540  			targetLineLocator: func(params LocatorParams) uint64 {
   541  				return locate.StartOfLineBelow(params.TextTree, 2, params.CursorPos)
   542  			},
   543  			expectedCursor: cursorState{position: 0},
   544  			expectedText:   "lmnop",
   545  			expectedClipboard: clipboard.PageContent{
   546  				Text:     "abcd\nefgh\nijk",
   547  				Linewise: true,
   548  			},
   549  		},
   550  		{
   551  			name:          "delete multiple lines up",
   552  			inputString:   "abcd\nefgh\nijk\nlmnop",
   553  			initialCursor: cursorState{position: 16},
   554  			targetLineLocator: func(params LocatorParams) uint64 {
   555  				return locate.StartOfLineAbove(params.TextTree, 2, params.CursorPos)
   556  			},
   557  			expectedCursor: cursorState{position: 0},
   558  			expectedText:   "abcd",
   559  			expectedClipboard: clipboard.PageContent{
   560  				Text:     "efgh\nijk\nlmnop",
   561  				Linewise: true,
   562  			},
   563  		},
   564  		{
   565  			name:          "replace with empty line, empty document",
   566  			inputString:   "",
   567  			initialCursor: cursorState{position: 0},
   568  			targetLineLocator: func(params LocatorParams) uint64 {
   569  				return locate.StartOfLineBelow(params.TextTree, 1, params.CursorPos)
   570  			},
   571  			replaceWithEmptyLine: true,
   572  			expectedCursor:       cursorState{position: 0},
   573  			expectedText:         "",
   574  		},
   575  		{
   576  			name:          "replace with empty line, on first line",
   577  			inputString:   "abc\nefgh",
   578  			initialCursor: cursorState{position: 0},
   579  			targetLineLocator: func(params LocatorParams) uint64 {
   580  				return params.CursorPos
   581  			},
   582  			replaceWithEmptyLine: true,
   583  			expectedCursor:       cursorState{position: 0},
   584  			expectedText:         "\nefgh",
   585  			expectedClipboard: clipboard.PageContent{
   586  				Text:     "abc",
   587  				Linewise: true,
   588  			},
   589  		},
   590  		{
   591  			name:          "replace with empty line, on middle line",
   592  			inputString:   "abc\nefg\nhij",
   593  			initialCursor: cursorState{position: 5},
   594  			targetLineLocator: func(params LocatorParams) uint64 {
   595  				return params.CursorPos
   596  			},
   597  			replaceWithEmptyLine: true,
   598  			expectedCursor:       cursorState{position: 4},
   599  			expectedText:         "abc\n\nhij",
   600  			expectedClipboard: clipboard.PageContent{
   601  				Text:     "efg",
   602  				Linewise: true,
   603  			},
   604  		},
   605  		{
   606  			name:          "replace with empty line, on empty line",
   607  			inputString:   "abc\n\n\nhij",
   608  			initialCursor: cursorState{position: 4},
   609  			targetLineLocator: func(params LocatorParams) uint64 {
   610  				return params.CursorPos
   611  			},
   612  			replaceWithEmptyLine: true,
   613  			expectedCursor:       cursorState{position: 4},
   614  			expectedText:         "abc\n\n\nhij",
   615  			expectedClipboard: clipboard.PageContent{
   616  				Text:     "",
   617  				Linewise: true,
   618  			},
   619  		},
   620  		{
   621  			name:          "replace with empty line, on last line",
   622  			inputString:   "abc\nefg\nhij",
   623  			initialCursor: cursorState{position: 8},
   624  			targetLineLocator: func(params LocatorParams) uint64 {
   625  				return params.CursorPos
   626  			},
   627  			replaceWithEmptyLine: true,
   628  			expectedCursor:       cursorState{position: 8},
   629  			expectedText:         "abc\nefg\n",
   630  			expectedClipboard: clipboard.PageContent{
   631  				Text:     "hij",
   632  				Linewise: true,
   633  			},
   634  		},
   635  		{
   636  			name:                 "replace with empty line, multiple lines selected",
   637  			inputString:          "abc\nefg\nhij\nlmnop",
   638  			initialCursor:        cursorState{position: 5},
   639  			targetLineLocator:    func(params LocatorParams) uint64 { return 9 },
   640  			replaceWithEmptyLine: true,
   641  			expectedCursor:       cursorState{position: 4},
   642  			expectedText:         "abc\n\nlmnop",
   643  			expectedClipboard: clipboard.PageContent{
   644  				Text:     "efg\nhij",
   645  				Linewise: true,
   646  			},
   647  		},
   648  	}
   649  
   650  	for _, tc := range testCases {
   651  		t.Run(tc.name, func(t *testing.T) {
   652  			textTree, err := text.NewTreeFromString(tc.inputString)
   653  			require.NoError(t, err)
   654  			state := NewEditorState(100, 100, nil, nil)
   655  			state.documentBuffer.textTree = textTree
   656  			state.documentBuffer.cursor = tc.initialCursor
   657  			DeleteLines(state, tc.targetLineLocator, tc.abortIfTargetIsCurrentLine, tc.replaceWithEmptyLine, clipboard.PageDefault)
   658  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
   659  			assert.Equal(t, tc.expectedText, textTree.String())
   660  			assert.Equal(t, tc.expectedClipboard, state.clipboard.Get(clipboard.PageDefault))
   661  		})
   662  	}
   663  }
   664  
   665  func TestReplaceChar(t *testing.T) {
   666  	testCases := []struct {
   667  		name           string
   668  		inputString    string
   669  		initialCursor  cursorState
   670  		newChar        rune
   671  		autoIndent     bool
   672  		tabExpand      bool
   673  		expectedCursor cursorState
   674  		expectedText   string
   675  	}{
   676  		{
   677  			name:           "empty",
   678  			inputString:    "",
   679  			newChar:        'a',
   680  			initialCursor:  cursorState{position: 0},
   681  			expectedCursor: cursorState{position: 0},
   682  			expectedText:   "",
   683  		},
   684  		{
   685  			name:           "replace char",
   686  			inputString:    "abcd",
   687  			newChar:        'x',
   688  			initialCursor:  cursorState{position: 1},
   689  			expectedCursor: cursorState{position: 1},
   690  			expectedText:   "axcd",
   691  		},
   692  		{
   693  			name:           "empty line",
   694  			inputString:    "ab\n\ncd",
   695  			newChar:        'x',
   696  			initialCursor:  cursorState{position: 3},
   697  			expectedCursor: cursorState{position: 3},
   698  			expectedText:   "ab\n\ncd",
   699  		},
   700  		{
   701  			name:           "insert newline",
   702  			inputString:    "abcd",
   703  			newChar:        '\n',
   704  			initialCursor:  cursorState{position: 2},
   705  			expectedCursor: cursorState{position: 3},
   706  			expectedText:   "ab\nd",
   707  		},
   708  		{
   709  			name:           "insert newline with autoindent",
   710  			inputString:    "\tabcd",
   711  			newChar:        '\n',
   712  			initialCursor:  cursorState{position: 2},
   713  			autoIndent:     true,
   714  			expectedCursor: cursorState{position: 4},
   715  			expectedText:   "\ta\n\tcd",
   716  		},
   717  		{
   718  			name:           "insert tab, no expand",
   719  			inputString:    "abcd",
   720  			newChar:        '\t',
   721  			initialCursor:  cursorState{position: 2},
   722  			expectedCursor: cursorState{position: 2},
   723  			expectedText:   "ab\td",
   724  		},
   725  		{
   726  			name:           "insert tab, expand",
   727  			inputString:    "abcd",
   728  			newChar:        '\t',
   729  			initialCursor:  cursorState{position: 2},
   730  			tabExpand:      true,
   731  			expectedCursor: cursorState{position: 3},
   732  			expectedText:   "ab  d",
   733  		},
   734  	}
   735  
   736  	for _, tc := range testCases {
   737  		t.Run(tc.name, func(t *testing.T) {
   738  			textTree, err := text.NewTreeFromString(tc.inputString)
   739  			require.NoError(t, err)
   740  			state := NewEditorState(100, 100, nil, nil)
   741  			state.documentBuffer.textTree = textTree
   742  			state.documentBuffer.cursor = tc.initialCursor
   743  			state.documentBuffer.autoIndent = tc.autoIndent
   744  			state.documentBuffer.tabExpand = tc.tabExpand
   745  			state.documentBuffer.tabSize = 4
   746  			ReplaceChar(state, tc.newChar)
   747  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
   748  			assert.Equal(t, tc.expectedText, textTree.String())
   749  		})
   750  	}
   751  }
   752  
   753  func TestToggleCaseAtCursor(t *testing.T) {
   754  	testCases := []struct {
   755  		name           string
   756  		inputString    string
   757  		initialCursor  cursorState
   758  		expectedCursor cursorState
   759  		expectedText   string
   760  	}{
   761  		{
   762  			name:           "empty",
   763  			inputString:    "",
   764  			initialCursor:  cursorState{position: 0},
   765  			expectedCursor: cursorState{position: 0},
   766  			expectedText:   "",
   767  		},
   768  		{
   769  			name:           "toggle lowercase to uppercase",
   770  			inputString:    "abcd",
   771  			initialCursor:  cursorState{position: 1},
   772  			expectedCursor: cursorState{position: 2},
   773  			expectedText:   "aBcd",
   774  		},
   775  		{
   776  			name:           "toggle uppercase to lowercase",
   777  			inputString:    "ABCD",
   778  			initialCursor:  cursorState{position: 1},
   779  			expectedCursor: cursorState{position: 2},
   780  			expectedText:   "AbCD",
   781  		},
   782  		{
   783  			name:           "toggle number",
   784  			inputString:    "1234",
   785  			initialCursor:  cursorState{position: 1},
   786  			expectedCursor: cursorState{position: 2},
   787  			expectedText:   "1234",
   788  		},
   789  		{
   790  			name:           "empty line",
   791  			inputString:    "ab\n\ncd",
   792  			initialCursor:  cursorState{position: 3},
   793  			expectedCursor: cursorState{position: 3},
   794  			expectedText:   "ab\n\ncd",
   795  		},
   796  		{
   797  			name:           "toggle at end of line",
   798  			inputString:    "abcd\nefgh",
   799  			initialCursor:  cursorState{position: 3},
   800  			expectedCursor: cursorState{position: 3},
   801  			expectedText:   "abcD\nefgh",
   802  		},
   803  		{
   804  			name:           "toggle at end of document",
   805  			inputString:    "abcd",
   806  			initialCursor:  cursorState{position: 3},
   807  			expectedCursor: cursorState{position: 3},
   808  			expectedText:   "abcD",
   809  		},
   810  	}
   811  
   812  	for _, tc := range testCases {
   813  		t.Run(tc.name, func(t *testing.T) {
   814  			textTree, err := text.NewTreeFromString(tc.inputString)
   815  			require.NoError(t, err)
   816  			state := NewEditorState(100, 100, nil, nil)
   817  			state.documentBuffer.textTree = textTree
   818  			state.documentBuffer.cursor = tc.initialCursor
   819  			ToggleCaseAtCursor(state)
   820  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
   821  			assert.Equal(t, tc.expectedText, textTree.String())
   822  		})
   823  	}
   824  }
   825  
   826  func TestToggleCaseInSelection(t *testing.T) {
   827  	testCases := []struct {
   828  		name              string
   829  		inputString       string
   830  		selectionMode     selection.Mode
   831  		selectionStartPos uint64
   832  		selectionEndPos   uint64
   833  		expectedCursor    cursorState
   834  		expectedText      string
   835  	}{
   836  		{
   837  			name:              "empty",
   838  			inputString:       "",
   839  			selectionMode:     selection.ModeChar,
   840  			selectionStartPos: 0,
   841  			selectionEndPos:   0,
   842  			expectedCursor:    cursorState{position: 0},
   843  			expectedText:      "",
   844  		},
   845  		{
   846  			name:              "select single character",
   847  			inputString:       "abcdefgh",
   848  			selectionMode:     selection.ModeChar,
   849  			selectionStartPos: 2,
   850  			selectionEndPos:   3,
   851  			expectedCursor:    cursorState{position: 2},
   852  			expectedText:      "abCdefgh",
   853  		},
   854  		{
   855  			name:              "select multiple characters",
   856  			inputString:       "abcdefgh",
   857  			selectionMode:     selection.ModeLine,
   858  			selectionStartPos: 2,
   859  			selectionEndPos:   6,
   860  			expectedCursor:    cursorState{position: 2},
   861  			expectedText:      "abCDEFgh",
   862  		},
   863  	}
   864  
   865  	for _, tc := range testCases {
   866  		t.Run(tc.name, func(t *testing.T) {
   867  			textTree, err := text.NewTreeFromString(tc.inputString)
   868  			require.NoError(t, err)
   869  			state := NewEditorState(100, 100, nil, nil)
   870  			state.documentBuffer.textTree = textTree
   871  			state.documentBuffer.cursor = cursorState{position: tc.selectionStartPos}
   872  			selectionEndLoc := func(p LocatorParams) uint64 { return tc.selectionEndPos }
   873  			ToggleCaseInSelection(state, selectionEndLoc)
   874  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
   875  			assert.Equal(t, tc.expectedText, textTree.String())
   876  		})
   877  	}
   878  }
   879  
   880  func TestIndentLines(t *testing.T) {
   881  	testCases := []struct {
   882  		name           string
   883  		inputString    string
   884  		cursorPos      uint64
   885  		targetLinePos  uint64
   886  		count          uint64
   887  		tabExpand      bool
   888  		expectedCursor cursorState
   889  		expectedText   string
   890  	}{
   891  		{
   892  			name:           "empty",
   893  			inputString:    "",
   894  			cursorPos:      0,
   895  			targetLinePos:  0,
   896  			count:          1,
   897  			expectedCursor: cursorState{position: 0},
   898  			expectedText:   "",
   899  		},
   900  		{
   901  			name:           "empty line",
   902  			inputString:    "abc\n\ndef",
   903  			cursorPos:      4,
   904  			targetLinePos:  4,
   905  			count:          1,
   906  			expectedCursor: cursorState{position: 4},
   907  			expectedText:   "abc\n\ndef",
   908  		},
   909  		{
   910  			name:           "empty line with carriage return",
   911  			inputString:    "abc\r\n\r\ndef",
   912  			cursorPos:      5,
   913  			targetLinePos:  5,
   914  			count:          1,
   915  			expectedCursor: cursorState{position: 5},
   916  			expectedText:   "abc\r\n\r\ndef",
   917  		},
   918  		{
   919  			name:           "line with single character",
   920  			inputString:    "a",
   921  			cursorPos:      0,
   922  			targetLinePos:  0,
   923  			count:          1,
   924  			expectedCursor: cursorState{position: 1},
   925  			expectedText:   "\ta",
   926  		},
   927  		{
   928  			name:           "line with single character, tab expand",
   929  			tabExpand:      true,
   930  			inputString:    "a",
   931  			cursorPos:      0,
   932  			targetLinePos:  0,
   933  			count:          1,
   934  			expectedCursor: cursorState{position: 4},
   935  			expectedText:   "    a",
   936  		},
   937  		{
   938  			name:           "first line, cursor at start",
   939  			inputString:    "abc\ndef\nghi",
   940  			cursorPos:      0,
   941  			targetLinePos:  0,
   942  			count:          1,
   943  			expectedCursor: cursorState{position: 1},
   944  			expectedText:   "\tabc\ndef\nghi",
   945  		},
   946  		{
   947  			name:           "first line, cursor past start",
   948  			inputString:    "abc\ndef\nghi",
   949  			cursorPos:      1,
   950  			targetLinePos:  1,
   951  			count:          1,
   952  			expectedCursor: cursorState{position: 1},
   953  			expectedText:   "\tabc\ndef\nghi",
   954  		},
   955  		{
   956  			name:           "second line, cursor at start",
   957  			inputString:    "abc\ndef\nghi",
   958  			cursorPos:      4,
   959  			targetLinePos:  4,
   960  			count:          1,
   961  			expectedCursor: cursorState{position: 5},
   962  			expectedText:   "abc\n\tdef\nghi",
   963  		},
   964  		{
   965  			name:           "second line, cursor past start",
   966  			inputString:    "abc\ndef\nghi",
   967  			cursorPos:      6,
   968  			targetLinePos:  6,
   969  			count:          1,
   970  			expectedCursor: cursorState{position: 5},
   971  			expectedText:   "abc\n\tdef\nghi",
   972  		},
   973  		{
   974  			name:           "last line, cursor at end",
   975  			inputString:    "abc\ndef\nghi",
   976  			cursorPos:      11,
   977  			targetLinePos:  11,
   978  			count:          1,
   979  			expectedCursor: cursorState{position: 9},
   980  			expectedText:   "abc\ndef\n\tghi",
   981  		},
   982  		{
   983  			name:           "tab expand, aligned",
   984  			inputString:    "abc\ndef\nghi",
   985  			cursorPos:      6,
   986  			targetLinePos:  6,
   987  			count:          1,
   988  			tabExpand:      true,
   989  			expectedCursor: cursorState{position: 8},
   990  			expectedText:   "abc\n    def\nghi",
   991  		},
   992  		{
   993  			name:           "tab expand, line with whitespace at start",
   994  			inputString:    "abc\n  def\nghi",
   995  			cursorPos:      7,
   996  			targetLinePos:  7,
   997  			count:          1,
   998  			tabExpand:      true,
   999  			expectedCursor: cursorState{position: 10},
  1000  			expectedText:   "abc\n      def\nghi",
  1001  		},
  1002  		{
  1003  			name:           "multiple lines",
  1004  			inputString:    "ab\ncd\nef\ngh",
  1005  			cursorPos:      4,
  1006  			targetLinePos:  7,
  1007  			count:          1,
  1008  			expectedCursor: cursorState{position: 4},
  1009  			expectedText:   "ab\n\tcd\n\tef\ngh",
  1010  		},
  1011  		{
  1012  			name:           "repeat count times",
  1013  			inputString:    "ab\ncd\nef\ngh",
  1014  			cursorPos:      4,
  1015  			targetLinePos:  7,
  1016  			count:          3,
  1017  			expectedCursor: cursorState{position: 6},
  1018  			expectedText:   "ab\n\t\t\tcd\n\t\t\tef\ngh",
  1019  		},
  1020  		{
  1021  			name:           "tab expand, repeat count times",
  1022  			inputString:    "abc\n  def\nghi",
  1023  			cursorPos:      7,
  1024  			targetLinePos:  7,
  1025  			count:          3,
  1026  			tabExpand:      true,
  1027  			expectedCursor: cursorState{position: 18},
  1028  			expectedText:   "abc\n              def\nghi",
  1029  		},
  1030  	}
  1031  
  1032  	for _, tc := range testCases {
  1033  		t.Run(tc.name, func(t *testing.T) {
  1034  			textTree, err := text.NewTreeFromString(tc.inputString)
  1035  			require.NoError(t, err)
  1036  			state := NewEditorState(100, 100, nil, nil)
  1037  			state.documentBuffer.textTree = textTree
  1038  			state.documentBuffer.cursor = cursorState{position: tc.cursorPos}
  1039  			state.documentBuffer.tabExpand = tc.tabExpand
  1040  			targetLineLoc := func(p LocatorParams) uint64 { return tc.targetLinePos }
  1041  			IndentLines(state, targetLineLoc, tc.count)
  1042  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
  1043  			assert.Equal(t, tc.expectedText, textTree.String())
  1044  		})
  1045  	}
  1046  }
  1047  
  1048  func TestOutdentLines(t *testing.T) {
  1049  	testCases := []struct {
  1050  		name           string
  1051  		inputString    string
  1052  		cursorPos      uint64
  1053  		targetLinePos  uint64
  1054  		count          uint64
  1055  		tabSize        uint64
  1056  		expectedCursor cursorState
  1057  		expectedText   string
  1058  	}{
  1059  		{
  1060  			name:           "empty",
  1061  			inputString:    "",
  1062  			cursorPos:      0,
  1063  			targetLinePos:  0,
  1064  			count:          1,
  1065  			tabSize:        4,
  1066  			expectedCursor: cursorState{position: 0},
  1067  			expectedText:   "",
  1068  		},
  1069  		{
  1070  			name:           "outdent first line starting with a single tab, on tab",
  1071  			inputString:    "\tabc",
  1072  			cursorPos:      0,
  1073  			targetLinePos:  0,
  1074  			count:          1,
  1075  			tabSize:        4,
  1076  			expectedCursor: cursorState{position: 0},
  1077  			expectedText:   "abc",
  1078  		},
  1079  		{
  1080  			name:           "outdent first line starting with a single tab, on start of text",
  1081  			inputString:    "\tabc",
  1082  			cursorPos:      1,
  1083  			targetLinePos:  1,
  1084  			count:          1,
  1085  			tabSize:        4,
  1086  			expectedCursor: cursorState{position: 0},
  1087  			expectedText:   "abc",
  1088  		},
  1089  		{
  1090  			name:           "outdent first line starting with a single tab, on end of text",
  1091  			inputString:    "\tabc",
  1092  			cursorPos:      3,
  1093  			targetLinePos:  3,
  1094  			count:          1,
  1095  			tabSize:        4,
  1096  			expectedCursor: cursorState{position: 0},
  1097  			expectedText:   "abc",
  1098  		},
  1099  		{
  1100  			name:           "outdent first line starting with multiple tabs",
  1101  			inputString:    "\t\t\tabc",
  1102  			cursorPos:      4,
  1103  			targetLinePos:  4,
  1104  			count:          1,
  1105  			tabSize:        4,
  1106  			expectedCursor: cursorState{position: 2},
  1107  			expectedText:   "\t\tabc",
  1108  		},
  1109  		{
  1110  			name:           "outdent first line starting with spaces less than tabsize",
  1111  			inputString:    "  abc",
  1112  			cursorPos:      2,
  1113  			targetLinePos:  2,
  1114  			count:          1,
  1115  			tabSize:        4,
  1116  			expectedCursor: cursorState{position: 0},
  1117  			expectedText:   "abc",
  1118  		},
  1119  		{
  1120  			name:           "outdent first line starting with spaces equal to tabsize",
  1121  			inputString:    "    abc",
  1122  			cursorPos:      2,
  1123  			targetLinePos:  2,
  1124  			count:          1,
  1125  			tabSize:        4,
  1126  			expectedCursor: cursorState{position: 0},
  1127  			expectedText:   "abc",
  1128  		},
  1129  		{
  1130  			name:           "outdent first line starting with spaces greater than tabsize",
  1131  			inputString:    "    abc",
  1132  			cursorPos:      2,
  1133  			targetLinePos:  2,
  1134  			count:          1,
  1135  			tabSize:        2,
  1136  			expectedCursor: cursorState{position: 2},
  1137  			expectedText:   "  abc",
  1138  		},
  1139  		{
  1140  			name:           "outdent empty line",
  1141  			inputString:    "abc\n\ndef",
  1142  			cursorPos:      5,
  1143  			targetLinePos:  5,
  1144  			count:          1,
  1145  			tabSize:        4,
  1146  			expectedCursor: cursorState{position: 5},
  1147  			expectedText:   "abc\n\ndef",
  1148  		},
  1149  		{
  1150  			name:           "outdent line with only space",
  1151  			inputString:    "abc\n      \ndef",
  1152  			cursorPos:      5,
  1153  			targetLinePos:  5,
  1154  			count:          1,
  1155  			tabSize:        4,
  1156  			expectedCursor: cursorState{position: 6},
  1157  			expectedText:   "abc\n  \ndef",
  1158  		},
  1159  		{
  1160  			name:           "outdent middle line",
  1161  			inputString:    "abc\n\t\tdef\nghi",
  1162  			cursorPos:      7,
  1163  			targetLinePos:  7,
  1164  			count:          1,
  1165  			tabSize:        4,
  1166  			expectedCursor: cursorState{position: 5},
  1167  			expectedText:   "abc\n\tdef\nghi",
  1168  		},
  1169  		{
  1170  			name:           "outdent mix of tabs and spaces",
  1171  			inputString:    "  \t abc",
  1172  			cursorPos:      5,
  1173  			targetLinePos:  5,
  1174  			count:          1,
  1175  			tabSize:        4,
  1176  			expectedCursor: cursorState{position: 1},
  1177  			expectedText:   " abc",
  1178  		},
  1179  		{
  1180  			name:           "multiple lines",
  1181  			inputString:    "ab\n\tcd\n\t\tef\ngh",
  1182  			cursorPos:      5,
  1183  			targetLinePos:  8,
  1184  			count:          1,
  1185  			tabSize:        4,
  1186  			expectedCursor: cursorState{position: 3},
  1187  			expectedText:   "ab\ncd\n\tef\ngh",
  1188  		},
  1189  		{
  1190  			name:           "repeat count times",
  1191  			inputString:    "ab\n\t\t\tcd\n\t\t\t\tef\ngh",
  1192  			cursorPos:      5,
  1193  			targetLinePos:  14,
  1194  			count:          3,
  1195  			tabSize:        4,
  1196  			expectedCursor: cursorState{position: 3},
  1197  			expectedText:   "ab\ncd\n\tef\ngh",
  1198  		},
  1199  	}
  1200  
  1201  	for _, tc := range testCases {
  1202  		t.Run(tc.name, func(t *testing.T) {
  1203  			textTree, err := text.NewTreeFromString(tc.inputString)
  1204  			require.NoError(t, err)
  1205  			state := NewEditorState(100, 100, nil, nil)
  1206  			state.documentBuffer.textTree = textTree
  1207  			state.documentBuffer.cursor = cursorState{position: tc.cursorPos}
  1208  			state.documentBuffer.tabSize = tc.tabSize
  1209  			targetLineLoc := func(p LocatorParams) uint64 { return tc.targetLinePos }
  1210  			OutdentLines(state, targetLineLoc, tc.count)
  1211  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
  1212  			assert.Equal(t, tc.expectedText, textTree.String())
  1213  		})
  1214  	}
  1215  }
  1216  
  1217  func TestBeginNewLineAbove(t *testing.T) {
  1218  	testCases := []struct {
  1219  		name           string
  1220  		inputString    string
  1221  		cursorPos      uint64
  1222  		autoIndent     bool
  1223  		expectedCursor cursorState
  1224  		expectedText   string
  1225  	}{
  1226  		{
  1227  			name:           "empty, no autoindent",
  1228  			inputString:    "",
  1229  			cursorPos:      0,
  1230  			autoIndent:     false,
  1231  			expectedCursor: cursorState{position: 0},
  1232  			expectedText:   "\n",
  1233  		},
  1234  		{
  1235  			name:           "empty, autoindent",
  1236  			inputString:    "",
  1237  			cursorPos:      0,
  1238  			autoIndent:     true,
  1239  			expectedCursor: cursorState{position: 0},
  1240  			expectedText:   "\n",
  1241  		},
  1242  		{
  1243  			name:           "multiple lines, no indentation, no autoindent",
  1244  			inputString:    "abc\ndef\nhij",
  1245  			cursorPos:      5,
  1246  			autoIndent:     false,
  1247  			expectedCursor: cursorState{position: 4},
  1248  			expectedText:   "abc\n\ndef\nhij",
  1249  		},
  1250  		{
  1251  			name:           "multiple lines, indentation, no autoindent",
  1252  			inputString:    "abc\n\t\tdef\nhij",
  1253  			cursorPos:      5,
  1254  			autoIndent:     false,
  1255  			expectedCursor: cursorState{position: 4},
  1256  			expectedText:   "abc\n\n\t\tdef\nhij",
  1257  		},
  1258  		{
  1259  			name:           "multiple lines, no indentation, autoindent",
  1260  			inputString:    "abc\ndef\nhij",
  1261  			cursorPos:      5,
  1262  			autoIndent:     true,
  1263  			expectedCursor: cursorState{position: 4},
  1264  			expectedText:   "abc\n\ndef\nhij",
  1265  		},
  1266  		{
  1267  			name:           "multiple lines, indentation, autoindent",
  1268  			inputString:    "abc\n\t\tdef\nhij",
  1269  			cursorPos:      5,
  1270  			autoIndent:     true,
  1271  			expectedCursor: cursorState{position: 6},
  1272  			expectedText:   "abc\n\t\t\n\t\tdef\nhij",
  1273  		},
  1274  	}
  1275  
  1276  	for _, tc := range testCases {
  1277  		t.Run(tc.name, func(t *testing.T) {
  1278  			textTree, err := text.NewTreeFromString(tc.inputString)
  1279  			require.NoError(t, err)
  1280  			state := NewEditorState(100, 100, nil, nil)
  1281  			state.documentBuffer.textTree = textTree
  1282  			state.documentBuffer.cursor = cursorState{position: tc.cursorPos}
  1283  			state.documentBuffer.autoIndent = tc.autoIndent
  1284  			BeginNewLineAbove(state)
  1285  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
  1286  			assert.Equal(t, tc.expectedText, textTree.String())
  1287  		})
  1288  	}
  1289  }
  1290  
  1291  func TestJoinLines(t *testing.T) {
  1292  	testCases := []struct {
  1293  		name           string
  1294  		inputString    string
  1295  		initialCursor  cursorState
  1296  		expectedText   string
  1297  		expectedCursor cursorState
  1298  	}{
  1299  		{
  1300  			name:           "empty",
  1301  			inputString:    "",
  1302  			initialCursor:  cursorState{position: 0},
  1303  			expectedText:   "",
  1304  			expectedCursor: cursorState{position: 0},
  1305  		},
  1306  		{
  1307  			name:           "two lines, no indentation, cursor at start",
  1308  			inputString:    "abc\ndef",
  1309  			initialCursor:  cursorState{position: 0},
  1310  			expectedText:   "abc def",
  1311  			expectedCursor: cursorState{position: 3},
  1312  		},
  1313  		{
  1314  			name:           "two lines, no indentation, cursor before newline",
  1315  			inputString:    "abc\ndef",
  1316  			initialCursor:  cursorState{position: 2},
  1317  			expectedText:   "abc def",
  1318  			expectedCursor: cursorState{position: 3},
  1319  		},
  1320  		{
  1321  			name:           "two lines, no indentation, cursor on newline",
  1322  			inputString:    "abc\ndef",
  1323  			initialCursor:  cursorState{position: 3},
  1324  			expectedText:   "abc def",
  1325  			expectedCursor: cursorState{position: 3},
  1326  		},
  1327  		{
  1328  			name:           "two lines, second line indented with spaces",
  1329  			inputString:    "abc\n    def",
  1330  			initialCursor:  cursorState{position: 2},
  1331  			expectedText:   "abc def",
  1332  			expectedCursor: cursorState{position: 3},
  1333  		},
  1334  		{
  1335  			name:           "two lines, second line indented with tabs",
  1336  			inputString:    "abc\n\t\tdef",
  1337  			initialCursor:  cursorState{position: 2},
  1338  			expectedText:   "abc def",
  1339  			expectedCursor: cursorState{position: 3},
  1340  		},
  1341  		{
  1342  			name:           "multiple lines, on last line",
  1343  			inputString:    "abc\ndef\nghijk",
  1344  			initialCursor:  cursorState{position: 10},
  1345  			expectedText:   "abc\ndef\nghijk",
  1346  			expectedCursor: cursorState{position: 10},
  1347  		},
  1348  		{
  1349  			name:           "second-to-last line, last line is whitespace",
  1350  			inputString:    "abc\n     ",
  1351  			initialCursor:  cursorState{position: 2},
  1352  			expectedText:   "abc",
  1353  			expectedCursor: cursorState{position: 2},
  1354  		},
  1355  		{
  1356  			name:           "before empty line",
  1357  			inputString:    "abc\n\ndef",
  1358  			initialCursor:  cursorState{position: 1},
  1359  			expectedText:   "abc\ndef",
  1360  			expectedCursor: cursorState{position: 2},
  1361  		},
  1362  		{
  1363  			name:           "before multiple empty lines",
  1364  			inputString:    "abc\n\n\n\ndef",
  1365  			initialCursor:  cursorState{position: 1},
  1366  			expectedText:   "abc\n\n\ndef",
  1367  			expectedCursor: cursorState{position: 2},
  1368  		},
  1369  		{
  1370  			name:           "on empty line before non-empty line",
  1371  			inputString:    "abc\n\ndef\nxyz",
  1372  			initialCursor:  cursorState{position: 4},
  1373  			expectedText:   "abc\ndef\nxyz",
  1374  			expectedCursor: cursorState{position: 4},
  1375  		},
  1376  		{
  1377  			name:           "on empty line before empty line",
  1378  			inputString:    "abc\n\n\n\ndef",
  1379  			initialCursor:  cursorState{position: 4},
  1380  			expectedText:   "abc\n\n\ndef",
  1381  			expectedCursor: cursorState{position: 4},
  1382  		},
  1383  		{
  1384  			name:           "before line all whitespace",
  1385  			inputString:    "abc\n       \ndef",
  1386  			initialCursor:  cursorState{position: 2},
  1387  			expectedText:   "abc\ndef",
  1388  			expectedCursor: cursorState{position: 2},
  1389  		},
  1390  	}
  1391  
  1392  	for _, tc := range testCases {
  1393  		t.Run(tc.name, func(t *testing.T) {
  1394  			textTree, err := text.NewTreeFromString(tc.inputString)
  1395  			require.NoError(t, err)
  1396  			state := NewEditorState(100, 100, nil, nil)
  1397  			state.documentBuffer.textTree = textTree
  1398  			state.documentBuffer.cursor = tc.initialCursor
  1399  			JoinLines(state)
  1400  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
  1401  			assert.Equal(t, tc.expectedText, textTree.String())
  1402  		})
  1403  	}
  1404  }
  1405  
  1406  func TestCopyRange(t *testing.T) {
  1407  	testCases := []struct {
  1408  		name              string
  1409  		inputString       string
  1410  		loc               RangeLocator
  1411  		expectedClipboard clipboard.PageContent
  1412  	}{
  1413  		{
  1414  			name:              "empty",
  1415  			inputString:       "",
  1416  			loc:               func(p LocatorParams) (uint64, uint64) { return 0, 0 },
  1417  			expectedClipboard: clipboard.PageContent{},
  1418  		},
  1419  		{
  1420  			name:              "start pos equal to end pos",
  1421  			inputString:       "abcd",
  1422  			loc:               func(p LocatorParams) (uint64, uint64) { return 2, 2 },
  1423  			expectedClipboard: clipboard.PageContent{},
  1424  		},
  1425  		{
  1426  			name:              "start pos after  end pos",
  1427  			inputString:       "abcd",
  1428  			loc:               func(p LocatorParams) (uint64, uint64) { return 3, 2 },
  1429  			expectedClipboard: clipboard.PageContent{},
  1430  		},
  1431  		{
  1432  			name:              "start pos before end pos",
  1433  			inputString:       "abcd",
  1434  			loc:               func(p LocatorParams) (uint64, uint64) { return 1, 3 },
  1435  			expectedClipboard: clipboard.PageContent{Text: "bc"},
  1436  		},
  1437  	}
  1438  
  1439  	for _, tc := range testCases {
  1440  		t.Run(tc.name, func(t *testing.T) {
  1441  			textTree, err := text.NewTreeFromString(tc.inputString)
  1442  			require.NoError(t, err)
  1443  			state := NewEditorState(100, 100, nil, nil)
  1444  			state.documentBuffer.textTree = textTree
  1445  			CopyRange(state, clipboard.PageDefault, tc.loc)
  1446  			assert.Equal(t, tc.expectedClipboard, state.clipboard.Get(clipboard.PageDefault))
  1447  		})
  1448  	}
  1449  }
  1450  
  1451  func TestCopyLine(t *testing.T) {
  1452  	testCases := []struct {
  1453  		name              string
  1454  		inputString       string
  1455  		initialCursor     cursorState
  1456  		expectedClipboard clipboard.PageContent
  1457  	}{
  1458  		{
  1459  			name:          "empty",
  1460  			inputString:   "",
  1461  			initialCursor: cursorState{position: 0},
  1462  			expectedClipboard: clipboard.PageContent{
  1463  				Linewise: true,
  1464  			},
  1465  		},
  1466  		{
  1467  			name:          "single line, cursor at start",
  1468  			inputString:   "abcd",
  1469  			initialCursor: cursorState{position: 0},
  1470  			expectedClipboard: clipboard.PageContent{
  1471  				Text:     "abcd",
  1472  				Linewise: true,
  1473  			},
  1474  		},
  1475  		{
  1476  			name:          "single line, cursor in middle",
  1477  			inputString:   "abcd",
  1478  			initialCursor: cursorState{position: 2},
  1479  			expectedClipboard: clipboard.PageContent{
  1480  				Text:     "abcd",
  1481  				Linewise: true,
  1482  			},
  1483  		},
  1484  		{
  1485  			name:          "single line, cursor at end",
  1486  			inputString:   "abcd",
  1487  			initialCursor: cursorState{position: 4},
  1488  			expectedClipboard: clipboard.PageContent{
  1489  				Text:     "abcd",
  1490  				Linewise: true,
  1491  			},
  1492  		},
  1493  		{
  1494  			name:          "multiple lines, cursor on first line",
  1495  			inputString:   "abcd\nefgh\nijkl",
  1496  			initialCursor: cursorState{position: 2},
  1497  			expectedClipboard: clipboard.PageContent{
  1498  				Text:     "abcd",
  1499  				Linewise: true,
  1500  			},
  1501  		},
  1502  		{
  1503  			name:          "multiple lines, cursor on middle line",
  1504  			inputString:   "abcd\nefgh\nijkl",
  1505  			initialCursor: cursorState{position: 5},
  1506  			expectedClipboard: clipboard.PageContent{
  1507  				Text:     "efgh",
  1508  				Linewise: true,
  1509  			},
  1510  		},
  1511  		{
  1512  			name:          "multiple lines, cursor on last line",
  1513  			inputString:   "abcd\nefgh\nijkl",
  1514  			initialCursor: cursorState{position: 10},
  1515  			expectedClipboard: clipboard.PageContent{
  1516  				Text:     "ijkl",
  1517  				Linewise: true,
  1518  			},
  1519  		},
  1520  		{
  1521  			name:          "cursor on empty line",
  1522  			inputString:   "abcd\n\n\nefgh",
  1523  			initialCursor: cursorState{position: 5},
  1524  			expectedClipboard: clipboard.PageContent{
  1525  				Text:     "",
  1526  				Linewise: true,
  1527  			},
  1528  		},
  1529  		{
  1530  			name:          "multi-byte unicode",
  1531  			inputString:   "丂丄丅丆丏 ¢ह€한",
  1532  			initialCursor: cursorState{position: 2},
  1533  			expectedClipboard: clipboard.PageContent{
  1534  				Text:     "丂丄丅丆丏 ¢ह€한",
  1535  				Linewise: true,
  1536  			},
  1537  		},
  1538  	}
  1539  
  1540  	for _, tc := range testCases {
  1541  		t.Run(tc.name, func(t *testing.T) {
  1542  			textTree, err := text.NewTreeFromString(tc.inputString)
  1543  			require.NoError(t, err)
  1544  			state := NewEditorState(100, 100, nil, nil)
  1545  			state.documentBuffer.textTree = textTree
  1546  			state.documentBuffer.cursor = tc.initialCursor
  1547  			CopyLine(state, clipboard.PageDefault)
  1548  			assert.Equal(t, tc.initialCursor, state.documentBuffer.cursor)
  1549  			assert.Equal(t, tc.expectedClipboard, state.clipboard.Get(clipboard.PageDefault))
  1550  		})
  1551  	}
  1552  }
  1553  
  1554  func TestCopySelection(t *testing.T) {
  1555  	testCases := []struct {
  1556  		name              string
  1557  		inputString       string
  1558  		selectionMode     selection.Mode
  1559  		cursorStartPos    uint64
  1560  		cursorEndPos      uint64
  1561  		expectedCursor    cursorState
  1562  		expectedText      string
  1563  		expectedClipboard clipboard.PageContent
  1564  	}{
  1565  		{
  1566  			name:              "empty document, select charwise",
  1567  			inputString:       "",
  1568  			selectionMode:     selection.ModeChar,
  1569  			cursorStartPos:    0,
  1570  			cursorEndPos:      0,
  1571  			expectedCursor:    cursorState{position: 0},
  1572  			expectedText:      "",
  1573  			expectedClipboard: clipboard.PageContent{Text: ""},
  1574  		},
  1575  		{
  1576  			name:           "empty document, select linewise",
  1577  			inputString:    "",
  1578  			selectionMode:  selection.ModeLine,
  1579  			cursorStartPos: 0,
  1580  			cursorEndPos:   0,
  1581  			expectedCursor: cursorState{position: 0},
  1582  			expectedText:   "",
  1583  			expectedClipboard: clipboard.PageContent{
  1584  				Text:     "",
  1585  				Linewise: true,
  1586  			},
  1587  		},
  1588  		{
  1589  			name:              "nonempty charwise selection",
  1590  			inputString:       "abcd1234",
  1591  			selectionMode:     selection.ModeChar,
  1592  			cursorStartPos:    1,
  1593  			cursorEndPos:      3,
  1594  			expectedCursor:    cursorState{position: 1},
  1595  			expectedText:      "abcd1234",
  1596  			expectedClipboard: clipboard.PageContent{Text: "bcd"},
  1597  		},
  1598  		{
  1599  			name:           "nonempty linewise selection",
  1600  			inputString:    "ab\ncde\nfgh\n12\n34",
  1601  			selectionMode:  selection.ModeLine,
  1602  			cursorStartPos: 4,
  1603  			cursorEndPos:   8,
  1604  			expectedCursor: cursorState{position: 3},
  1605  			expectedText:   "ab\ncde\nfgh\n12\n34",
  1606  			expectedClipboard: clipboard.PageContent{
  1607  				Text:     "cde\nfgh",
  1608  				Linewise: true,
  1609  			},
  1610  		},
  1611  		{
  1612  			name:              "empty line, select charwise",
  1613  			inputString:       "abc\n\ndef",
  1614  			selectionMode:     selection.ModeChar,
  1615  			cursorStartPos:    4,
  1616  			cursorEndPos:      4,
  1617  			expectedCursor:    cursorState{position: 4},
  1618  			expectedText:      "abc\n\ndef",
  1619  			expectedClipboard: clipboard.PageContent{Text: "\n"},
  1620  		},
  1621  		{
  1622  			name:           "empty line, select linewise",
  1623  			inputString:    "abc\n\ndef",
  1624  			selectionMode:  selection.ModeLine,
  1625  			cursorStartPos: 4,
  1626  			cursorEndPos:   4,
  1627  			expectedCursor: cursorState{position: 4},
  1628  			expectedText:   "abc\n\ndef",
  1629  			expectedClipboard: clipboard.PageContent{
  1630  				Text:     "",
  1631  				Linewise: true,
  1632  			},
  1633  		},
  1634  	}
  1635  
  1636  	for _, tc := range testCases {
  1637  		t.Run(tc.name, func(t *testing.T) {
  1638  			textTree, err := text.NewTreeFromString(tc.inputString)
  1639  			require.NoError(t, err)
  1640  			state := NewEditorState(100, 100, nil, nil)
  1641  			state.documentBuffer.textTree = textTree
  1642  			state.documentBuffer.selector.Start(tc.selectionMode, tc.cursorStartPos)
  1643  			state.documentBuffer.cursor = cursorState{position: tc.cursorEndPos}
  1644  			CopySelection(state, clipboard.PageDefault)
  1645  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
  1646  			assert.Equal(t, tc.expectedText, textTree.String())
  1647  			assert.Equal(t, tc.expectedClipboard, state.clipboard.Get(clipboard.PageDefault))
  1648  			assert.Equal(t, false, state.documentBuffer.undoLog.HasUnsavedChanges())
  1649  		})
  1650  	}
  1651  }
  1652  
  1653  func TestPasteAfterCursor(t *testing.T) {
  1654  	testCases := []struct {
  1655  		name           string
  1656  		inputString    string
  1657  		initialCursor  cursorState
  1658  		clipboard      clipboard.PageContent
  1659  		expectedCursor cursorState
  1660  		expectedText   string
  1661  	}{
  1662  		{
  1663  			name:           "empty document, empty clipboard",
  1664  			inputString:    "",
  1665  			initialCursor:  cursorState{position: 0},
  1666  			clipboard:      clipboard.PageContent{},
  1667  			expectedCursor: cursorState{position: 0},
  1668  			expectedText:   "",
  1669  		},
  1670  		{
  1671  			name:          "empty document, empty clipboard insert on next line",
  1672  			inputString:   "",
  1673  			initialCursor: cursorState{position: 0},
  1674  			clipboard: clipboard.PageContent{
  1675  				Linewise: true,
  1676  			},
  1677  			expectedCursor: cursorState{position: 1},
  1678  			expectedText:   "\n",
  1679  		},
  1680  		{
  1681  			name:          "paste after cursor",
  1682  			inputString:   "abcd",
  1683  			initialCursor: cursorState{position: 2},
  1684  			clipboard: clipboard.PageContent{
  1685  				Text:     "xyz",
  1686  				Linewise: false,
  1687  			},
  1688  			expectedCursor: cursorState{position: 5},
  1689  			expectedText:   "abcxyzd",
  1690  		},
  1691  		{
  1692  			name:          "paste after cursor insert on next line",
  1693  			inputString:   "abcd",
  1694  			initialCursor: cursorState{position: 2},
  1695  			clipboard: clipboard.PageContent{
  1696  				Text:     "xyz",
  1697  				Linewise: true,
  1698  			},
  1699  			expectedCursor: cursorState{position: 5},
  1700  			expectedText:   "abcd\nxyz",
  1701  		},
  1702  		{
  1703  			name:          "paste newline after cursor",
  1704  			inputString:   "abcd",
  1705  			initialCursor: cursorState{position: 1},
  1706  			clipboard: clipboard.PageContent{
  1707  				Text:     "\n",
  1708  				Linewise: false,
  1709  			},
  1710  			expectedCursor: cursorState{position: 3},
  1711  			expectedText:   "ab\ncd",
  1712  		},
  1713  		{
  1714  			name:          "multi-byte unicode",
  1715  			inputString:   "abc",
  1716  			initialCursor: cursorState{position: 1},
  1717  			clipboard: clipboard.PageContent{
  1718  				Text:     "丂丄丅丆丏 ¢ह€한",
  1719  				Linewise: false,
  1720  			},
  1721  			expectedCursor: cursorState{position: 11},
  1722  			expectedText:   "ab丂丄丅丆丏 ¢ह€한c",
  1723  		},
  1724  	}
  1725  
  1726  	for _, tc := range testCases {
  1727  		t.Run(tc.name, func(t *testing.T) {
  1728  			textTree, err := text.NewTreeFromString(tc.inputString)
  1729  			require.NoError(t, err)
  1730  			state := NewEditorState(100, 100, nil, nil)
  1731  			state.documentBuffer.textTree = textTree
  1732  			state.documentBuffer.cursor = tc.initialCursor
  1733  			state.clipboard.Set(clipboard.PageDefault, tc.clipboard)
  1734  			PasteAfterCursor(state, clipboard.PageDefault)
  1735  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
  1736  			assert.Equal(t, tc.expectedText, textTree.String())
  1737  		})
  1738  	}
  1739  }
  1740  
  1741  func TestPasteBeforeCursor(t *testing.T) {
  1742  	testCases := []struct {
  1743  		name           string
  1744  		inputString    string
  1745  		initialCursor  cursorState
  1746  		clipboard      clipboard.PageContent
  1747  		expectedCursor cursorState
  1748  		expectedText   string
  1749  	}{
  1750  		{
  1751  			name:           "empty document, empty clipboard",
  1752  			inputString:    "",
  1753  			initialCursor:  cursorState{position: 0},
  1754  			clipboard:      clipboard.PageContent{},
  1755  			expectedCursor: cursorState{position: 0},
  1756  			expectedText:   "",
  1757  		},
  1758  		{
  1759  			name:          "empty document, empty clipboard insert on next line",
  1760  			inputString:   "",
  1761  			initialCursor: cursorState{position: 0},
  1762  			clipboard: clipboard.PageContent{
  1763  				Linewise: true,
  1764  			},
  1765  			expectedCursor: cursorState{position: 0},
  1766  			expectedText:   "\n",
  1767  		},
  1768  		{
  1769  			name:          "paste before cursor",
  1770  			inputString:   "abcd",
  1771  			initialCursor: cursorState{position: 2},
  1772  			clipboard: clipboard.PageContent{
  1773  				Text:     "xyz",
  1774  				Linewise: false,
  1775  			},
  1776  			expectedCursor: cursorState{position: 4},
  1777  			expectedText:   "abxyzcd",
  1778  		},
  1779  		{
  1780  			name:          "paste before cursor insert on next line",
  1781  			inputString:   "abcd",
  1782  			initialCursor: cursorState{position: 2},
  1783  			clipboard: clipboard.PageContent{
  1784  				Text:     "xyz",
  1785  				Linewise: true,
  1786  			},
  1787  			expectedCursor: cursorState{position: 0},
  1788  			expectedText:   "xyz\nabcd",
  1789  		},
  1790  		{
  1791  			name:          "paste newline before cursor",
  1792  			inputString:   "abcd",
  1793  			initialCursor: cursorState{position: 2},
  1794  			clipboard: clipboard.PageContent{
  1795  				Text:     "\n",
  1796  				Linewise: false,
  1797  			},
  1798  			expectedCursor: cursorState{position: 1},
  1799  			expectedText:   "ab\ncd",
  1800  		},
  1801  		{
  1802  			name:          "multi-byte unicode",
  1803  			inputString:   "abc",
  1804  			initialCursor: cursorState{position: 1},
  1805  			clipboard: clipboard.PageContent{
  1806  				Text:     "丂丄丅丆丏 ¢ह€한",
  1807  				Linewise: false,
  1808  			},
  1809  			expectedCursor: cursorState{position: 10},
  1810  			expectedText:   "a丂丄丅丆丏 ¢ह€한bc",
  1811  		},
  1812  	}
  1813  
  1814  	for _, tc := range testCases {
  1815  		t.Run(tc.name, func(t *testing.T) {
  1816  			textTree, err := text.NewTreeFromString(tc.inputString)
  1817  			require.NoError(t, err)
  1818  			state := NewEditorState(100, 100, nil, nil)
  1819  			state.documentBuffer.textTree = textTree
  1820  			state.documentBuffer.cursor = tc.initialCursor
  1821  			state.clipboard.Set(clipboard.PageDefault, tc.clipboard)
  1822  			PasteBeforeCursor(state, clipboard.PageDefault)
  1823  			assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor)
  1824  			assert.Equal(t, tc.expectedText, textTree.String())
  1825  		})
  1826  	}
  1827  }