Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 1 | // Copyright 2019 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 | |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 5 | package cmd |
| 6 | |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 7 | // span and point represent positions and ranges in text files. |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 8 | |
| 9 | import ( |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 10 | "encoding/json" |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 11 | "fmt" |
Ian Cottrell | 83df196 | 2019-05-02 13:04:19 -0400 | [diff] [blame] | 12 | "path" |
Alan Donovan | d54e12b | 2022-11-30 18:11:33 -0500 | [diff] [blame] | 13 | "sort" |
| 14 | "strings" |
Alan Donovan | 81e741e | 2022-12-28 10:49:34 -0500 | [diff] [blame] | 15 | |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 16 | "golang.org/x/tools/gopls/internal/lsp/protocol" |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 17 | ) |
| 18 | |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 19 | // A span represents a range of text within a source file. The start |
Alan Donovan | 55935f4 | 2023-01-08 10:56:46 -0500 | [diff] [blame] | 20 | // and end points of a valid span may be hold either its byte offset, |
| 21 | // or its (line, column) pair, or both. Columns are measured in bytes. |
| 22 | // |
| 23 | // Spans are appropriate in user interfaces (e.g. command-line tools) |
| 24 | // and tests where a position is notated without access to the content |
| 25 | // of the file. |
| 26 | // |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 27 | // Use protocol.Mapper to convert between span and other |
Alan Donovan | 55935f4 | 2023-01-08 10:56:46 -0500 | [diff] [blame] | 28 | // representations, such as go/token (also UTF-8) or the LSP protocol |
| 29 | // (UTF-16). The latter requires access to file contents. |
Alan Donovan | b798934 | 2023-01-10 21:55:45 -0500 | [diff] [blame] | 30 | // |
| 31 | // See overview comments at ../lsp/protocol/mapper.go. |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 32 | type span struct { |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 33 | v _span |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 34 | } |
| 35 | |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 36 | // point represents a single point within a file. |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 37 | // In general this should only be used as part of a span, as on its own it |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 38 | // does not carry enough information. |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 39 | type point struct { |
| 40 | v _point |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 41 | } |
| 42 | |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 43 | // The span_/point_ types have public fields to support JSON encoding, |
| 44 | // but the span/point types hide these fields by defining methods that |
| 45 | // shadow them. (This is used by a few of the command-line tool |
| 46 | // subcommands, which emit spans and have a -json flag.) |
| 47 | // |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 48 | // TODO(adonovan): simplify now that it's all internal to cmd. |
| 49 | |
| 50 | type _span struct { |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 51 | URI protocol.DocumentURI `json:"uri"` |
| 52 | Start _point `json:"start"` |
| 53 | End _point `json:"end"` |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 54 | } |
| 55 | |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 56 | type _point struct { |
Alan Donovan | 02bea03 | 2023-01-04 18:52:54 -0500 | [diff] [blame] | 57 | Line int `json:"line"` // 1-based line number |
| 58 | Column int `json:"column"` // 1-based, UTF-8 codes (bytes) |
| 59 | Offset int `json:"offset"` // 0-based byte offset |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 60 | } |
| 61 | |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 62 | func newSpan(uri protocol.DocumentURI, start, end point) span { |
| 63 | s := span{v: _span{URI: uri, Start: start.v, End: end.v}} |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 64 | s.v.clean() |
| 65 | return s |
| 66 | } |
| 67 | |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 68 | func newPoint(line, col, offset int) point { |
| 69 | p := point{v: _point{Line: line, Column: col, Offset: offset}} |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 70 | p.v.clean() |
| 71 | return p |
| 72 | } |
| 73 | |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 74 | // sortSpans sorts spans into a stable but unspecified order. |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 75 | func sortSpans(spans []span) { |
Alan Donovan | d54e12b | 2022-11-30 18:11:33 -0500 | [diff] [blame] | 76 | sort.SliceStable(spans, func(i, j int) bool { |
| 77 | return compare(spans[i], spans[j]) < 0 |
| 78 | }) |
| 79 | } |
| 80 | |
| 81 | // compare implements a three-valued ordered comparison of Spans. |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 82 | func compare(a, b span) int { |
Oleksandr Redko | 6b6857a | 2023-02-06 16:18:32 +0000 | [diff] [blame] | 83 | // This is a textual comparison. It does not perform path |
Alan Donovan | d54e12b | 2022-11-30 18:11:33 -0500 | [diff] [blame] | 84 | // cleaning, case folding, resolution of symbolic links, |
| 85 | // testing for existence, or any I/O. |
| 86 | if cmp := strings.Compare(string(a.URI()), string(b.URI())); cmp != 0 { |
| 87 | return cmp |
Ian Cottrell | 183b688 | 2019-04-17 18:01:48 -0400 | [diff] [blame] | 88 | } |
Alan Donovan | d54e12b | 2022-11-30 18:11:33 -0500 | [diff] [blame] | 89 | if cmp := comparePoint(a.v.Start, b.v.Start); cmp != 0 { |
| 90 | return cmp |
Ian Cottrell | 183b688 | 2019-04-17 18:01:48 -0400 | [diff] [blame] | 91 | } |
| 92 | return comparePoint(a.v.End, b.v.End) |
| 93 | } |
| 94 | |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 95 | func comparePoint(a, b _point) int { |
Ian Cottrell | 183b688 | 2019-04-17 18:01:48 -0400 | [diff] [blame] | 96 | if !a.hasPosition() { |
| 97 | if a.Offset < b.Offset { |
| 98 | return -1 |
| 99 | } |
| 100 | if a.Offset > b.Offset { |
| 101 | return 1 |
| 102 | } |
| 103 | return 0 |
| 104 | } |
| 105 | if a.Line < b.Line { |
| 106 | return -1 |
| 107 | } |
| 108 | if a.Line > b.Line { |
| 109 | return 1 |
| 110 | } |
| 111 | if a.Column < b.Column { |
| 112 | return -1 |
| 113 | } |
| 114 | if a.Column > b.Column { |
| 115 | return 1 |
| 116 | } |
| 117 | return 0 |
| 118 | } |
| 119 | |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 120 | func (s span) HasPosition() bool { return s.v.Start.hasPosition() } |
| 121 | func (s span) HasOffset() bool { return s.v.Start.hasOffset() } |
| 122 | func (s span) IsValid() bool { return s.v.Start.isValid() } |
| 123 | func (s span) IsPoint() bool { return s.v.Start == s.v.End } |
| 124 | func (s span) URI() protocol.DocumentURI { return s.v.URI } |
| 125 | func (s span) Start() point { return point{s.v.Start} } |
| 126 | func (s span) End() point { return point{s.v.End} } |
| 127 | func (s *span) MarshalJSON() ([]byte, error) { return json.Marshal(&s.v) } |
| 128 | func (s *span) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &s.v) } |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 129 | |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 130 | func (p point) HasPosition() bool { return p.v.hasPosition() } |
| 131 | func (p point) HasOffset() bool { return p.v.hasOffset() } |
| 132 | func (p point) IsValid() bool { return p.v.isValid() } |
| 133 | func (p *point) MarshalJSON() ([]byte, error) { return json.Marshal(&p.v) } |
| 134 | func (p *point) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &p.v) } |
| 135 | func (p point) Line() int { |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 136 | if !p.v.hasPosition() { |
| 137 | panic(fmt.Errorf("position not set in %v", p.v)) |
| 138 | } |
| 139 | return p.v.Line |
| 140 | } |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 141 | func (p point) Column() int { |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 142 | if !p.v.hasPosition() { |
| 143 | panic(fmt.Errorf("position not set in %v", p.v)) |
| 144 | } |
| 145 | return p.v.Column |
| 146 | } |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 147 | func (p point) Offset() int { |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 148 | if !p.v.hasOffset() { |
| 149 | panic(fmt.Errorf("offset not set in %v", p.v)) |
| 150 | } |
| 151 | return p.v.Offset |
| 152 | } |
| 153 | |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 154 | func (p _point) hasPosition() bool { return p.Line > 0 } |
| 155 | func (p _point) hasOffset() bool { return p.Offset >= 0 } |
| 156 | func (p _point) isValid() bool { return p.hasPosition() || p.hasOffset() } |
| 157 | func (p _point) isZero() bool { |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 158 | return (p.Line == 1 && p.Column == 1) || (!p.hasPosition() && p.Offset == 0) |
| 159 | } |
| 160 | |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 161 | func (s *_span) clean() { |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 162 | //this presumes the points are already clean |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 163 | if !s.End.isValid() || (s.End == _point{}) { |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 164 | s.End = s.Start |
| 165 | } |
| 166 | } |
| 167 | |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 168 | func (p *_point) clean() { |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 169 | if p.Line < 0 { |
| 170 | p.Line = 0 |
| 171 | } |
| 172 | if p.Column <= 0 { |
| 173 | if p.Line > 0 { |
| 174 | p.Column = 1 |
| 175 | } else { |
| 176 | p.Column = 0 |
| 177 | } |
| 178 | } |
| 179 | if p.Offset == 0 && (p.Line > 1 || p.Column > 1) { |
| 180 | p.Offset = -1 |
| 181 | } |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 182 | } |
| 183 | |
| 184 | // Format implements fmt.Formatter to print the Location in a standard form. |
Alan Donovan | 3292b36 | 2023-11-14 01:08:40 -0500 | [diff] [blame] | 185 | // The format produced is one that can be read back in using parseSpan. |
| 186 | // |
| 187 | // TODO(adonovan): this is esoteric, and the formatting options are |
| 188 | // never used outside of TestFormat. Replace with something simpler |
| 189 | // along the lines of MappedRange.String. |
Alan Donovan | 057c099 | 2023-11-16 13:47:55 -0500 | [diff] [blame] | 190 | func (s span) Format(f fmt.State, c rune) { |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 191 | fullForm := f.Flag('+') |
| 192 | preferOffset := f.Flag('#') |
| 193 | // we should always have a uri, simplify if it is file format |
| 194 | //TODO: make sure the end of the uri is unambiguous |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 195 | uri := string(s.v.URI) |
Ian Cottrell | 83df196 | 2019-05-02 13:04:19 -0400 | [diff] [blame] | 196 | if c == 'f' { |
| 197 | uri = path.Base(uri) |
| 198 | } else if !fullForm { |
Alan Donovan | 1cab127 | 2023-11-17 16:16:59 -0500 | [diff] [blame] | 199 | uri = s.v.URI.Path() |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 200 | } |
| 201 | fmt.Fprint(f, uri) |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 202 | if !s.IsValid() || (!fullForm && s.v.Start.isZero() && s.v.End.isZero()) { |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 203 | return |
| 204 | } |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 205 | // see which bits of start to write |
| 206 | printOffset := s.HasOffset() && (fullForm || preferOffset || !s.HasPosition()) |
| 207 | printLine := s.HasPosition() && (fullForm || !printOffset) |
| 208 | printColumn := printLine && (fullForm || (s.v.Start.Column > 1 || s.v.End.Column > 1)) |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 209 | fmt.Fprint(f, ":") |
| 210 | if printLine { |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 211 | fmt.Fprintf(f, "%d", s.v.Start.Line) |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 212 | } |
| 213 | if printColumn { |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 214 | fmt.Fprintf(f, ":%d", s.v.Start.Column) |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 215 | } |
| 216 | if printOffset { |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 217 | fmt.Fprintf(f, "#%d", s.v.Start.Offset) |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 218 | } |
| 219 | // start is written, do we need end? |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 220 | if s.IsPoint() { |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 221 | return |
| 222 | } |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 223 | // we don't print the line if it did not change |
| 224 | printLine = fullForm || (printLine && s.v.End.Line > s.v.Start.Line) |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 225 | fmt.Fprint(f, "-") |
| 226 | if printLine { |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 227 | fmt.Fprintf(f, "%d", s.v.End.Line) |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 228 | } |
| 229 | if printColumn { |
| 230 | if printLine { |
| 231 | fmt.Fprint(f, ":") |
| 232 | } |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 233 | fmt.Fprintf(f, "%d", s.v.End.Column) |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 234 | } |
| 235 | if printOffset { |
Ian Cottrell | 2f43c6d | 2019-03-15 13:19:43 -0400 | [diff] [blame] | 236 | fmt.Fprintf(f, "#%d", s.v.End.Offset) |
Ian Cottrell | b40df0f | 2019-02-12 13:13:49 -0500 | [diff] [blame] | 237 | } |
| 238 | } |