Something went wrong. Try again.
A Thrift language server, formatter, and linter.
lsp linter helix vim vscode thrift
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160package mapper
import ( "bytes" "fmt" "sort" "sync" "unicode/utf8"
"go.lsp.dev/protocol"
"github.com/karitham/thrift-ls/syntax")
type Mapper struct { content []byte
lineInit sync.Once lineStart []int // line start 0-based byte offset. lsp: 0-based, parser: 1-based nonASCII bool}
// NewMapper returns a Mapper for the given document content.func NewMapper(content []byte) *Mapper { return &Mapper{ content: content, }}
func (m *Mapper) initLineStart() { m.lineInit.Do(func() { nlines := bytes.Count(m.content, []byte("\n"))
m.lineStart = make([]int, 1, nlines+1) // initially []int{0} for offset, b := range m.content { if b == '\n' { m.lineStart = append(m.lineStart, offset+1) }
if b >= utf8.RuneSelf { m.nonASCII = true } } })}
// GetLSPEndPosition returns the position immediately after the last// character of the document: the last line (0-based) at the UTF-16 length// of its content. A document ending with a newline has an empty last line.func (m *Mapper) GetLSPEndPosition() protocol.Position { m.initLineStart() lastLineStart := m.lineStart[len(m.lineStart)-1] lastLine := m.content[lastLineStart:]
return protocol.Position{ Line: uint32(len(m.lineStart) - 1), Character: uint32(utf16Count(lastLine)), }}
// OffsetToLSPPosition converts a byte offset in the mapped content to an LSP// position (0-based line, UTF-16 code-unit column).func (m *Mapper) OffsetToLSPPosition(offset int) (protocol.Position, error) { m.initLineStart()
if offset < 0 || offset > len(m.content) { return protocol.Position{}, fmt.Errorf("invalid offset: %d, total content: %d", offset, len(m.content)) }
line := max(sort.Search(len(m.lineStart), func(i int) bool { return m.lineStart[i] > offset })-1, 0)
return protocol.Position{ Line: uint32(line), Character: uint32(utf16Count(m.content[m.lineStart[line]:offset])), }, nil}
// LSPPosToParserPosition converts an LSP position (0-based line, UTF-16// code-unit column) to a parser position (1-based line, rune-based column).//// Client positions are hints, not facts: they race with edits, so a column// past the line end clamps to the line end instead of failing. Only a line// past the document is an error — there is nothing to clamp to.func (m *Mapper) LSPPosToParserPosition(pos protocol.Position) (syntax.Position, error) { m.initLineStart()
line := int(pos.Line) + 1 if line > len(m.lineStart) { return syntax.InvalidPosition, fmt.Errorf("invalid position line, request line: %d, total line: %d", line, len(m.lineStart)) }
lineStart := m.lineStart[pos.Line] lineEnd := len(m.content) if line < len(m.lineStart) { lineEnd = m.lineStart[line] }
if !m.nonASCII { char := int(pos.Character) if lineLen := lineEnd - lineStart; char > lineLen { char = lineLen }
return syntax.Position{ Line: line, Col: char + 1, Offset: lineStart + char, }, nil }
lineBytes := m.content[lineStart:lineEnd]
utf16Col := 0 bytesCol := 0
for len(lineBytes) > 0 && utf16Col < int(pos.Character) { if lineBytes[0] < utf8.RuneSelf { utf16Col++ lineBytes = lineBytes[1:] bytesCol++
continue }
r, size := utf8.DecodeRune(lineBytes)
utf16Col++ if r >= 0x10000 { utf16Col++ }
lineBytes = lineBytes[size:] bytesCol += size }
runeLen := utf8.RuneCount(m.content[lineStart : lineStart+bytesCol])
return syntax.Position{ Line: line, Col: runeLen + 1, Offset: lineStart + bytesCol, }, nil}
func utf16Count(contents []byte) int { utf16Len := 0 for len(contents) > 0 { utf16Len++
r, size := utf8.DecodeRune(contents) if r >= 0x10000 { utf16Len++ }
contents = contents[size:] }
return utf16Len}