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 }