| // Copyright 2018 The CUE Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package token |
| |
| import ( |
| "fmt" |
| "sort" |
| "sync" |
| ) |
| |
| // ----------------------------------------------------------------------------- |
| // Positions |
| |
| // Position describes an arbitrary source position |
| // including the file, line, and column location. |
| // A Position is valid if the line number is > 0. |
| type Position struct { |
| Filename string // filename, if any |
| Offset int // offset, starting at 0 |
| Line int // line number, starting at 1 |
| Column int // column number, starting at 1 (byte count) |
| // RelPos Pos // relative position information |
| } |
| |
| // IsValid reports whether the position is valid. |
| func (pos *Position) IsValid() bool { return pos.Line > 0 } |
| |
| // String returns a string in one of several forms: |
| // |
| // file:line:column valid position with file name |
| // line:column valid position without file name |
| // file invalid position with file name |
| // - invalid position without file name |
| // |
| func (pos Position) String() string { |
| s := pos.Filename |
| if pos.IsValid() { |
| if s != "" { |
| s += ":" |
| } |
| s += fmt.Sprintf("%d:%d", pos.Line, pos.Column) |
| } |
| if s == "" { |
| s = "-" |
| } |
| return s |
| } |
| |
| // Pos is a compact encoding of a source position within a file, as well as |
| // relative positioning information. It can be converted into a Position for a |
| // more convenient, but much larger, representation. |
| // |
| type Pos struct { |
| file *File |
| offset int |
| } |
| |
| // File returns the file that contains the position p or nil if there is no |
| // such file (for instance for p == NoPos). |
| // |
| func (p Pos) File() *File { |
| if p.index() == 0 { |
| return nil |
| } |
| return p.file |
| } |
| |
| func (p Pos) Line() int { |
| if p.file == nil { |
| return 0 |
| } |
| return p.Position().Line |
| } |
| |
| func (p Pos) Column() int { |
| if p.file == nil { |
| return 0 |
| } |
| return p.Position().Column |
| } |
| |
| func (p Pos) Filename() string { |
| if p.file == nil { |
| return "" |
| } |
| return p.Position().Filename |
| } |
| |
| func (p Pos) Position() Position { |
| if p.file == nil { |
| return Position{} |
| } |
| return p.file.Position(p) |
| } |
| |
| func (p Pos) String() string { |
| return p.Position().String() |
| } |
| |
| // NoPos is the zero value for Pos; there is no file and line information |
| // associated with it, and NoPos().IsValid() is false. NoPos is always |
| // smaller than any other Pos value. The corresponding Position value |
| // for NoPos is the zero value for Position. |
| var NoPos = Pos{} |
| |
| // RelPos indicates the relative position of token to the previous token. |
| type RelPos int |
| |
| const ( |
| // NoRelPos indicates no relative position is specified. |
| NoRelPos RelPos = iota |
| |
| // Elided indicates that the token for which this position is defined is |
| // not rendered at all. |
| Elided |
| |
| // NoSpace indicates there is no whitespace after this token. |
| NoSpace |
| |
| // Blank means there is horizontal space after this token. |
| Blank |
| |
| // Newline means there is a single newline after this token. |
| Newline |
| |
| // NewSection means there are two or more newlines after this token. |
| NewSection |
| |
| relMask = 0xf |
| relShift = 4 |
| ) |
| |
| var relNames = []string{ |
| "invalid", "elided", "nospace", "blank", "newline", "section", |
| } |
| |
| func (p RelPos) String() string { return relNames[p] } |
| |
| func (p RelPos) Pos() Pos { |
| return Pos{nil, int(p)} |
| } |
| |
| // HasRelPos repors whether p has a relative position. |
| func (p Pos) HasRelPos() bool { |
| return p.offset&relMask != 0 |
| |
| } |
| |
| func (p Pos) Before(q Pos) bool { |
| return p.file == q.file && p.Offset() < q.Offset() |
| } |
| |
| // Offset reports the byte offset relative to the file. |
| func (p Pos) Offset() int { |
| return p.Position().Offset |
| } |
| |
| // Add creates a new position relative to the p offset by n. |
| func (p Pos) Add(n int) Pos { |
| return Pos{p.file, p.offset + toPos(index(n))} |
| } |
| |
| // IsValid reports whether the position is valid. |
| func (p Pos) IsValid() bool { |
| return p != NoPos |
| } |
| |
| // IsNewline reports whether the relative information suggests this node should |
| // be printed on a new lien. |
| func (p Pos) IsNewline() bool { |
| return p.RelPos() >= Newline |
| } |
| |
| func (p Pos) WithRel(rel RelPos) Pos { |
| return Pos{p.file, p.offset&^relMask | int(rel)} |
| } |
| |
| func (p Pos) RelPos() RelPos { |
| return RelPos(p.offset & relMask) |
| } |
| |
| func (p Pos) index() index { |
| return index(p.offset) >> relShift |
| } |
| |
| func toPos(x index) int { |
| return (int(x) << relShift) |
| } |
| |
| // ----------------------------------------------------------------------------- |
| // File |
| |
| type index int |
| |
| // A File has a name, size, and line offset table. |
| type File struct { |
| mutex sync.RWMutex |
| name string // file name as provided to AddFile |
| base index // Pos index range for this file is [base...base+size] |
| size index // file size as provided to AddFile |
| |
| // lines and infos are protected by set.mutex |
| lines []index // lines contains the offset of the first character for each line (the first entry is always 0) |
| infos []lineInfo |
| } |
| |
| // NewFile returns a new file. |
| func NewFile(filename string, base, size int) *File { |
| if base < 0 { |
| base = 1 |
| } |
| return &File{sync.RWMutex{}, filename, index(base), index(size), []index{0}, nil} |
| } |
| |
| // Name returns the file name of file f as registered with AddFile. |
| func (f *File) Name() string { |
| return f.name |
| } |
| |
| // Base returns the base offset of file f as registered with AddFile. |
| func (f *File) Base() int { |
| return int(f.base) |
| } |
| |
| // Size returns the size of file f as registered with AddFile. |
| func (f *File) Size() int { |
| return int(f.size) |
| } |
| |
| // LineCount returns the number of lines in file f. |
| func (f *File) LineCount() int { |
| f.mutex.RLock() |
| n := len(f.lines) |
| f.mutex.RUnlock() |
| return n |
| } |
| |
| // AddLine adds the line offset for a new line. |
| // The line offset must be larger than the offset for the previous line |
| // and smaller than the file size; otherwise the line offset is ignored. |
| // |
| func (f *File) AddLine(offset int) { |
| x := index(offset) |
| f.mutex.Lock() |
| if i := len(f.lines); (i == 0 || f.lines[i-1] < x) && x < f.size { |
| f.lines = append(f.lines, x) |
| } |
| f.mutex.Unlock() |
| } |
| |
| // MergeLine merges a line with the following line. It is akin to replacing |
| // the newline character at the end of the line with a space (to not change the |
| // remaining offsets). To obtain the line number, consult e.g. Position.Line. |
| // MergeLine will panic if given an invalid line number. |
| // |
| func (f *File) MergeLine(line int) { |
| if line <= 0 { |
| panic("illegal line number (line numbering starts at 1)") |
| } |
| f.mutex.Lock() |
| defer f.mutex.Unlock() |
| if line >= len(f.lines) { |
| panic("illegal line number") |
| } |
| // To merge the line numbered <line> with the line numbered <line+1>, |
| // we need to remove the entry in lines corresponding to the line |
| // numbered <line+1>. The entry in lines corresponding to the line |
| // numbered <line+1> is located at index <line>, since indices in lines |
| // are 0-based and line numbers are 1-based. |
| copy(f.lines[line:], f.lines[line+1:]) |
| f.lines = f.lines[:len(f.lines)-1] |
| } |
| |
| // SetLines sets the line offsets for a file and reports whether it succeeded. |
| // The line offsets are the offsets of the first character of each line; |
| // for instance for the content "ab\nc\n" the line offsets are {0, 3}. |
| // An empty file has an empty line offset table. |
| // Each line offset must be larger than the offset for the previous line |
| // and smaller than the file size; otherwise SetLines fails and returns |
| // false. |
| // Callers must not mutate the provided slice after SetLines returns. |
| // |
| func (f *File) SetLines(lines []int) bool { |
| // verify validity of lines table |
| size := f.size |
| for i, offset := range lines { |
| if i > 0 && offset <= lines[i-1] || size <= index(offset) { |
| return false |
| } |
| } |
| |
| // set lines table |
| f.mutex.Lock() |
| f.lines = f.lines[:0] |
| for _, l := range lines { |
| f.lines = append(f.lines, index(l)) |
| } |
| f.mutex.Unlock() |
| return true |
| } |
| |
| // SetLinesForContent sets the line offsets for the given file content. |
| // It ignores position-altering //line comments. |
| func (f *File) SetLinesForContent(content []byte) { |
| var lines []index |
| line := index(0) |
| for offset, b := range content { |
| if line >= 0 { |
| lines = append(lines, line) |
| } |
| line = -1 |
| if b == '\n' { |
| line = index(offset) + 1 |
| } |
| } |
| |
| // set lines table |
| f.mutex.Lock() |
| f.lines = lines |
| f.mutex.Unlock() |
| } |
| |
| // A lineInfo object describes alternative file and line number |
| // information (such as provided via a //line comment in a .go |
| // file) for a given file offset. |
| type lineInfo struct { |
| // fields are exported to make them accessible to gob |
| Offset int |
| Filename string |
| Line int |
| } |
| |
| // AddLineInfo adds alternative file and line number information for |
| // a given file offset. The offset must be larger than the offset for |
| // the previously added alternative line info and smaller than the |
| // file size; otherwise the information is ignored. |
| // |
| // AddLineInfo is typically used to register alternative position |
| // information for //line filename:line comments in source files. |
| // |
| func (f *File) AddLineInfo(offset int, filename string, line int) { |
| x := index(offset) |
| f.mutex.Lock() |
| if i := len(f.infos); i == 0 || index(f.infos[i-1].Offset) < x && x < f.size { |
| f.infos = append(f.infos, lineInfo{offset, filename, line}) |
| } |
| f.mutex.Unlock() |
| } |
| |
| // Pos returns the Pos value for the given file offset; |
| // the offset must be <= f.Size(). |
| // f.Pos(f.Offset(p)) == p. |
| // |
| func (f *File) Pos(offset int, rel RelPos) Pos { |
| if index(offset) > f.size { |
| panic("illegal file offset") |
| } |
| return Pos{f, toPos(f.base+index(offset)) + int(rel)} |
| } |
| |
| // Offset returns the offset for the given file position p; |
| // p must be a valid Pos value in that file. |
| // f.Offset(f.Pos(offset)) == offset. |
| // |
| func (f *File) Offset(p Pos) int { |
| x := p.index() |
| if x < f.base || x > f.base+index(f.size) { |
| panic("illegal Pos value") |
| } |
| return int(x - f.base) |
| } |
| |
| // Line returns the line number for the given file position p; |
| // p must be a Pos value in that file or NoPos. |
| // |
| func (f *File) Line(p Pos) int { |
| return f.Position(p).Line |
| } |
| |
| func searchLineInfos(a []lineInfo, x int) int { |
| return sort.Search(len(a), func(i int) bool { return a[i].Offset > x }) - 1 |
| } |
| |
| // unpack returns the filename and line and column number for a file offset. |
| // If adjusted is set, unpack will return the filename and line information |
| // possibly adjusted by //line comments; otherwise those comments are ignored. |
| // |
| func (f *File) unpack(offset index, adjusted bool) (filename string, line, column int) { |
| filename = f.name |
| if i := searchInts(f.lines, offset); i >= 0 { |
| line, column = int(i+1), int(offset-f.lines[i]+1) |
| } |
| if adjusted && len(f.infos) > 0 { |
| // almost no files have extra line infos |
| if i := searchLineInfos(f.infos, int(offset)); i >= 0 { |
| alt := &f.infos[i] |
| filename = alt.Filename |
| if i := searchInts(f.lines, index(alt.Offset)); i >= 0 { |
| line += alt.Line - i - 1 |
| } |
| } |
| } |
| return |
| } |
| |
| func (f *File) position(p Pos, adjusted bool) (pos Position) { |
| offset := p.index() - f.base |
| pos.Offset = int(offset) |
| pos.Filename, pos.Line, pos.Column = f.unpack(offset, adjusted) |
| return |
| } |
| |
| // PositionFor returns the Position value for the given file position p. |
| // If adjusted is set, the position may be adjusted by position-altering |
| // //line comments; otherwise those comments are ignored. |
| // p must be a Pos value in f or NoPos. |
| // |
| func (f *File) PositionFor(p Pos, adjusted bool) (pos Position) { |
| x := p.index() |
| if p != NoPos { |
| if x < f.base || x > f.base+f.size { |
| panic("illegal Pos value") |
| } |
| pos = f.position(p, adjusted) |
| } |
| return |
| } |
| |
| // Position returns the Position value for the given file position p. |
| // Calling f.Position(p) is equivalent to calling f.PositionFor(p, true). |
| // |
| func (f *File) Position(p Pos) (pos Position) { |
| return f.PositionFor(p, true) |
| } |
| |
| // ----------------------------------------------------------------------------- |
| // Helper functions |
| |
| func searchInts(a []index, x index) int { |
| // This function body is a manually inlined version of: |
| // |
| // return sort.Search(len(a), func(i int) bool { return a[i] > x }) - 1 |
| // |
| // With better compiler optimizations, this may not be needed in the |
| // future, but at the moment this change improves the go/printer |
| // benchmark performance by ~30%. This has a direct impact on the |
| // speed of gofmt and thus seems worthwhile (2011-04-29). |
| // TODO(gri): Remove this when compilers have caught up. |
| i, j := 0, len(a) |
| for i < j { |
| h := i + (j-i)/2 // avoid overflow when computing h |
| // i ≤ h < j |
| if a[h] <= x { |
| i = h + 1 |
| } else { |
| j = h |
| } |
| } |
| return i - 1 |
| } |