1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
package grammar
// Diag is one grammar issue for a single logical line, in glint's coordinate
// system: rune columns, not the LSP wire's UTF-16 units. Multi-line LSP ranges
// are split into one Diag per covered line before reaching this type.
type Diag struct {
Line int // 0-based logical line
StartCol int // 0-based rune column, inclusive
EndCol int // rune column, exclusive
Message string // human-readable description from Harper
Code string // Harper rule code (e.g. "RepeatedWords")
}
// utf16ToRuneCol converts a UTF-16 code-unit offset within line (LSP's default
// position encoding) to a rune column. Characters outside the BMP count as two
// UTF-16 units but one rune, so plain len-based indexing would drift once a doc
// contains emoji or other astral characters. An offset past the line's end
// clamps to the rune length.
func utf16ToRuneCol(line string, u16 int) int {
if u16 <= 0 {
return 0
}
units, col := 0, 0
for _, r := range line {
if units >= u16 {
return col
}
if r > 0xFFFF {
units += 2
} else {
units++
}
col++
}
return col
}
// runeColToUTF16 converts a rune column within line to a UTF-16 code-unit offset,
// the inverse of utf16ToRuneCol. It is used to phrase a glint rune range as an LSP
// position when requesting code actions. A column past the line's end clamps to the
// line's total UTF-16 length.
func runeColToUTF16(line string, col int) int {
if col <= 0 {
return 0
}
units, seen := 0, 0
for _, r := range line {
if seen >= col {
return units
}
if r > 0xFFFF {
units += 2
} else {
units++
}
seen++
}
return units
}
// lspRangeToDiags splits one LSP diagnostic range (start/end line+character in
// UTF-16 units) into per-line Diags with rune columns, using lines as the
// authoritative document text. A single-line range yields one Diag; a range that
// spans lines covers the start line from its column to end-of-line, every whole
// intermediate line, and the end line up to its column. Out-of-range lines are
// skipped so a stale diagnostic can never index past the buffer.
func lspRangeToDiags(lines []string, startLine, startChar, endLine, endChar int, msg, code string) []Diag {
line := func(i int) (string, bool) {
if i < 0 || i >= len(lines) {
return "", false
}
return lines[i], true
}
var out []Diag
for ln := startLine; ln <= endLine; ln++ {
text, ok := line(ln)
if !ok {
continue
}
runes := len([]rune(text))
start, end := 0, runes
if ln == startLine {
start = utf16ToRuneCol(text, startChar)
}
if ln == endLine {
end = utf16ToRuneCol(text, endChar)
}
if end > start {
out = append(out, Diag{Line: ln, StartCol: start, EndCol: end, Message: msg, Code: code})
}
}
return out
}
|