github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/internal/lsp/lsppos/lsppos.go (about)

     1  // Copyright 2021 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package lsppos provides utilities for working with LSP positions.
     6  //
     7  // See https://microsoft.github.io/language-server-protocol/specification#textDocuments
     8  // for a description of LSP positions. Notably:
     9  //  - Positions are specified by a 0-based line count and 0-based utf-16
    10  //    character offset.
    11  //  - Positions are line-ending agnostic: there is no way to specify \r|\n or
    12  //    \n|. Instead the former maps to the end of the current line, and the
    13  //    latter to the start of the next line.
    14  package lsppos
    15  
    16  import (
    17  	"sort"
    18  	"unicode/utf8"
    19  )
    20  
    21  type Mapper struct {
    22  	nonASCII bool
    23  	src      []byte
    24  
    25  	// Start-of-line positions. If src is newline-terminated, the final entry will be empty.
    26  	lines []int
    27  }
    28  
    29  func NewMapper(src []byte) *Mapper {
    30  	m := &Mapper{src: src}
    31  	if len(src) == 0 {
    32  		return m
    33  	}
    34  	m.lines = []int{0}
    35  	for offset, b := range src {
    36  		if b == '\n' {
    37  			m.lines = append(m.lines, offset+1)
    38  		}
    39  		if b >= utf8.RuneSelf {
    40  			m.nonASCII = true
    41  		}
    42  	}
    43  	return m
    44  }
    45  
    46  func (m *Mapper) Position(offset int) (line, char int) {
    47  	if offset < 0 || offset > len(m.src) {
    48  		return -1, -1
    49  	}
    50  	nextLine := sort.Search(len(m.lines), func(i int) bool {
    51  		return offset < m.lines[i]
    52  	})
    53  	if nextLine == 0 {
    54  		return -1, -1
    55  	}
    56  	line = nextLine - 1
    57  	start := m.lines[line]
    58  	var charOffset int
    59  	if m.nonASCII {
    60  		charOffset = UTF16len(m.src[start:offset])
    61  	} else {
    62  		charOffset = offset - start
    63  	}
    64  
    65  	var eol int
    66  	if line == len(m.lines)-1 {
    67  		eol = len(m.src)
    68  	} else {
    69  		eol = m.lines[line+1] - 1
    70  	}
    71  
    72  	// Adjustment for line-endings: \r|\n is the same as |\r\n.
    73  	if offset == eol && offset > 0 && m.src[offset-1] == '\r' {
    74  		charOffset--
    75  	}
    76  
    77  	return line, charOffset
    78  }
    79  
    80  func UTF16len(buf []byte) int {
    81  	cnt := 0
    82  	for _, r := range string(buf) {
    83  		cnt++
    84  		if r >= 1<<16 {
    85  			cnt++
    86  		}
    87  	}
    88  	return cnt
    89  }