-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathparse.ts
More file actions
210 lines (188 loc) · 8.17 KB
/
parse.ts
File metadata and controls
210 lines (188 loc) · 8.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import {Tree, Range} from "./tree"
/// The [`TreeFragment.applyChanges`](#common.TreeFragment^applyChanges)
/// method expects changed ranges in this format.
export interface ChangedRange {
/// The start of the change in the start document
fromA: number
/// The end of the change in the start document
toA: number
/// The start of the replacement in the new document
fromB: number
/// The end of the replacement in the new document
toB: number
}
const enum Open { Start = 1, End = 2 }
/// Tree fragments are used during [incremental
/// parsing](#common.Parser.startParse) to track parts of old trees
/// that can be reused in a new parse. An array of fragments is used
/// to track regions of an old tree whose nodes might be reused in new
/// parses. Use the static
/// [`applyChanges`](#common.TreeFragment^applyChanges) method to
/// update fragments for document changes.
export class TreeFragment {
/// @internal
open: Open
/// Construct a tree fragment. You'll usually want to use
/// [`addTree`](#common.TreeFragment^addTree) and
/// [`applyChanges`](#common.TreeFragment^applyChanges) instead of
/// calling this directly.
constructor(
/// The start of the unchanged range pointed to by this fragment.
/// This refers to an offset in the _updated_ document (as opposed
/// to the original tree).
readonly from: number,
/// The end of the unchanged range.
readonly to: number,
/// The tree that this fragment is based on.
readonly tree: Tree,
/// The offset between the fragment's tree and the document that
/// this fragment can be used against. Add this when going from
/// document to tree positions, subtract it to go from tree to
/// document positions.
readonly offset: number,
openStart: boolean = false,
openEnd: boolean = false
) {
this.open = (openStart ? Open.Start : 0) | (openEnd ? Open.End : 0)
}
/// Whether the start of the fragment represents the start of a
/// parse, or the end of a change. (In the second case, it may not
/// be safe to reuse some nodes at the start, depending on the
/// parsing algorithm.)
get openStart() { return (this.open & Open.Start) > 0 }
/// Whether the end of the fragment represents the end of a
/// full-document parse, or the start of a change.
get openEnd() { return (this.open & Open.End) > 0 }
/// Create a set of fragments from a freshly parsed tree, or update
/// an existing set of fragments by replacing the ones that overlap
/// with a tree with content from the new tree. When `partial` is
/// true, the parse is treated as incomplete, and the resulting
/// fragment has [`openEnd`](#common.TreeFragment.openEnd) set to
/// true.
static addTree(tree: Tree, fragments: readonly TreeFragment[] = [], partial = false): readonly TreeFragment[] {
let result = [new TreeFragment(0, tree.length, tree, 0, false, partial)]
for (let f of fragments) if (f.to > tree.length) result.push(f)
return result
}
/// Apply a set of edits to an array of fragments, removing or
/// splitting fragments as necessary to remove edited ranges, and
/// adjusting offsets for fragments that moved.
static applyChanges(fragments: readonly TreeFragment[], changes: readonly ChangedRange[], minGap = 128) {
if (!changes.length) return fragments
let result: TreeFragment[] = []
let fI = 1, nextF = fragments.length ? fragments[0] : null
for (let cI = 0, pos = 0, off = 0;; cI++) {
let nextC = cI < changes.length ? changes[cI] : null
let nextPos = nextC ? nextC.fromA : 1e9
if (nextPos - pos >= minGap) while (nextF && nextF.from < nextPos) {
let cut: TreeFragment | null = nextF
if (pos >= cut.from || nextPos <= cut.to || off) {
let fFrom = Math.max(cut.from, pos) - off, fTo = Math.min(cut.to, nextPos) - off
cut = fFrom >= fTo ? null : new TreeFragment(fFrom, fTo, cut.tree, cut.offset + off, cI > 0, !!nextC)
}
if (cut) result.push(cut)
if (nextF.to > nextPos) break
nextF = fI < fragments.length ? fragments[fI++] : null
}
if (!nextC) break
pos = nextC.toA
off = nextC.toA - nextC.toB
}
return result
}
}
/// Interface used to represent an in-progress parse, which can be
/// moved forward piece-by-piece.
export interface PartialParse {
/// Advance the parse state by some amount. Will return the finished
/// syntax tree when the parse completes.
advance(): Tree | null
/// The position up to which the document has been parsed. Note
/// that, in multi-pass parsers, this will stay back until the last
/// pass has moved past a given position.
readonly parsedPos: number
/// Tell the parse to not advance beyond the given position.
/// `advance` will return a tree when the parse has reached the
/// position. Note that, depending on the parser algorithm and the
/// state of the parse when `stopAt` was called, that tree may
/// contain nodes beyond the position. It is an error to call
/// `stopAt` with a higher position than it's [current
/// value](#common.PartialParse.stoppedAt).
stopAt(pos: number): void
/// Reports whether `stopAt` has been called on this parse.
readonly stoppedAt: number | null
}
/// A superclass that parsers should extend.
export abstract class Parser {
/// Start a parse for a single tree. This is the method concrete
/// parser implementations must implement. Called by `startParse`,
/// with the optional arguments resolved.
abstract createParse(
input: Input,
fragments: readonly TreeFragment[],
ranges: readonly {from: number, to: number}[]
): PartialParse
/// Start a parse, returning a [partial parse](#common.PartialParse)
/// object. [`fragments`](#common.TreeFragment) can be passed in to
/// make the parse incremental.
///
/// By default, the entire input is parsed. You can pass `ranges`,
/// which should be a sorted array of non-empty, non-overlapping
/// ranges, to parse only those ranges. The tree returned in that
/// case will start at `ranges[0].from`.
startParse(
input: Input | string,
fragments?: readonly TreeFragment[],
ranges?: readonly {from: number, to: number}[]
): PartialParse {
if (typeof input == "string") input = new StringInput(input)
ranges = !ranges ? [new Range(0, input.length)] : ranges.length ? ranges.map(r => new Range(r.from, r.to)) : [new Range(0, 0)]
return this.createParse(input, fragments || [], ranges)
}
/// Run a full parse, returning the resulting tree.
parse(
input: Input | string,
fragments?: readonly TreeFragment[],
ranges?: readonly {from: number, to: number}[]
) {
let parse = this.startParse(input, fragments, ranges)
for (;;) {
let done = parse.advance()
if (done) return done
}
}
}
/// This is the interface parsers use to access the document. To run
/// Lezer directly on your own document data structure, you have to
/// write an implementation of it.
export interface Input {
/// The length of the document.
readonly length: number
/// Get the chunk after the given position. The returned string
/// should start at `from` and, if that isn't the end of the
/// document, may be of any length greater than zero.
chunk(from: number): string
/// Indicates whether the chunks already end at line breaks, so that
/// client code that wants to work by-line can avoid re-scanning
/// them for line breaks. When this is true, the result of `chunk()`
/// should either be a single line break, or the content between
/// `from` and the next line break.
readonly lineChunks: boolean
/// Read the part of the document between the given positions.
read(from: number, to: number): string
}
class StringInput implements Input {
constructor(readonly string: string) {}
get length() { return this.string.length }
chunk(from: number) { return this.string.slice(from) }
get lineChunks() { return false }
read(from: number, to: number) { return this.string.slice(from, to) }
}
/// Parse wrapper functions are supported by some parsers to inject
/// additional parsing logic.
export type ParseWrapper = (
inner: PartialParse,
input: Input,
fragments: readonly TreeFragment[],
ranges: readonly {from: number, to: number}[]
) => PartialParse