▍ humdrum codex / glint v1.1.2
license AGPL-3.0
2.2 KB raw
 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
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
}

// 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
}