all repos

clerk @ 23d0a0ddf451ff6ac76c6dd2ffda2cd7d4fe34fc

missing tooling for ledger/hledger

clerk/internal/lsp/lsputil/lineindex.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
fix typos and grammar, 9 days ago
1
package lsputil
2
3
import (
4
	"sort"
5
	"unicode/utf8"
6
7
	"go.lsp.dev/protocol"
8
9
	"olexsmir.xyz/clerk/journal/token"
10
)
11
12
// LineIndex resolves byte offsets to LSP positions (and back) in O(log n) from
13
// a precomputed table of line starts, avoiding a per-request content scan.
14
type LineIndex struct {
15
	content string
16
	starts  []int // byte offset of each line's first byte; starts[0] == 0
17
}
18
19
func NewLineIndex(content string) *LineIndex {
20
	starts := make([]int, 1, len(content)/20+1)
21
	for i := 0; i < len(content); i++ {
22
		if content[i] == '\n' {
23
			starts = append(starts, i+1)
24
		}
25
	}
26
	return &LineIndex{content: content, starts: starts}
27
}
28
29
// Position converts a byte offset to a 0-based LSP position.
30
func (l *LineIndex) Position(offset int) protocol.Position {
31
	if offset < 0 {
32
		offset = 0
33
	}
34
	if offset > len(l.content) {
35
		offset = len(l.content)
36
	}
37
	idx := sort.Search(len(l.starts), func(i int) bool { return l.starts[i] > offset }) - 1
38
	lineStart := l.starts[idx]
39
	return protocol.Position{
40
		Line:      uint32(idx),
41
		Character: uint32(Utf16Col(l.content[lineStart:offset], offset-lineStart)),
42
	}
43
}
44
45
// Offset converts a 0-based line and UTF-16 code unit column to a byte offset,
46
// clamped to the content bounds. The inverse of Position; matches the
47
// standalone Offset on the same content.
48
func (l *LineIndex) Offset(line, col int) int {
49
	if line < 0 {
50
		line = 0
51
	}
52
	if line >= len(l.starts) {
53
		return len(l.content)
54
	}
55
	lineStart := l.starts[line]
56
	lineEnd := len(l.content)
57
	if line+1 < len(l.starts) {
58
		lineEnd = l.starts[line+1]
59
	}
60
	for lineEnd > lineStart && (l.content[lineEnd-1] == '\n' || l.content[lineEnd-1] == '\r') {
61
		lineEnd--
62
	}
63
	seg := l.content[lineStart:lineEnd]
64
	ascii := true
65
	for i := range seg {
66
		if seg[i] >= utf8.RuneSelf {
67
			ascii = false
68
			break
69
		}
70
	}
71
	if ascii {
72
		if col >= len(seg) {
73
			return lineEnd
74
		}
75
		return lineStart + col
76
	}
77
	off := lineStart
78
	units := 0
79
	for off < lineEnd && units < col {
80
		r, size := utf8.DecodeRuneInString(l.content[off:lineEnd])
81
		off += size
82
		units += Utf16LenRune(r)
83
	}
84
	return off
85
}
86
87
// SpanRange converts a span to a protocol range, trimming trailing whitespace and newlines from the end.
88
func (l *LineIndex) SpanRange(span token.Span) protocol.Range {
89
	return protocol.Range{
90
		Start: l.Position(span.Start.Offset),
91
		End:   l.Position(l.clampEnd(span.End.Offset)),
92
	}
93
}
94
95
func (l *LineIndex) clampEnd(end int) int {
96
	for end > 0 {
97
		switch l.content[end-1] {
98
		case ' ', '\t', '\r', '\n':
99
			end--
100
		default:
101
			return end
102
		}
103
	}
104
	return end
105
}