all repos

clerk @ 8909472

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: textdocument/selectionRange..., 1 month ago
1
package lsputil
2
3
import (
4
	"unicode/utf8"
5
6
	"go.lsp.dev/protocol"
7
)
8
9
// Utf16Col returns the UTF-16 code unit column (0-based) for a byte offset
10
// within the given line content (without newline). Pure-ASCII prefixes score
11
// one unit per byte without decoding.
12
func Utf16Col(line string, byteOffset int) int {
13
	if byteOffset <= 0 {
14
		return 0
15
	}
16
	if byteOffset > len(line) {
17
		byteOffset = len(line)
18
	}
19
	asc := line[:byteOffset]
20
	for i := range asc {
21
		if asc[i] >= utf8.RuneSelf {
22
			return utf16ColSlow(line, byteOffset)
23
		}
24
	}
25
	return byteOffset
26
}
27
28
func utf16ColSlow(line string, byteOffset int) int {
29
	col := 0
30
	for i := 0; i < byteOffset; {
31
		r, size := utf8.DecodeRuneInString(line[i:])
32
		if r == utf8.RuneError && size <= 1 {
33
			break
34
		}
35
		col += utf16Len(r)
36
		i += size
37
	}
38
	return col
39
}
40
41
// Utf16ColBytes returns the UTF-16 code unit column of b (without newline).
42
func Utf16ColBytes(b []byte) int {
43
	for _, c := range b {
44
		if c >= utf8.RuneSelf {
45
			return utf16ColBytesSlow(b)
46
		}
47
	}
48
	return len(b)
49
}
50
51
func utf16ColBytesSlow(b []byte) int {
52
	col := 0
53
	for i := 0; i < len(b); {
54
		r, size := utf8.DecodeRune(b[i:])
55
		if r == utf8.RuneError && size <= 1 {
56
			break
57
		}
58
		col += utf16Len(r)
59
		i += size
60
	}
61
	return col
62
}
63
64
// Utf16Len returns the UTF-16 code unit length of content[offset:end].
65
func Utf16Len(content string, offset, end int) int {
66
	if offset < 0 {
67
		offset = 0
68
	}
69
	if end > len(content) {
70
		end = len(content)
71
	}
72
	if offset >= end {
73
		return 0
74
	}
75
	seg := content[offset:end]
76
	for i := range seg {
77
		if seg[i] >= utf8.RuneSelf {
78
			return utf16LenSlow(content, offset, end)
79
		}
80
	}
81
	return end - offset
82
}
83
84
func utf16LenSlow(content string, offset, end int) int {
85
	n := 0
86
	for i := offset; i < end; {
87
		r, size := utf8.DecodeRuneInString(content[i:])
88
		if r == utf8.RuneError && size <= 1 {
89
			break
90
		}
91
		n += utf16Len(r)
92
		i += size
93
	}
94
	return n
95
}
96
97
// Offset converts a 0-based line and UTF-16 code unit column to a byte offset
98
// in content, clamped to the content bounds. The inverse of LineCol.
99
func Offset(content string, line, col int) int {
100
	if line < 0 {
101
		line = 0
102
	}
103
	off := 0
104
	for curLine := 0; curLine < line && off < len(content); {
105
		switch content[off] {
106
		case '\n':
107
			off++
108
			curLine++
109
		case '\r':
110
			off++
111
			if off < len(content) && content[off] == '\n' {
112
				off++
113
			}
114
			curLine++
115
		default:
116
			for off < len(content) && content[off] != '\n' && content[off] != '\r' {
117
				off++
118
			}
119
		}
120
	}
121
	lineEnd := off
122
	for lineEnd < len(content) && content[lineEnd] != '\n' && content[lineEnd] != '\r' {
123
		lineEnd++
124
	}
125
	units := 0
126
	for off < lineEnd && units < col {
127
		r, size := utf8.DecodeRuneInString(content[off:])
128
		off += size
129
		units += utf16Len(r)
130
	}
131
	return off
132
}
133
134
// LineCol converts a byte offset in content to 0-based line number and
135
// UTF-16 code unit column.
136
func LineCol(content string, offset int) (line int, col int) {
137
	if offset <= 0 {
138
		return 0, 0
139
	}
140
	if offset > len(content) {
141
		offset = len(content)
142
	}
143
	lineStart := 0
144
	lineNum := 0
145
	for i := 0; i < len(content); {
146
		r, size := utf8.DecodeRuneInString(content[i:])
147
		if r == utf8.RuneError && size <= 1 {
148
			break
149
		}
150
		if r == '\n' {
151
			if offset < i+size {
152
				col = Utf16Col(content[lineStart:offset], offset-lineStart)
153
				return lineNum, col
154
			}
155
			lineStart = i + size
156
			lineNum++
157
			i += size
158
			continue
159
		}
160
		if r == '\r' {
161
			// skip \r\n
162
			if i+1 < len(content) && content[i+1] == '\n' {
163
				size++
164
			}
165
			if offset < i+size {
166
				col = Utf16Col(content[lineStart:offset], offset-lineStart)
167
				return lineNum, col
168
			}
169
			lineStart = i + size
170
			lineNum++
171
			i += size
172
			continue
173
		}
174
		if i+size > offset {
175
			col = Utf16Col(content[lineStart:offset], offset-lineStart)
176
			return lineNum, col
177
		}
178
		i += size
179
	}
180
	col = Utf16Col(content[lineStart:], len(content)-lineStart)
181
	return lineNum, col
182
}
183
184
// Position converts a byte offset to an LSP position (0-based line, UTF-16
185
// code unit column). The inverse of Offset at the protocol.Position level.
186
func Position(content string, offset int) protocol.Position {
187
	line, col := LineCol(content, offset)
188
	return protocol.Position{Line: uint32(line), Character: uint32(col)}
189
}
190
191
// utf16Len returns the number of UTF-16 code units for a rune.
192
func utf16Len(r rune) int {
193
	if r >= 0x10000 && r <= 0x10FFFF {
194
		return 2
195
	}
196
	return 1
197
}