github.com/aretext/aretext@v1.3.0/locate/character_test.go (about)

     1  package locate
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/stretchr/testify/assert"
     7  	"github.com/stretchr/testify/require"
     8  
     9  	"github.com/aretext/aretext/text"
    10  )
    11  
    12  func TestNextCharInLine(t *testing.T) {
    13  	testCases := []struct {
    14  		name                   string
    15  		inputString            string
    16  		pos                    uint64
    17  		count                  uint64
    18  		includeEndOfLineOrFile bool
    19  		expectedPos            uint64
    20  	}{
    21  		{
    22  			name:        "empty string",
    23  			inputString: "",
    24  			pos:         0,
    25  			count:       1,
    26  			expectedPos: 0,
    27  		},
    28  		{
    29  			name:        "first char, ASCII string",
    30  			inputString: "abcd",
    31  			pos:         0,
    32  			count:       1,
    33  			expectedPos: 1,
    34  		},
    35  		{
    36  			name:        "second char, ASCII string",
    37  			inputString: "abcd",
    38  			pos:         1,
    39  			count:       1,
    40  			expectedPos: 2,
    41  		},
    42  		{
    43  			name:        "last char, ASCII string",
    44  			inputString: "abcd",
    45  			pos:         3,
    46  			count:       1,
    47  			expectedPos: 3,
    48  		},
    49  		{
    50  			name:        "multi-char grapheme cluster",
    51  			inputString: "e\u0301xyz",
    52  			pos:         0,
    53  			count:       1,
    54  			expectedPos: 2,
    55  		},
    56  		{
    57  			name:        "up to end of line",
    58  			inputString: "ab\ncd",
    59  			pos:         1,
    60  			count:       1,
    61  			expectedPos: 1,
    62  		},
    63  		{
    64  			name:        "at end of line",
    65  			inputString: "ab\ncd",
    66  			pos:         2,
    67  			count:       1,
    68  			expectedPos: 2,
    69  		},
    70  		{
    71  			name:        "end of line with carriage return",
    72  			inputString: "ab\r\ncd",
    73  			pos:         1,
    74  			count:       1,
    75  			expectedPos: 1,
    76  		},
    77  		{
    78  			name:        "move cursor multiple chars within line",
    79  			inputString: "abcdefgh",
    80  			pos:         2,
    81  			count:       3,
    82  			expectedPos: 5,
    83  		},
    84  		{
    85  			name:        "move cursor multiple chars to end of line",
    86  			inputString: "abcd\nefgh",
    87  			pos:         1,
    88  			count:       2,
    89  			expectedPos: 3,
    90  		},
    91  		{
    92  			name:        "move cursor multiple chars past end of line",
    93  			inputString: "abcd\nefgh",
    94  			pos:         1,
    95  			count:       5,
    96  			expectedPos: 3,
    97  		},
    98  		{
    99  			name:        "move cursor multiple chars past end of string",
   100  			inputString: "abcd",
   101  			pos:         0,
   102  			count:       100,
   103  			expectedPos: 3,
   104  		},
   105  		{
   106  			name:                   "include end of line",
   107  			inputString:            "abcd\nefgh",
   108  			pos:                    2,
   109  			count:                  5,
   110  			includeEndOfLineOrFile: true,
   111  			expectedPos:            4,
   112  		},
   113  		{
   114  			name:                   "include end of file",
   115  			inputString:            "abcd",
   116  			pos:                    2,
   117  			count:                  5,
   118  			includeEndOfLineOrFile: true,
   119  			expectedPos:            4,
   120  		},
   121  		{
   122  			name:                   "first character when including end of line or file",
   123  			inputString:            "abcdefgh",
   124  			pos:                    0,
   125  			count:                  1,
   126  			includeEndOfLineOrFile: true,
   127  			expectedPos:            1,
   128  		},
   129  	}
   130  
   131  	for _, tc := range testCases {
   132  		t.Run(tc.name, func(t *testing.T) {
   133  			textTree, err := text.NewTreeFromString(tc.inputString)
   134  			require.NoError(t, err)
   135  			actualPos := NextCharInLine(textTree, tc.count, tc.includeEndOfLineOrFile, tc.pos)
   136  			assert.Equal(t, tc.expectedPos, actualPos)
   137  		})
   138  	}
   139  }
   140  
   141  func TestPrevCharInLine(t *testing.T) {
   142  	testCases := []struct {
   143  		name                   string
   144  		inputString            string
   145  		pos                    uint64
   146  		count                  uint64
   147  		includeEndOfLineOrFile bool
   148  		expectedPos            uint64
   149  	}{
   150  		{
   151  			name:        "empty",
   152  			inputString: "",
   153  			pos:         0,
   154  			count:       1,
   155  			expectedPos: 0,
   156  		},
   157  		{
   158  			name:        "last char, ASCII string",
   159  			inputString: "abcd",
   160  			pos:         3,
   161  			count:       1,
   162  			expectedPos: 2,
   163  		},
   164  		{
   165  			name:        "second-to-last char, ASCII string",
   166  			inputString: "abcd",
   167  			pos:         2,
   168  			count:       1,
   169  			expectedPos: 1,
   170  		},
   171  		{
   172  			name:        "first char, ASCII string",
   173  			inputString: "abcd",
   174  			pos:         0,
   175  			count:       1,
   176  			expectedPos: 0,
   177  		},
   178  		{
   179  			name:        "first char in line",
   180  			inputString: "ab\ncd",
   181  			pos:         3,
   182  			count:       1,
   183  			expectedPos: 3,
   184  		},
   185  		{
   186  			name:        "multi-char grapheme cluster",
   187  			inputString: "abe\u0301xyz",
   188  			pos:         4,
   189  			count:       1,
   190  			expectedPos: 2,
   191  		},
   192  		{
   193  			name:        "move cursor multiple chars within line",
   194  			inputString: "abcdefgh",
   195  			pos:         4,
   196  			count:       3,
   197  			expectedPos: 1,
   198  		},
   199  		{
   200  			name:        "move cursor multiple chars to beginning of line",
   201  			inputString: "ab\ncdefgh",
   202  			pos:         5,
   203  			count:       2,
   204  			expectedPos: 3,
   205  		},
   206  		{
   207  			name:        "move cursor multiple chars past beginning of line",
   208  			inputString: "ab\ncdefgh",
   209  			pos:         5,
   210  			count:       4,
   211  			expectedPos: 3,
   212  		},
   213  		{
   214  			name:                   "include end of previous line",
   215  			inputString:            "abcd\nefgh",
   216  			pos:                    5,
   217  			count:                  1,
   218  			includeEndOfLineOrFile: true,
   219  			expectedPos:            4,
   220  		},
   221  	}
   222  
   223  	for _, tc := range testCases {
   224  		t.Run(tc.name, func(t *testing.T) {
   225  			textTree, err := text.NewTreeFromString(tc.inputString)
   226  			require.NoError(t, err)
   227  			actualPos := PrevCharInLine(textTree, tc.count, tc.includeEndOfLineOrFile, tc.pos)
   228  			assert.Equal(t, tc.expectedPos, actualPos)
   229  		})
   230  	}
   231  }
   232  
   233  func TestPrevChar(t *testing.T) {
   234  	testCases := []struct {
   235  		name        string
   236  		inputString string
   237  		pos         uint64
   238  		count       uint64
   239  		expectedPos uint64
   240  	}{
   241  		{
   242  			name:        "empty string",
   243  			inputString: "",
   244  			pos:         0,
   245  			count:       1,
   246  			expectedPos: 0,
   247  		},
   248  		{
   249  			name:        "back single char, same line",
   250  			inputString: "abc\ndef",
   251  			pos:         5,
   252  			count:       1,
   253  			expectedPos: 4,
   254  		},
   255  		{
   256  			name:        "back single char, prev line",
   257  			inputString: "abc\ndef",
   258  			pos:         3,
   259  			count:       1,
   260  			expectedPos: 2,
   261  		},
   262  		{
   263  			name:        "back multi-char grapheme cluster",
   264  			inputString: "e\u0301xyz",
   265  			pos:         2,
   266  			count:       1,
   267  			expectedPos: 0,
   268  		},
   269  		{
   270  			name:        "back multiple chars, within document",
   271  			inputString: "abc\ndef",
   272  			pos:         5,
   273  			count:       3,
   274  			expectedPos: 2,
   275  		},
   276  		{
   277  			name:        "back multiple chars, outside document",
   278  			inputString: "abc\ndef",
   279  			pos:         5,
   280  			count:       100,
   281  			expectedPos: 0,
   282  		},
   283  	}
   284  
   285  	for _, tc := range testCases {
   286  		t.Run(tc.name, func(t *testing.T) {
   287  			textTree, err := text.NewTreeFromString(tc.inputString)
   288  			require.NoError(t, err)
   289  			actualPos := PrevChar(textTree, tc.count, tc.pos)
   290  			assert.Equal(t, tc.expectedPos, actualPos)
   291  		})
   292  	}
   293  }
   294  
   295  func TestNextMatchingCharInLine(t *testing.T) {
   296  	testCases := []struct {
   297  		name        string
   298  		inputString string
   299  		char        rune
   300  		count       uint64
   301  		includeChar bool
   302  		pos         uint64
   303  		expectFound bool
   304  		expectedPos uint64
   305  	}{
   306  		{
   307  			name:        "empty string",
   308  			inputString: "",
   309  			char:        'x',
   310  			count:       1,
   311  			pos:         0,
   312  			expectFound: false,
   313  			expectedPos: 0,
   314  		},
   315  		{
   316  			name:        "not found on first line",
   317  			inputString: "abcxyz",
   318  			char:        'm',
   319  			count:       1,
   320  			pos:         1,
   321  			expectFound: false,
   322  			expectedPos: 0,
   323  		},
   324  		{
   325  			name:        "count zero finds nothing",
   326  			inputString: "abcxyz",
   327  			char:        'x',
   328  			count:       0,
   329  			pos:         1,
   330  			expectFound: false,
   331  			expectedPos: 0,
   332  		},
   333  		{
   334  			name:        "found on first line, include",
   335  			inputString: "abcxyz",
   336  			char:        'x',
   337  			count:       1,
   338  			includeChar: true,
   339  			pos:         1,
   340  			expectFound: true,
   341  			expectedPos: 3,
   342  		},
   343  		{
   344  			name:        "found on first line, exclude",
   345  			inputString: "abcxyz",
   346  			char:        'x',
   347  			count:       1,
   348  			includeChar: false,
   349  			pos:         1,
   350  			expectFound: true,
   351  			expectedPos: 2,
   352  		},
   353  		{
   354  			name:        "found on first line, count > 0",
   355  			inputString: "abcxyzxyz",
   356  			char:        'x',
   357  			count:       2,
   358  			includeChar: true,
   359  			pos:         1,
   360  			expectFound: true,
   361  			expectedPos: 6,
   362  		},
   363  		{
   364  			name:        "next match on subsequent line",
   365  			inputString: "abc\nxyz",
   366  			char:        'x',
   367  			count:       1,
   368  			includeChar: true,
   369  			pos:         1,
   370  			expectFound: false,
   371  			expectedPos: 0,
   372  		},
   373  		{
   374  			name:        "match at end of current line",
   375  			inputString: "abc\nabx\nyz",
   376  			char:        'x',
   377  			count:       1,
   378  			includeChar: true,
   379  			pos:         4,
   380  			expectFound: true,
   381  			expectedPos: 6,
   382  		},
   383  		{
   384  			name:        "no match character same as under cursor",
   385  			inputString: "ab",
   386  			char:        'a',
   387  			count:       1,
   388  			includeChar: false,
   389  			pos:         0,
   390  			expectFound: false,
   391  			expectedPos: 0,
   392  		},
   393  		{
   394  			name:        "match character same as under cursor",
   395  			inputString: "xaaaaaaaxbbbb",
   396  			char:        'x',
   397  			count:       1,
   398  			includeChar: false,
   399  			pos:         0,
   400  			expectFound: true,
   401  			expectedPos: 7,
   402  		},
   403  		{
   404  			name:        "match next character same as character under cursor",
   405  			inputString: "aab",
   406  			char:        'a',
   407  			count:       1,
   408  			includeChar: false,
   409  			pos:         0,
   410  			expectFound: true,
   411  			expectedPos: 0,
   412  		},
   413  	}
   414  
   415  	for _, tc := range testCases {
   416  		t.Run(tc.name, func(t *testing.T) {
   417  			textTree, err := text.NewTreeFromString(tc.inputString)
   418  			require.NoError(t, err)
   419  			found, actualPos := NextMatchingCharInLine(textTree, tc.char, tc.count, tc.includeChar, tc.pos)
   420  			assert.Equal(t, tc.expectFound, found)
   421  			assert.Equal(t, tc.expectedPos, actualPos)
   422  		})
   423  	}
   424  }
   425  
   426  func TestPrevMatchingCharInLine(t *testing.T) {
   427  	testCases := []struct {
   428  		name        string
   429  		inputString string
   430  		char        rune
   431  		count       uint64
   432  		includeChar bool
   433  		pos         uint64
   434  		expectFound bool
   435  		expectedPos uint64
   436  	}{
   437  		{
   438  			name:        "empty string",
   439  			inputString: "",
   440  			char:        'x',
   441  			count:       1,
   442  			pos:         0,
   443  			expectFound: false,
   444  			expectedPos: 0,
   445  		},
   446  		{
   447  			name:        "not found on first line",
   448  			inputString: "abcxyz",
   449  			char:        'm',
   450  			count:       1,
   451  			pos:         5,
   452  			expectFound: false,
   453  			expectedPos: 0,
   454  		},
   455  		{
   456  			name:        "count zero finds nothing",
   457  			inputString: "abcxyz",
   458  			char:        'x',
   459  			count:       0,
   460  			pos:         5,
   461  			expectFound: false,
   462  			expectedPos: 0,
   463  		},
   464  		{
   465  			name:        "found on first line, include",
   466  			inputString: "abcxyz",
   467  			char:        'x',
   468  			count:       1,
   469  			includeChar: true,
   470  			pos:         5,
   471  			expectFound: true,
   472  			expectedPos: 3,
   473  		},
   474  		{
   475  			name:        "found on first line, exclude",
   476  			inputString: "abcxyz",
   477  			char:        'x',
   478  			count:       1,
   479  			includeChar: false,
   480  			pos:         5,
   481  			expectFound: true,
   482  			expectedPos: 4,
   483  		},
   484  		{
   485  			name:        "found on first line, count > 0",
   486  			inputString: "abcxyzxyz",
   487  			char:        'x',
   488  			count:       2,
   489  			includeChar: true,
   490  			pos:         8,
   491  			expectFound: true,
   492  			expectedPos: 3,
   493  		},
   494  		{
   495  			name:        "next match on previous line",
   496  			inputString: "abcx\nyz",
   497  			char:        'x',
   498  			count:       1,
   499  			includeChar: true,
   500  			pos:         6,
   501  			expectFound: false,
   502  			expectedPos: 0,
   503  		},
   504  		{
   505  			name:        "match at start of current line",
   506  			inputString: "abc\nxab\nyz",
   507  			char:        'x',
   508  			count:       1,
   509  			includeChar: true,
   510  			pos:         6,
   511  			expectFound: true,
   512  			expectedPos: 4,
   513  		},
   514  	}
   515  
   516  	for _, tc := range testCases {
   517  		t.Run(tc.name, func(t *testing.T) {
   518  			textTree, err := text.NewTreeFromString(tc.inputString)
   519  			require.NoError(t, err)
   520  			found, actualPos := PrevMatchingCharInLine(textTree, tc.char, tc.count, tc.includeChar, tc.pos)
   521  			assert.Equal(t, tc.expectFound, found)
   522  			assert.Equal(t, tc.expectedPos, actualPos)
   523  		})
   524  	}
   525  }
   526  
   527  func TestPrevAutoIndent(t *testing.T) {
   528  	testCases := []struct {
   529  		name              string
   530  		inputString       string
   531  		autoIndentEnabled bool
   532  		pos               uint64
   533  		expectedPos       uint64
   534  	}{
   535  		{
   536  			name:        "empty string",
   537  			inputString: "",
   538  			pos:         0,
   539  			expectedPos: 0,
   540  		},
   541  		{
   542  			name:        "multiple tabs, autoindent disabled",
   543  			inputString: "\t\t",
   544  			pos:         2,
   545  			expectedPos: 2,
   546  		},
   547  		{
   548  			name:              "single space, autoindent enabled",
   549  			inputString:       " ",
   550  			autoIndentEnabled: true,
   551  			pos:               1,
   552  			expectedPos:       0,
   553  		},
   554  		{
   555  			name:              "multiple spaces, autoindent enabled",
   556  			inputString:       "        ",
   557  			autoIndentEnabled: true,
   558  			pos:               8,
   559  			expectedPos:       4,
   560  		},
   561  		{
   562  			name:              "multiple tabs, autoindent enabled",
   563  			inputString:       "\t\t",
   564  			autoIndentEnabled: true,
   565  			pos:               2,
   566  			expectedPos:       1,
   567  		},
   568  		{
   569  			name:              "mixed tabs and spaces, autoindent enabled",
   570  			inputString:       " \t",
   571  			autoIndentEnabled: true,
   572  			pos:               2,
   573  			expectedPos:       0,
   574  		},
   575  		{
   576  			name:              "no tabs or spaces, autoindent enabled",
   577  			inputString:       "ab",
   578  			autoIndentEnabled: true,
   579  			pos:               2,
   580  			expectedPos:       2,
   581  		},
   582  		{
   583  			name:              "start of line, autoindent enabled",
   584  			inputString:       "ab\ncd",
   585  			autoIndentEnabled: true,
   586  			pos:               2,
   587  			expectedPos:       2,
   588  		},
   589  		{
   590  			name:              "end of document, autoindent enabled",
   591  			inputString:       "ab\n\n",
   592  			autoIndentEnabled: true,
   593  			pos:               3,
   594  			expectedPos:       3,
   595  		},
   596  		{
   597  			name:              "spaces within line aligned, autoindent enabled",
   598  			inputString:       "abcd    ef",
   599  			autoIndentEnabled: true,
   600  			pos:               8,
   601  			expectedPos:       4,
   602  		},
   603  		{
   604  			name:              "spaces within line misaligned, autoindent enabled",
   605  			inputString:       "ab    cd",
   606  			autoIndentEnabled: true,
   607  			pos:               6,
   608  			expectedPos:       4,
   609  		},
   610  		{
   611  			name:              "tabs within line, autoindent enabled",
   612  			inputString:       "ab\t\tcd",
   613  			autoIndentEnabled: true,
   614  			pos:               4,
   615  			expectedPos:       3,
   616  		},
   617  		{
   618  			name:              "spaces within line but not before cursor, autoindent enabled",
   619  			inputString:       "ab    cdef",
   620  			autoIndentEnabled: true,
   621  			pos:               7,
   622  			expectedPos:       7,
   623  		},
   624  		{
   625  			name:              "spaces at end of line less than tab size, autoindent enabled",
   626  			inputString:       "abcdef  ",
   627  			autoIndentEnabled: true,
   628  			pos:               8,
   629  			expectedPos:       6,
   630  		},
   631  	}
   632  
   633  	for _, tc := range testCases {
   634  		t.Run(tc.name, func(t *testing.T) {
   635  			textTree, err := text.NewTreeFromString(tc.inputString)
   636  			require.NoError(t, err)
   637  			actualPos := PrevAutoIndent(textTree, tc.autoIndentEnabled, 4, tc.pos)
   638  			assert.Equal(t, tc.expectedPos, actualPos)
   639  		})
   640  	}
   641  }
   642  
   643  func TestNextNonWhitespaceOrNewline(t *testing.T) {
   644  	testCases := []struct {
   645  		name        string
   646  		inputString string
   647  		pos         uint64
   648  		expectedPos uint64
   649  	}{
   650  		{
   651  			name:        "empty",
   652  			inputString: "",
   653  			pos:         0,
   654  			expectedPos: 0,
   655  		},
   656  		{
   657  			name:        "no movement",
   658  			inputString: "   abcd   ",
   659  			pos:         4,
   660  			expectedPos: 4,
   661  		},
   662  		{
   663  			name:        "movement",
   664  			inputString: "   abcd   ",
   665  			pos:         1,
   666  			expectedPos: 3,
   667  		},
   668  		{
   669  			name:        "stop before newline on empty line",
   670  			inputString: "abcd\n\n\nefgh",
   671  			pos:         5,
   672  			expectedPos: 5,
   673  		},
   674  		{
   675  			name:        "stop before newline at end of line",
   676  			inputString: "abcd\nefghi",
   677  			pos:         3,
   678  			expectedPos: 3,
   679  		},
   680  	}
   681  
   682  	for _, tc := range testCases {
   683  		t.Run(tc.name, func(t *testing.T) {
   684  			textTree, err := text.NewTreeFromString(tc.inputString)
   685  			require.NoError(t, err)
   686  			actualPos := NextNonWhitespaceOrNewline(textTree, tc.pos)
   687  			assert.Equal(t, tc.expectedPos, actualPos)
   688  		})
   689  	}
   690  }
   691  
   692  func TestNextNewline(t *testing.T) {
   693  	testCases := []struct {
   694  		name        string
   695  		inputString string
   696  		pos         uint64
   697  		expectedOk  bool
   698  		expectedPos uint64
   699  		expectedLen uint64
   700  	}{
   701  		{
   702  			name:        "empty",
   703  			inputString: "",
   704  			pos:         0,
   705  			expectedOk:  false,
   706  		},
   707  		{
   708  			name:        "last line",
   709  			inputString: "abcd",
   710  			pos:         2,
   711  			expectedOk:  false,
   712  		},
   713  		{
   714  			name:        "before LF",
   715  			inputString: "abc\ndef",
   716  			pos:         1,
   717  			expectedOk:  true,
   718  			expectedPos: 3,
   719  			expectedLen: 1,
   720  		},
   721  		{
   722  			name:        "on LF",
   723  			inputString: "abc\ndef",
   724  			pos:         3,
   725  			expectedOk:  true,
   726  			expectedPos: 3,
   727  			expectedLen: 1,
   728  		},
   729  		{
   730  			name:        "before CR LF",
   731  			inputString: "abc\r\ndef",
   732  			pos:         1,
   733  			expectedOk:  true,
   734  			expectedPos: 3,
   735  			expectedLen: 2,
   736  		},
   737  		{
   738  			name:        "on CR LF",
   739  			inputString: "abc\r\ndef",
   740  			pos:         3,
   741  			expectedOk:  true,
   742  			expectedPos: 3,
   743  			expectedLen: 2,
   744  		},
   745  	}
   746  	for _, tc := range testCases {
   747  		t.Run(tc.name, func(t *testing.T) {
   748  			textTree, err := text.NewTreeFromString(tc.inputString)
   749  			require.NoError(t, err)
   750  			actualPos, actualLen, actualOk := NextNewline(textTree, tc.pos)
   751  			assert.Equal(t, tc.expectedOk, actualOk)
   752  			assert.Equal(t, tc.expectedPos, actualPos)
   753  			assert.Equal(t, tc.expectedLen, actualLen)
   754  		})
   755  	}
   756  }
   757  
   758  func TestNumGraphemeClustersInRange(t *testing.T) {
   759  	testCases := []struct {
   760  		name          string
   761  		inputString   string
   762  		startPos      uint64
   763  		endPos        uint64
   764  		expectedCount uint64
   765  	}{
   766  		{
   767  			name:          "empty text",
   768  			inputString:   "",
   769  			startPos:      0,
   770  			endPos:        0,
   771  			expectedCount: 0,
   772  		},
   773  		{
   774  			name:          "empty range",
   775  			inputString:   "abcdefgh",
   776  			startPos:      1,
   777  			endPos:        1,
   778  			expectedCount: 0,
   779  		},
   780  		{
   781  			name:          "single-rune grapheme clusters",
   782  			inputString:   "abcdefgh",
   783  			startPos:      1,
   784  			endPos:        4,
   785  			expectedCount: 3,
   786  		},
   787  		{
   788  			name:          "multi-rune grapheme clusters",
   789  			inputString:   "ᄀ̈각각̈͏",
   790  			startPos:      0,
   791  			endPos:        6,
   792  			expectedCount: 3,
   793  		},
   794  		{
   795  			name:          "past end of file",
   796  			inputString:   "abcdefgh",
   797  			startPos:      3,
   798  			endPos:        100,
   799  			expectedCount: 5,
   800  		},
   801  	}
   802  
   803  	for _, tc := range testCases {
   804  		t.Run(tc.name, func(t *testing.T) {
   805  			textTree, err := text.NewTreeFromString(tc.inputString)
   806  			require.NoError(t, err)
   807  			actualCount := NumGraphemeClustersInRange(textTree, tc.startPos, tc.endPos)
   808  			assert.Equal(t, tc.expectedCount, actualCount)
   809  		})
   810  	}
   811  }