1// Copyright 2018 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
5package lsp
6
7import (
8	"context"
9	"go/token"
10	"net/url"
11
12	"golang.org/x/tools/internal/lsp/cache"
13	"golang.org/x/tools/internal/lsp/protocol"
14	"golang.org/x/tools/internal/lsp/source"
15)
16
17// fromProtocolURI converts a string to a source.URI.
18// TODO(rstambler): Add logic here to support Windows.
19func fromProtocolURI(uri string) (source.URI, error) {
20	unescaped, err := url.PathUnescape(string(uri))
21	if err != nil {
22		return "", err
23	}
24	return source.URI(unescaped), nil
25}
26
27// fromProtocolLocation converts from a protocol location to a source range.
28// It will return an error if the file of the location was not valid.
29// It uses fromProtocolRange to convert the start and end positions.
30func fromProtocolLocation(ctx context.Context, v *cache.View, loc protocol.Location) (source.Range, error) {
31	sourceURI, err := fromProtocolURI(loc.URI)
32	if err != nil {
33		return source.Range{}, err
34	}
35	f, err := v.GetFile(ctx, sourceURI)
36	if err != nil {
37		return source.Range{}, err
38	}
39	tok := f.GetToken(ctx)
40	return fromProtocolRange(tok, loc.Range), nil
41}
42
43// toProtocolLocation converts from a source range back to a protocol location.
44func toProtocolLocation(fset *token.FileSet, r source.Range) protocol.Location {
45	tok := fset.File(r.Start)
46	uri := source.ToURI(tok.Name())
47	return protocol.Location{
48		URI:   string(uri),
49		Range: toProtocolRange(tok, r),
50	}
51}
52
53// fromProtocolRange converts a protocol range to a source range.
54// It uses fromProtocolPosition to convert the start and end positions, which
55// requires the token file the positions belongs to.
56func fromProtocolRange(f *token.File, r protocol.Range) source.Range {
57	start := fromProtocolPosition(f, r.Start)
58	var end token.Pos
59	switch {
60	case r.End == r.Start:
61		end = start
62	case r.End.Line < 0:
63		end = token.NoPos
64	default:
65		end = fromProtocolPosition(f, r.End)
66	}
67	return source.Range{
68		Start: start,
69		End:   end,
70	}
71}
72
73// toProtocolRange converts from a source range back to a protocol range.
74func toProtocolRange(f *token.File, r source.Range) protocol.Range {
75	return protocol.Range{
76		Start: toProtocolPosition(f, r.Start),
77		End:   toProtocolPosition(f, r.End),
78	}
79}
80
81// fromProtocolPosition converts a protocol position (0-based line and column
82// number) to a token.Pos (byte offset value).
83// It requires the token file the pos belongs to in order to do this.
84func fromProtocolPosition(f *token.File, pos protocol.Position) token.Pos {
85	line := lineStart(f, int(pos.Line)+1)
86	return line + token.Pos(pos.Character) // TODO: this is wrong, bytes not characters
87}
88
89// toProtocolPosition converts from a token pos (byte offset) to a protocol
90// position  (0-based line and column number)
91// It requires the token file the pos belongs to in order to do this.
92func toProtocolPosition(f *token.File, pos token.Pos) protocol.Position {
93	if !pos.IsValid() {
94		return protocol.Position{Line: -1.0, Character: -1.0}
95	}
96	p := f.Position(pos)
97	return protocol.Position{
98		Line:      float64(p.Line - 1),
99		Character: float64(p.Column - 1),
100	}
101}
102
103// fromTokenPosition converts a token.Position (1-based line and column
104// number) to a token.Pos (byte offset value).
105// It requires the token file the pos belongs to in order to do this.
106func fromTokenPosition(f *token.File, pos token.Position) token.Pos {
107	line := lineStart(f, pos.Line)
108	return line + token.Pos(pos.Column-1) // TODO: this is wrong, bytes not characters
109}
110
111// this functionality was borrowed from the analysisutil package
112func lineStart(f *token.File, line int) token.Pos {
113	// Use binary search to find the start offset of this line.
114	//
115	// TODO(rstambler): eventually replace this function with the
116	// simpler and more efficient (*go/token.File).LineStart, added
117	// in go1.12.
118
119	min := 0        // inclusive
120	max := f.Size() // exclusive
121	for {
122		offset := (min + max) / 2
123		pos := f.Pos(offset)
124		posn := f.Position(pos)
125		if posn.Line == line {
126			return pos - (token.Pos(posn.Column) - 1)
127		}
128
129		if min+1 >= max {
130			return token.NoPos
131		}
132
133		if posn.Line < line {
134			min = offset
135		} else {
136			max = offset
137		}
138	}
139}
140