golang.org/x/tools/gopls@v0.15.3/internal/protocol/mapper_test.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package protocol_test
     6  
     7  import (
     8  	"fmt"
     9  	"strings"
    10  	"testing"
    11  
    12  	"golang.org/x/tools/gopls/internal/protocol"
    13  )
    14  
    15  // This file tests Mapper's logic for converting between offsets,
    16  // UTF-8 columns, and UTF-16 columns. (The strange form attests to
    17  // earlier abstractions.)
    18  
    19  // 𐐀 is U+10400 = [F0 90 90 80] in UTF-8, [D801 DC00] in UTF-16.
    20  var funnyString = []byte("𐐀23\n𐐀45")
    21  
    22  var toUTF16Tests = []struct {
    23  	scenario    string
    24  	input       []byte
    25  	line        int    // 1-indexed count
    26  	col         int    // 1-indexed byte position in line
    27  	offset      int    // 0-indexed byte offset into input
    28  	resUTF16col int    // 1-indexed UTF-16 col number
    29  	pre         string // everything before the cursor on the line
    30  	post        string // everything from the cursor onwards
    31  	err         string // expected error string in call to ToUTF16Column
    32  	issue       *bool
    33  }{
    34  	{
    35  		scenario: "cursor missing content",
    36  		input:    nil,
    37  		offset:   -1,
    38  		err:      "point has neither offset nor line/column",
    39  	},
    40  	{
    41  		scenario: "cursor missing position",
    42  		input:    funnyString,
    43  		line:     -1,
    44  		col:      -1,
    45  		offset:   -1,
    46  		err:      "point has neither offset nor line/column",
    47  	},
    48  	{
    49  		scenario:    "zero length input; cursor at first col, first line",
    50  		input:       []byte(""),
    51  		line:        1,
    52  		col:         1,
    53  		offset:      0,
    54  		resUTF16col: 1,
    55  	},
    56  	{
    57  		scenario:    "cursor before funny character; first line",
    58  		input:       funnyString,
    59  		line:        1,
    60  		col:         1,
    61  		offset:      0,
    62  		resUTF16col: 1,
    63  		pre:         "",
    64  		post:        "𐐀23",
    65  	},
    66  	{
    67  		scenario:    "cursor after funny character; first line",
    68  		input:       funnyString,
    69  		line:        1,
    70  		col:         5, // 4 + 1 (1-indexed)
    71  		offset:      4, // (unused since we have line+col)
    72  		resUTF16col: 3, // 2 + 1 (1-indexed)
    73  		pre:         "𐐀",
    74  		post:        "23",
    75  	},
    76  	{
    77  		scenario:    "cursor after last character on first line",
    78  		input:       funnyString,
    79  		line:        1,
    80  		col:         7, // 4 + 1 + 1 + 1 (1-indexed)
    81  		offset:      6, // 4 + 1 + 1 (unused since we have line+col)
    82  		resUTF16col: 5, // 2 + 1 + 1 + 1 (1-indexed)
    83  		pre:         "𐐀23",
    84  		post:        "",
    85  	},
    86  	{
    87  		scenario:    "cursor before funny character; second line",
    88  		input:       funnyString,
    89  		line:        2,
    90  		col:         1,
    91  		offset:      7, // length of first line (unused since we have line+col)
    92  		resUTF16col: 1,
    93  		pre:         "",
    94  		post:        "𐐀45",
    95  	},
    96  	{
    97  		scenario:    "cursor after funny character; second line",
    98  		input:       funnyString,
    99  		line:        1,
   100  		col:         5,  // 4 + 1 (1-indexed)
   101  		offset:      11, // 7 (length of first line) + 4 (unused since we have line+col)
   102  		resUTF16col: 3,  // 2 + 1 (1-indexed)
   103  		pre:         "𐐀",
   104  		post:        "45",
   105  	},
   106  	{
   107  		scenario:    "cursor after last character on second line",
   108  		input:       funnyString,
   109  		line:        2,
   110  		col:         7,  // 4 + 1 + 1 + 1 (1-indexed)
   111  		offset:      13, // 7 (length of first line) + 4 + 1 + 1 (unused since we have line+col)
   112  		resUTF16col: 5,  // 2 + 1 + 1 + 1 (1-indexed)
   113  		pre:         "𐐀45",
   114  		post:        "",
   115  	},
   116  	{
   117  		scenario: "cursor beyond end of file",
   118  		input:    funnyString,
   119  		line:     2,
   120  		col:      8,  // 4 + 1 + 1 + 1 + 1 (1-indexed)
   121  		offset:   14, // 4 + 1 + 1 + 1 (unused since we have line+col)
   122  		err:      "column is beyond end of file",
   123  	},
   124  }
   125  
   126  var fromUTF16Tests = []struct {
   127  	scenario  string
   128  	input     []byte
   129  	line      int    // 1-indexed line number (isn't actually used)
   130  	utf16col  int    // 1-indexed UTF-16 col number
   131  	resCol    int    // 1-indexed byte position in line
   132  	resOffset int    // 0-indexed byte offset into input
   133  	pre       string // everything before the cursor on the line
   134  	post      string // everything from the cursor onwards
   135  	err       string // expected error string in call to ToUTF16Column
   136  }{
   137  	{
   138  		scenario:  "zero length input; cursor at first col, first line",
   139  		input:     []byte(""),
   140  		line:      1,
   141  		utf16col:  1,
   142  		resCol:    1,
   143  		resOffset: 0,
   144  		pre:       "",
   145  		post:      "",
   146  	},
   147  	{
   148  		scenario:  "cursor before funny character",
   149  		input:     funnyString,
   150  		line:      1,
   151  		utf16col:  1,
   152  		resCol:    1,
   153  		resOffset: 0,
   154  		pre:       "",
   155  		post:      "𐐀23",
   156  	},
   157  	{
   158  		scenario:  "cursor after funny character",
   159  		input:     funnyString,
   160  		line:      1,
   161  		utf16col:  3,
   162  		resCol:    5,
   163  		resOffset: 4,
   164  		pre:       "𐐀",
   165  		post:      "23",
   166  	},
   167  	{
   168  		scenario:  "cursor after last character on line",
   169  		input:     funnyString,
   170  		line:      1,
   171  		utf16col:  5,
   172  		resCol:    7,
   173  		resOffset: 6,
   174  		pre:       "𐐀23",
   175  		post:      "",
   176  	},
   177  	{
   178  		scenario:  "cursor beyond last character on line",
   179  		input:     funnyString,
   180  		line:      1,
   181  		utf16col:  6,
   182  		resCol:    7,
   183  		resOffset: 6,
   184  		pre:       "𐐀23",
   185  		post:      "",
   186  		err:       "column is beyond end of line",
   187  	},
   188  	{
   189  		scenario:  "cursor before funny character; second line",
   190  		input:     funnyString,
   191  		line:      2,
   192  		utf16col:  1,
   193  		resCol:    1,
   194  		resOffset: 7,
   195  		pre:       "",
   196  		post:      "𐐀45",
   197  	},
   198  	{
   199  		scenario:  "cursor after funny character; second line",
   200  		input:     funnyString,
   201  		line:      2,
   202  		utf16col:  3,  // 2 + 1 (1-indexed)
   203  		resCol:    5,  // 4 + 1 (1-indexed)
   204  		resOffset: 11, // 7 (length of first line) + 4
   205  		pre:       "𐐀",
   206  		post:      "45",
   207  	},
   208  	{
   209  		scenario:  "cursor after last character on second line",
   210  		input:     funnyString,
   211  		line:      2,
   212  		utf16col:  5,  // 2 + 1 + 1 + 1 (1-indexed)
   213  		resCol:    7,  // 4 + 1 + 1 + 1 (1-indexed)
   214  		resOffset: 13, // 7 (length of first line) + 4 + 1 + 1
   215  		pre:       "𐐀45",
   216  		post:      "",
   217  	},
   218  	{
   219  		scenario:  "cursor beyond end of file",
   220  		input:     funnyString,
   221  		line:      2,
   222  		utf16col:  6,  // 2 + 1 + 1 + 1 + 1(1-indexed)
   223  		resCol:    8,  // 4 + 1 + 1 + 1 + 1 (1-indexed)
   224  		resOffset: 14, // 7 (length of first line) + 4 + 1 + 1 + 1
   225  		err:       "column is beyond end of file",
   226  	},
   227  }
   228  
   229  func TestToUTF16(t *testing.T) {
   230  	for _, e := range toUTF16Tests {
   231  		t.Run(e.scenario, func(t *testing.T) {
   232  			if e.issue != nil && !*e.issue {
   233  				t.Skip("expected to fail")
   234  			}
   235  			m := protocol.NewMapper("", e.input)
   236  			var pos protocol.Position
   237  			var err error
   238  			if e.line > 0 {
   239  				pos, err = m.LineCol8Position(e.line, e.col)
   240  			} else if e.offset >= 0 {
   241  				pos, err = m.OffsetPosition(e.offset)
   242  			} else {
   243  				err = fmt.Errorf("point has neither offset nor line/column")
   244  			}
   245  			if err != nil {
   246  				if err.Error() != e.err {
   247  					t.Fatalf("expected error %v; got %v", e.err, err)
   248  				}
   249  				return
   250  			}
   251  			if e.err != "" {
   252  				t.Fatalf("unexpected success; wanted %v", e.err)
   253  			}
   254  			got := int(pos.Character) + 1
   255  			if got != e.resUTF16col {
   256  				t.Fatalf("expected result %v; got %v", e.resUTF16col, got)
   257  			}
   258  			pre, post := getPrePost(e.input, e.offset)
   259  			if pre != e.pre {
   260  				t.Fatalf("expected #%d pre %q; got %q", e.offset, e.pre, pre)
   261  			}
   262  			if post != e.post {
   263  				t.Fatalf("expected #%d, post %q; got %q", e.offset, e.post, post)
   264  			}
   265  		})
   266  	}
   267  }
   268  
   269  func TestFromUTF16(t *testing.T) {
   270  	for _, e := range fromUTF16Tests {
   271  		t.Run(e.scenario, func(t *testing.T) {
   272  			m := protocol.NewMapper("", e.input)
   273  			offset, err := m.PositionOffset(protocol.Position{
   274  				Line:      uint32(e.line - 1),
   275  				Character: uint32(e.utf16col - 1),
   276  			})
   277  			if err != nil {
   278  				if err.Error() != e.err {
   279  					t.Fatalf("expected error %v; got %v", e.err, err)
   280  				}
   281  				return
   282  			}
   283  			if e.err != "" {
   284  				t.Fatalf("unexpected success; wanted %v", e.err)
   285  			}
   286  			if offset != e.resOffset {
   287  				t.Fatalf("expected offset %v; got %v", e.resOffset, offset)
   288  			}
   289  			line, col8 := m.OffsetLineCol8(offset)
   290  			if line != e.line {
   291  				t.Fatalf("expected resulting line %v; got %v", e.line, line)
   292  			}
   293  			if col8 != e.resCol {
   294  				t.Fatalf("expected resulting col %v; got %v", e.resCol, col8)
   295  			}
   296  			pre, post := getPrePost(e.input, offset)
   297  			if pre != e.pre {
   298  				t.Fatalf("expected #%d pre %q; got %q", offset, e.pre, pre)
   299  			}
   300  			if post != e.post {
   301  				t.Fatalf("expected #%d post %q; got %q", offset, e.post, post)
   302  			}
   303  		})
   304  	}
   305  }
   306  
   307  func getPrePost(content []byte, offset int) (string, string) {
   308  	pre, post := string(content)[:offset], string(content)[offset:]
   309  	if i := strings.LastIndex(pre, "\n"); i >= 0 {
   310  		pre = pre[i+1:]
   311  	}
   312  	if i := strings.IndexRune(post, '\n'); i >= 0 {
   313  		post = post[:i]
   314  	}
   315  	return pre, post
   316  }
   317  
   318  // -- these are the historical lsppos tests --
   319  
   320  type testCase struct {
   321  	content            string      // input text
   322  	substrOrOffset     interface{} // explicit integer offset, or a substring
   323  	wantLine, wantChar int         // expected LSP position information
   324  }
   325  
   326  // offset returns the test case byte offset
   327  func (c testCase) offset() int {
   328  	switch x := c.substrOrOffset.(type) {
   329  	case int:
   330  		return x
   331  	case string:
   332  		i := strings.Index(c.content, x)
   333  		if i < 0 {
   334  			panic(fmt.Sprintf("%q does not contain substring %q", c.content, x))
   335  		}
   336  		return i
   337  	}
   338  	panic("substrOrIndex must be an integer or string")
   339  }
   340  
   341  var tests = []testCase{
   342  	{"a𐐀b", "a", 0, 0},
   343  	{"a𐐀b", "𐐀", 0, 1},
   344  	{"a𐐀b", "b", 0, 3},
   345  	{"a𐐀b\n", "\n", 0, 4},
   346  	{"a𐐀b\r\n", "\n", 0, 4}, // \r|\n is not a valid position, so we move back to the end of the first line.
   347  	{"a𐐀b\r\nx", "x", 1, 0},
   348  	{"a𐐀b\r\nx\ny", "y", 2, 0},
   349  
   350  	// Testing EOL and EOF positions
   351  	{"", 0, 0, 0}, // 0th position of an empty buffer is (0, 0)
   352  	{"abc", "c", 0, 2},
   353  	{"abc", 3, 0, 3},
   354  	{"abc\n", "\n", 0, 3},
   355  	{"abc\n", 4, 1, 0}, // position after a newline is on the next line
   356  }
   357  
   358  func TestLineChar(t *testing.T) {
   359  	for _, test := range tests {
   360  		m := protocol.NewMapper("", []byte(test.content))
   361  		offset := test.offset()
   362  		posn, _ := m.OffsetPosition(offset)
   363  		gotLine, gotChar := int(posn.Line), int(posn.Character)
   364  		if gotLine != test.wantLine || gotChar != test.wantChar {
   365  			t.Errorf("LineChar(%d) = (%d,%d), want (%d,%d)", offset, gotLine, gotChar, test.wantLine, test.wantChar)
   366  		}
   367  	}
   368  }
   369  
   370  func TestInvalidOffset(t *testing.T) {
   371  	content := []byte("a𐐀b\r\nx\ny")
   372  	m := protocol.NewMapper("", content)
   373  	for _, offset := range []int{-1, 100} {
   374  		posn, err := m.OffsetPosition(offset)
   375  		if err == nil {
   376  			t.Errorf("OffsetPosition(%d) = %s, want error", offset, posn)
   377  		}
   378  	}
   379  }
   380  
   381  func TestPosition(t *testing.T) {
   382  	for _, test := range tests {
   383  		m := protocol.NewMapper("", []byte(test.content))
   384  		offset := test.offset()
   385  		got, err := m.OffsetPosition(offset)
   386  		if err != nil {
   387  			t.Errorf("OffsetPosition(%d) failed: %v", offset, err)
   388  			continue
   389  		}
   390  		want := protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)}
   391  		if got != want {
   392  			t.Errorf("Position(%d) = %v, want %v", offset, got, want)
   393  		}
   394  	}
   395  }
   396  
   397  func TestRange(t *testing.T) {
   398  	for _, test := range tests {
   399  		m := protocol.NewMapper("", []byte(test.content))
   400  		offset := test.offset()
   401  		got, err := m.OffsetRange(0, offset)
   402  		if err != nil {
   403  			t.Fatal(err)
   404  		}
   405  		want := protocol.Range{
   406  			End: protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)},
   407  		}
   408  		if got != want {
   409  			t.Errorf("Range(%d) = %v, want %v", offset, got, want)
   410  		}
   411  	}
   412  }
   413  
   414  func TestBytesOffset(t *testing.T) {
   415  	tests := []struct {
   416  		text string
   417  		pos  protocol.Position
   418  		want int
   419  	}{
   420  		// U+10400 encodes as [F0 90 90 80] in UTF-8 and [D801 DC00] in UTF-16.
   421  		{text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 0}, want: 0},
   422  		{text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 1}, want: 1},
   423  		{text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 2}, want: 1},
   424  		{text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 3}, want: 5},
   425  		{text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 4}, want: 6},
   426  		{text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 5}, want: -1},
   427  		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 3}, want: 3},
   428  		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 4}, want: -1},
   429  		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 0}, want: 4},
   430  		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 3}, want: 7},
   431  		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 4}, want: -1},
   432  		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8},
   433  		{text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 1}, want: -1},
   434  		{text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8},
   435  	}
   436  
   437  	for i, test := range tests {
   438  		fname := fmt.Sprintf("test %d", i)
   439  		uri := protocol.URIFromPath(fname)
   440  		mapper := protocol.NewMapper(uri, []byte(test.text))
   441  		got, err := mapper.PositionOffset(test.pos)
   442  		if err != nil && test.want != -1 {
   443  			t.Errorf("%d: unexpected error: %v", i, err)
   444  		}
   445  		if err == nil && got != test.want {
   446  			t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got)
   447  		}
   448  	}
   449  }