github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/cue/token/position.go (about) 1 // Copyright 2018 The CUE Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package token 16 17 import ( 18 "fmt" 19 "sort" 20 "sync" 21 ) 22 23 // ----------------------------------------------------------------------------- 24 // Positions 25 26 // Position describes an arbitrary source position 27 // including the file, line, and column location. 28 // A Position is valid if the line number is > 0. 29 type Position struct { 30 Filename string // filename, if any 31 Offset int // offset, starting at 0 32 Line int // line number, starting at 1 33 Column int // column number, starting at 1 (byte count) 34 // RelPos Pos // relative position information 35 } 36 37 // IsValid reports whether the position is valid. 38 func (pos *Position) IsValid() bool { return pos.Line > 0 } 39 40 // String returns a string in one of several forms: 41 // 42 // file:line:column valid position with file name 43 // line:column valid position without file name 44 // file invalid position with file name 45 // - invalid position without file name 46 // 47 func (pos Position) String() string { 48 s := pos.Filename 49 if pos.IsValid() { 50 if s != "" { 51 s += ":" 52 } 53 s += fmt.Sprintf("%d:%d", pos.Line, pos.Column) 54 } 55 if s == "" { 56 s = "-" 57 } 58 return s 59 } 60 61 // Pos is a compact encoding of a source position within a file, as well as 62 // relative positioning information. It can be converted into a Position for a 63 // more convenient, but much larger, representation. 64 // 65 type Pos struct { 66 file *File 67 offset int 68 } 69 70 // File returns the file that contains the position p or nil if there is no 71 // such file (for instance for p == NoPos). 72 // 73 func (p Pos) File() *File { 74 if p.index() == 0 { 75 return nil 76 } 77 return p.file 78 } 79 80 func (p Pos) Line() int { 81 if p.file == nil { 82 return 0 83 } 84 return p.Position().Line 85 } 86 87 func (p Pos) Column() int { 88 if p.file == nil { 89 return 0 90 } 91 return p.Position().Column 92 } 93 94 func (p Pos) Filename() string { 95 if p.file == nil { 96 return "" 97 } 98 return p.Position().Filename 99 } 100 101 func (p Pos) Position() Position { 102 if p.file == nil { 103 return Position{} 104 } 105 return p.file.Position(p) 106 } 107 108 func (p Pos) String() string { 109 return p.Position().String() 110 } 111 112 // NoPos is the zero value for Pos; there is no file and line information 113 // associated with it, and NoPos().IsValid() is false. NoPos is always 114 // smaller than any other Pos value. The corresponding Position value 115 // for NoPos is the zero value for Position. 116 var NoPos = Pos{} 117 118 // RelPos indicates the relative position of token to the previous token. 119 type RelPos int 120 121 const ( 122 // NoRelPos indicates no relative position is specified. 123 NoRelPos RelPos = iota 124 125 // Elided indicates that the token for which this position is defined is 126 // not rendered at all. 127 Elided 128 129 // NoSpace indicates there is no whitespace after this token. 130 NoSpace 131 132 // Blank means there is horizontal space after this token. 133 Blank 134 135 // Newline means there is a single newline after this token. 136 Newline 137 138 // NewSection means there are two or more newlines after this token. 139 NewSection 140 141 relMask = 0xf 142 relShift = 4 143 ) 144 145 var relNames = []string{ 146 "invalid", "elided", "nospace", "blank", "newline", "section", 147 } 148 149 func (p RelPos) String() string { return relNames[p] } 150 151 func (p RelPos) Pos() Pos { 152 return Pos{nil, int(p)} 153 } 154 155 // HasRelPos repors whether p has a relative position. 156 func (p Pos) HasRelPos() bool { 157 return p.offset&relMask != 0 158 159 } 160 161 func (p Pos) Before(q Pos) bool { 162 return p.file == q.file && p.Offset() < q.Offset() 163 } 164 165 // Offset reports the byte offset relative to the file. 166 func (p Pos) Offset() int { 167 return p.Position().Offset 168 } 169 170 // Add creates a new position relative to the p offset by n. 171 func (p Pos) Add(n int) Pos { 172 return Pos{p.file, p.offset + toPos(index(n))} 173 } 174 175 // IsValid reports whether the position is valid. 176 func (p Pos) IsValid() bool { 177 return p != NoPos 178 } 179 180 // IsNewline reports whether the relative information suggests this node should 181 // be printed on a new lien. 182 func (p Pos) IsNewline() bool { 183 return p.RelPos() >= Newline 184 } 185 186 func (p Pos) WithRel(rel RelPos) Pos { 187 return Pos{p.file, p.offset&^relMask | int(rel)} 188 } 189 190 func (p Pos) RelPos() RelPos { 191 return RelPos(p.offset & relMask) 192 } 193 194 func (p Pos) index() index { 195 return index(p.offset) >> relShift 196 } 197 198 func toPos(x index) int { 199 return (int(x) << relShift) 200 } 201 202 // ----------------------------------------------------------------------------- 203 // File 204 205 type index int 206 207 // A File has a name, size, and line offset table. 208 type File struct { 209 mutex sync.RWMutex 210 name string // file name as provided to AddFile 211 base index // Pos index range for this file is [base...base+size] 212 size index // file size as provided to AddFile 213 214 // lines and infos are protected by set.mutex 215 lines []index // lines contains the offset of the first character for each line (the first entry is always 0) 216 infos []lineInfo 217 } 218 219 // NewFile returns a new file. 220 func NewFile(filename string, base, size int) *File { 221 if base < 0 { 222 base = 1 223 } 224 return &File{sync.RWMutex{}, filename, index(base), index(size), []index{0}, nil} 225 } 226 227 // Name returns the file name of file f as registered with AddFile. 228 func (f *File) Name() string { 229 return f.name 230 } 231 232 // Base returns the base offset of file f as registered with AddFile. 233 func (f *File) Base() int { 234 return int(f.base) 235 } 236 237 // Size returns the size of file f as registered with AddFile. 238 func (f *File) Size() int { 239 return int(f.size) 240 } 241 242 // LineCount returns the number of lines in file f. 243 func (f *File) LineCount() int { 244 f.mutex.RLock() 245 n := len(f.lines) 246 f.mutex.RUnlock() 247 return n 248 } 249 250 // AddLine adds the line offset for a new line. 251 // The line offset must be larger than the offset for the previous line 252 // and smaller than the file size; otherwise the line offset is ignored. 253 // 254 func (f *File) AddLine(offset int) { 255 x := index(offset) 256 f.mutex.Lock() 257 if i := len(f.lines); (i == 0 || f.lines[i-1] < x) && x < f.size { 258 f.lines = append(f.lines, x) 259 } 260 f.mutex.Unlock() 261 } 262 263 // MergeLine merges a line with the following line. It is akin to replacing 264 // the newline character at the end of the line with a space (to not change the 265 // remaining offsets). To obtain the line number, consult e.g. Position.Line. 266 // MergeLine will panic if given an invalid line number. 267 // 268 func (f *File) MergeLine(line int) { 269 if line <= 0 { 270 panic("illegal line number (line numbering starts at 1)") 271 } 272 f.mutex.Lock() 273 defer f.mutex.Unlock() 274 if line >= len(f.lines) { 275 panic("illegal line number") 276 } 277 // To merge the line numbered <line> with the line numbered <line+1>, 278 // we need to remove the entry in lines corresponding to the line 279 // numbered <line+1>. The entry in lines corresponding to the line 280 // numbered <line+1> is located at index <line>, since indices in lines 281 // are 0-based and line numbers are 1-based. 282 copy(f.lines[line:], f.lines[line+1:]) 283 f.lines = f.lines[:len(f.lines)-1] 284 } 285 286 // SetLines sets the line offsets for a file and reports whether it succeeded. 287 // The line offsets are the offsets of the first character of each line; 288 // for instance for the content "ab\nc\n" the line offsets are {0, 3}. 289 // An empty file has an empty line offset table. 290 // Each line offset must be larger than the offset for the previous line 291 // and smaller than the file size; otherwise SetLines fails and returns 292 // false. 293 // Callers must not mutate the provided slice after SetLines returns. 294 // 295 func (f *File) SetLines(lines []int) bool { 296 // verify validity of lines table 297 size := f.size 298 for i, offset := range lines { 299 if i > 0 && offset <= lines[i-1] || size <= index(offset) { 300 return false 301 } 302 } 303 304 // set lines table 305 f.mutex.Lock() 306 f.lines = f.lines[:0] 307 for _, l := range lines { 308 f.lines = append(f.lines, index(l)) 309 } 310 f.mutex.Unlock() 311 return true 312 } 313 314 // SetLinesForContent sets the line offsets for the given file content. 315 // It ignores position-altering //line comments. 316 func (f *File) SetLinesForContent(content []byte) { 317 var lines []index 318 line := index(0) 319 for offset, b := range content { 320 if line >= 0 { 321 lines = append(lines, line) 322 } 323 line = -1 324 if b == '\n' { 325 line = index(offset) + 1 326 } 327 } 328 329 // set lines table 330 f.mutex.Lock() 331 f.lines = lines 332 f.mutex.Unlock() 333 } 334 335 // A lineInfo object describes alternative file and line number 336 // information (such as provided via a //line comment in a .go 337 // file) for a given file offset. 338 type lineInfo struct { 339 // fields are exported to make them accessible to gob 340 Offset int 341 Filename string 342 Line int 343 } 344 345 // AddLineInfo adds alternative file and line number information for 346 // a given file offset. The offset must be larger than the offset for 347 // the previously added alternative line info and smaller than the 348 // file size; otherwise the information is ignored. 349 // 350 // AddLineInfo is typically used to register alternative position 351 // information for //line filename:line comments in source files. 352 // 353 func (f *File) AddLineInfo(offset int, filename string, line int) { 354 x := index(offset) 355 f.mutex.Lock() 356 if i := len(f.infos); i == 0 || index(f.infos[i-1].Offset) < x && x < f.size { 357 f.infos = append(f.infos, lineInfo{offset, filename, line}) 358 } 359 f.mutex.Unlock() 360 } 361 362 // Pos returns the Pos value for the given file offset; 363 // the offset must be <= f.Size(). 364 // f.Pos(f.Offset(p)) == p. 365 // 366 func (f *File) Pos(offset int, rel RelPos) Pos { 367 if index(offset) > f.size { 368 panic("illegal file offset") 369 } 370 return Pos{f, toPos(f.base+index(offset)) + int(rel)} 371 } 372 373 // Offset returns the offset for the given file position p; 374 // p must be a valid Pos value in that file. 375 // f.Offset(f.Pos(offset)) == offset. 376 // 377 func (f *File) Offset(p Pos) int { 378 x := p.index() 379 if x < f.base || x > f.base+index(f.size) { 380 panic("illegal Pos value") 381 } 382 return int(x - f.base) 383 } 384 385 // Line returns the line number for the given file position p; 386 // p must be a Pos value in that file or NoPos. 387 // 388 func (f *File) Line(p Pos) int { 389 return f.Position(p).Line 390 } 391 392 func searchLineInfos(a []lineInfo, x int) int { 393 return sort.Search(len(a), func(i int) bool { return a[i].Offset > x }) - 1 394 } 395 396 // unpack returns the filename and line and column number for a file offset. 397 // If adjusted is set, unpack will return the filename and line information 398 // possibly adjusted by //line comments; otherwise those comments are ignored. 399 // 400 func (f *File) unpack(offset index, adjusted bool) (filename string, line, column int) { 401 filename = f.name 402 if i := searchInts(f.lines, offset); i >= 0 { 403 line, column = int(i+1), int(offset-f.lines[i]+1) 404 } 405 if adjusted && len(f.infos) > 0 { 406 // almost no files have extra line infos 407 if i := searchLineInfos(f.infos, int(offset)); i >= 0 { 408 alt := &f.infos[i] 409 filename = alt.Filename 410 if i := searchInts(f.lines, index(alt.Offset)); i >= 0 { 411 line += alt.Line - i - 1 412 } 413 } 414 } 415 return 416 } 417 418 func (f *File) position(p Pos, adjusted bool) (pos Position) { 419 offset := p.index() - f.base 420 pos.Offset = int(offset) 421 pos.Filename, pos.Line, pos.Column = f.unpack(offset, adjusted) 422 return 423 } 424 425 // PositionFor returns the Position value for the given file position p. 426 // If adjusted is set, the position may be adjusted by position-altering 427 // //line comments; otherwise those comments are ignored. 428 // p must be a Pos value in f or NoPos. 429 // 430 func (f *File) PositionFor(p Pos, adjusted bool) (pos Position) { 431 x := p.index() 432 if p != NoPos { 433 if x < f.base || x > f.base+f.size { 434 panic("illegal Pos value") 435 } 436 pos = f.position(p, adjusted) 437 } 438 return 439 } 440 441 // Position returns the Position value for the given file position p. 442 // Calling f.Position(p) is equivalent to calling f.PositionFor(p, true). 443 // 444 func (f *File) Position(p Pos) (pos Position) { 445 return f.PositionFor(p, true) 446 } 447 448 // ----------------------------------------------------------------------------- 449 // Helper functions 450 451 func searchInts(a []index, x index) int { 452 // This function body is a manually inlined version of: 453 // 454 // return sort.Search(len(a), func(i int) bool { return a[i] > x }) - 1 455 // 456 // With better compiler optimizations, this may not be needed in the 457 // future, but at the moment this change improves the go/printer 458 // benchmark performance by ~30%. This has a direct impact on the 459 // speed of gofmt and thus seems worthwhile (2011-04-29). 460 // TODO(gri): Remove this when compilers have caught up. 461 i, j := 0, len(a) 462 for i < j { 463 h := i + (j-i)/2 // avoid overflow when computing h 464 // i ≤ h < j 465 if a[h] <= x { 466 i = h + 1 467 } else { 468 j = h 469 } 470 } 471 return i - 1 472 }