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 }