3 files changed,
94 insertions(+),
109 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-08-03 12:52:15 +0300
Authored at:
2026-08-02 21:09:51 +0300
Change ID:
ynnxqzrrkmuyxuuwsxtpmzpqowpostln
Parent:
03eda2a
M
internal/lsp/lsputil/utf16_test.go
··· 1 1 package lsputil 2 2 3 3 import ( 4 + "fmt" 4 5 "testing" 5 6 "unicode/utf8" 6 7 ) 7 8 8 -func TestUtf16Col_Basic(t *testing.T) { 9 - col := Utf16Col("hello", 3) 10 - if col != 3 { 11 - t.Errorf("Utf16Col for ASCII = %d, want 3", col) 9 +func TestUtf16Col(t *testing.T) { 10 + tests := map[string]struct { 11 + line string 12 + offset int 13 + want int 14 + }{ 15 + "ascii": {"hello", 3, 3}, 16 + "cyrillic full line": {"Привіт", len("Привіт"), 6}, 12 17 } 13 -} 14 - 15 -func TestUtf16Col_Cyrillic(t *testing.T) { 16 - // "Привет" is 6 Cyrillic chars = 12 bytes, 6 UTF-16 code units 17 - line := "Привет" 18 - col := Utf16Col(line, len(line)) 19 - if col != 6 { 20 - t.Errorf("Utf16Col for 'Привет' = %d, want 6", col) 18 + for name, tt := range tests { 19 + t.Run(name, func(t *testing.T) { 20 + if got := Utf16Col(tt.line, tt.offset); got != tt.want { 21 + t.Errorf("Utf16Col(%q, %d) = %d, want %d", tt.line, tt.offset, got, tt.want) 22 + } 23 + }) 21 24 } 22 25 } 23 26 24 -func TestUtf16Len_ASCII(t *testing.T) { 25 - l := Utf16Len("hello world", 0, 5) 26 - if l != 5 { 27 - t.Errorf("Utf16Len = %d, want 5", l) 27 +func TestUtf16Len(t *testing.T) { 28 + tests := map[string]struct { 29 + content string 30 + offset int 31 + end int 32 + want int 33 + }{ 34 + "ascii": {"hello world", 0, 5, 5}, 35 + "cyrillic": {"Привіт світ", 0, len("Привіт"), 6}, 28 36 } 29 -} 30 - 31 -func TestUtf16Len_Cyrillic(t *testing.T) { 32 - // "Привет" = 6 UTF-16 code units 33 - l := Utf16Len("Привет мир", 0, 12) 34 - if l != 6 { 35 - t.Errorf("Utf16Len for 'Привет' = %d, want 6", l) 37 + for name, tt := range tests { 38 + t.Run(name, func(t *testing.T) { 39 + if got := Utf16Len(tt.content, tt.offset, tt.end); got != tt.want { 40 + t.Errorf("Utf16Len(%q, %d, %d) = %d, want %d", tt.content, tt.offset, tt.end, got, tt.want) 41 + } 42 + }) 36 43 } 37 44 } 38 45 39 -func TestLineCol_Basic(t *testing.T) { 40 - line, col := LineCol("hello\nworld", 11) 41 - if line != 1 || col != 5 { 42 - t.Errorf("LineCol = (%d,%d), want (1,5)", line, col) 46 +func TestLineCol(t *testing.T) { 47 + tests := map[string]struct { 48 + content string 49 + offset int 50 + line, col int 51 + }{ 52 + "basic": {"hello\nworld", 11, 1, 5}, 53 + "first line": {"hello world", 5, 0, 5}, 54 + "line start": {"ab\ncd", 3, 1, 0}, 55 + "newline ends line": {"ab\ncd", 2, 0, 2}, 56 + "past blank line": {"a\n\nb", 3, 2, 0}, 57 + "blank line col": {"a\n\nb", 2, 1, 0}, 58 + "CRLF line start": {"ab\r\ncd", 4, 1, 0}, 59 + "crlf newline ends": {"ab\r\ncd", 2, 0, 2}, 43 60 } 44 -} 45 - 46 -func TestLineCol_FirstLine(t *testing.T) { 47 - line, col := LineCol("hello world", 5) 48 - if line != 0 || col != 5 { 49 - t.Errorf("LineCol = (%d,%d), want (0,5)", line, col) 61 + for name, tt := range tests { 62 + t.Run(name, func(t *testing.T) { 63 + line, col := LineCol(tt.content, tt.offset) 64 + if line != tt.line || col != tt.col { 65 + t.Errorf("LineCol(%q, %d) = (%d,%d), want (%d,%d)", tt.content, tt.offset, line, col, tt.line, tt.col) 66 + } 67 + }) 50 68 } 51 69 } 52 70 53 -func TestLineCol_LineStart(t *testing.T) { 54 - // offset at the first char of line 1 must be (1,0), not the tail of line 0 55 - line, col := LineCol("ab\ncd", 3) 56 - if line != 1 || col != 0 { 57 - t.Errorf("LineCol(3) = (%d,%d), want (1,0)", line, col) 71 +func TestOffset(t *testing.T) { 72 + tests := map[string]struct { 73 + content string 74 + line, col int 75 + want int 76 + }{ 77 + "basic": {"hello\nworld", 1, 5, 11}, 78 + "past EOL col": {"ab\ncd", 0, 99, 2}, 79 + "cyrillic": {"Привіт😀", 0, 6, 12}, 80 + "emoji": {"Привіт😀", 0, 7, 16}, 58 81 } 59 - // newline char itself belongs to the line it ends 60 - line, col = LineCol("ab\ncd", 2) 61 - if line != 0 || col != 2 { 62 - t.Errorf("LineCol(2) = (%d,%d), want (0,2)", line, col) 82 + for name, tt := range tests { 83 + t.Run(name, func(t *testing.T) { 84 + if got := Offset(tt.content, tt.line, tt.col); got != tt.want { 85 + t.Errorf("Offset(%d,%d) = %d, want %d", tt.line, tt.col, got, tt.want) 86 + } 87 + }) 63 88 } 64 -} 65 89 66 -func TestLineCol_BlankLine(t *testing.T) { 67 - // "a\n\nb": line 1 is blank; offset 3 is the start of line 2 68 - line, col := LineCol("a\n\nb", 3) 69 - if line != 2 || col != 0 { 70 - t.Errorf("LineCol(3) = (%d,%d), want (2,0)", line, col) 71 - } 72 - line, col = LineCol("a\n\nb", 2) 73 - if line != 1 || col != 0 { 74 - t.Errorf("LineCol(2) = (%d,%d), want (1,0)", line, col) 75 - } 76 -} 77 - 78 -func TestLineCol_CRLF(t *testing.T) { 79 - line, col := LineCol("ab\r\ncd", 4) 80 - if line != 1 || col != 0 { 81 - t.Errorf("LineCol(4) = (%d,%d), want (1,0)", line, col) 82 - } 83 - line, col = LineCol("ab\r\ncd", 2) 84 - if line != 0 || col != 2 { 85 - t.Errorf("LineCol(2) = (%d,%d), want (0,2)", line, col) 86 - } 87 -} 88 - 89 -func TestOffset_Basic(t *testing.T) { 90 - if got := Offset("hello\nworld", 1, 5); got != 11 { 91 - t.Errorf("Offset(1,5) = %d, want 11", got) 92 - } 90 + t.Run("past EOF line", func(t *testing.T) { 91 + if got := Offset("ab\ncd", 5, 0); got != len("ab\ncd") { 92 + t.Errorf("Offset = %d, want %d", got, len("ab\ncd")) 93 + } 94 + }) 93 95 } 94 96 95 97 func TestOffset_RoundTrip(t *testing.T) { 96 - content := "first\nПривет мир\r\nlast\n" 97 - // only rune-boundary offsets round-trip (LineCol clamps mid-rune offsets) 98 + content := "first\nПривіт світ\r\nlast\n" 99 + 98 100 var boundaries []int 99 101 for i := 0; i < len(content); { 100 102 _, size := utf8.DecodeRuneInString(content[i:]) 101 - if !(content[i] == '\n' && i > 0 && content[i-1] == '\r') { 103 + if content[i] != '\n' || i <= 0 || content[i-1] != '\r' { 102 104 boundaries = append(boundaries, i) 103 105 } 104 106 i += size 105 107 } 106 - for _, off := range boundaries { 107 - line, col := LineCol(content, off) 108 - if got := Offset(content, line, col); got != off { 109 - t.Errorf("round trip at %d: Offset(LineCol(%d)) = %d", off, off, got) 110 - } 111 - } 112 -} 113 108 114 -func TestOffset_Clamps(t *testing.T) { 115 - content := "ab\ncd" 116 - if got := Offset(content, 5, 0); got != len(content) { 117 - t.Errorf("past EOF line: Offset = %d, want %d", got, len(content)) 118 - } 119 - // column beyond line end clamps to line end 120 - if got := Offset(content, 0, 99); got != 2 { 121 - t.Errorf("past EOL col: Offset = %d, want 2", got) 122 - } 123 -} 124 - 125 -func TestOffset_UTF16(t *testing.T) { 126 - // Cyrillic chars are 1 UTF-16 unit each; emoji are 2 127 - content := "Привет😀" 128 - if got := Offset(content, 0, 6); got != 12 { 129 - t.Errorf("Offset after Cyrillic = %d, want 12", got) 130 - } 131 - if got := Offset(content, 0, 7); got != 16 { 132 - t.Errorf("Offset after emoji (2 UTF-16 units) = %d, want 16", got) 109 + for _, off := range boundaries { 110 + t.Run(fmt.Sprintf("round trip at %d", off), func(t *testing.T) { 111 + line, col := LineCol(content, off) 112 + if got := Offset(content, line, col); got != off { 113 + t.Errorf("round trip at %d: Offset(LineCol(%d)) = %d", off, off, got) 114 + } 115 + }) 133 116 } 134 117 } 135 118 136 -func TestPosition_RoundTrip(t *testing.T) { 137 - content := "2024-01-15 Супермаркет\r\n Витрати:Продукти ¥50\n" 119 +func TestPosition(t *testing.T) { 120 + content := "2024-01-15 Супермаркет\r\n Витрати:Продукти ¥50\n" 138 121 for _, off := range []int{0, 10, len("2024-01-15 Супермаркет"), len(content)} { 139 - p := Position(content, off) 140 - if got := Offset(content, int(p.Line), int(p.Character)); got != off { 141 - t.Errorf("round trip of %d = %d (pos %d:%d), want identity", off, got, p.Line, p.Character) 142 - } 122 + t.Run(fmt.Sprintf("round trip at %d", off), func(t *testing.T) { 123 + p := Position(content, off) 124 + if got := Offset(content, int(p.Line), int(p.Character)); got != off { 125 + t.Errorf("round trip of %d = %d (pos %d:%d), want identity", off, got, p.Line, p.Character) 126 + } 127 + }) 143 128 } 144 129 }
M
internal/lsp/testdata/semantic-journal.txtar
··· 80 80 2020-02-14 * more 81 81 82 82 2024-01-15 午餐 83 -; Привет мир 84 -2024-01-15 Такси 83 +; Привіт світ 84 +2024-01-15 Таксі 85 85 86 86 ~ monthly 87 87 a $10 ··· 317 317 78:13+4 property 318 318 80:0+10 class 319 319 80:11+2 property 320 -81:0+13 comment 320 +81:0+14 comment 321 321 82:0+10 class 322 322 82:11+5 property 323 323 84:0+1 operator