-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathpythonParsing.ts
330 lines (291 loc) · 10.1 KB
/
pythonParsing.ts
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
import * as crypto from "crypto";
import { Range, TextDocument } from "vscode";
/**
* Cache is a simple key-value store that keeps a maximum number of entries.
* The key is a VSCode TextDocument, and the value is of the generic type T.
*
* The cache is used to store the results of expensive calculations, e.g. the
* Manim cell ranges in a document.
*/
class Cache<T> {
private cache: Map<string, T> = new Map();
private static readonly MAX_CACHE_SIZE = 100;
private hash(document: TextDocument): string {
const text = document.getText();
const hash = crypto.createHash("md5").update(text);
return hash.digest("hex");
}
public get(document: TextDocument): T | undefined {
const key = this.hash(document);
return this.cache.get(key);
}
public add(document: TextDocument, value: T): void {
if (this.cache.size >= Cache.MAX_CACHE_SIZE) {
const keys = this.cache.keys();
const firstKey = keys.next().value;
if (firstKey) {
this.cache.delete(firstKey);
}
}
this.cache.set(this.hash(document), value);
}
}
const cellRangesCache = new Cache<Range[]>();
const manimClassesCache = new Cache<ManimClass[]>();
/**
* ManimCellRanges calculates the ranges of Manim cells in a given document.
* It is used to provide folding ranges, code lenses, and decorations for Manim
* Cells in the editor.
*/
export class ManimCellRanges {
/**
* Regular expression to match the start of a Manim cell.
*
* The marker is a comment line starting with "##". That is, "### # #" is also
* considered a valid marker.
*
* Since the comment might be indented, we allow for any number of
* leading whitespaces.
*
* Manim cells themselves might contain further comments, but no nested
* Manim cells, i.e. no further comment starting with "##".
*/
private static readonly MARKER = /^(\s*##)/;
/**
* Calculates the ranges of Manim cells in the given document.
*
* A new Manim cell starts at a custom MARKER. The cell ends either:
* - when the next line starts with the MARKER
* - when the indentation level decreases
* - at the end of the document
*
* Manim Cells are only recognized inside the construct() method of a
* Manim class (see `ManimClass`).
*/
public static calculateRanges(document: TextDocument): Range[] {
const cachedRanges = cellRangesCache.get(document);
if (cachedRanges) {
return cachedRanges;
}
const ranges: Range[] = [];
const manimClasses = ManimClass.findAllIn(document);
manimClasses.forEach((manimClass) => {
const construct = manimClass.constructMethod;
const startCell = construct.bodyRange.start;
const endCell = construct.bodyRange.end;
let start = startCell;
let end = startCell;
let inManimCell = false;
for (let i = startCell; i <= endCell; i++) {
const line = document.lineAt(i);
const indentation = line.firstNonWhitespaceCharacterIndex;
if (indentation === construct.bodyIndent && this.MARKER.test(line.text)) {
if (inManimCell) {
ranges.push(this.constructRange(document, start, end));
}
inManimCell = true;
start = i;
end = i;
} else {
if (!inManimCell) {
start = i;
}
end = i;
}
}
// last cell
if (inManimCell) {
ranges.push(this.constructRange(document, start, endCell));
}
});
cellRangesCache.add(document, ranges);
return ranges;
}
/**
* Returns the cell range of the Manim Cell at the given line number.
*
* Returns null if no cell range contains the line, e.g. if the cursor is
* outside of a Manim cell.
*/
public static getCellRangeAtLine(document: TextDocument, line: number): Range | null {
const ranges = this.calculateRanges(document);
for (const range of ranges) {
if (range.start.line <= line && line <= range.end.line) {
return range;
}
}
return null;
}
/**
* Constructs a new cell range from the given start and end line numbers.
* Discards all trailing empty lines at the end of the range.
*
* The column is set to 0 for `start` and to the end of the line for `end`.
*/
private static constructRange(document: TextDocument, start: number, end: number): Range {
let endNew = end;
while (endNew > start && document.lineAt(endNew).isEmptyOrWhitespace) {
endNew--;
}
return new Range(start, 0, endNew, document.lineAt(endNew).text.length);
}
}
/**
* A range of lines in a document. Both start and end are inclusive and 0-based.
*/
interface LineRange {
start: number;
end: number;
}
/**
* Information for a method, including the range of the method body and the
* indentation level of the body.
*
* This is used to gather infos about the construct() method of a Manim class.
*/
interface MethodInfo {
bodyRange: LineRange;
bodyIndent: number;
}
/**
* A Manim class is defined as:
*
* - Inherits from any object. This constraint is necessary since in the chain
* of class inheritance, the base class must inherit from "Scene", otherwise
* Manim itself won't recognize the class as a scene. We don't enforce the
* name "Scene" here since subclasses might also inherit from other classes.
*
* - Contains a "def construct(self)" method with exactly this signature.
*
* This class provides static methods to work with Manim classes in a
* Python document.
*/
export class ManimClass {
/**
* Regular expression to match a class that inherits from any object.
* The class name is captured in the first group.
*
* Note that this regex doesn't trigger on MyClassName() since we expect
* some words inside the parentheses, e.g. MyClassName(MyBaseClass).
*
* This regex and the class regex should not trigger both on the same input.
*/
private static INHERITED_CLASS_REGEX = /^\s*class\s+(\w+)\s*\(\w.*\)\s*:/;
/**
* Regular expression to match a class definition.
* The class name is captured in the first group.
*
* This includes the case MyClassName(), but not MyClassName(AnyClass).
* The class name is captured in the first group.
*
* This regex and the inherited class regex should not trigger both
* on the same input.
*/
private static CLASS_REGEX = /^\s*class\s+(\w+)\s*(\(\s*\))?\s*:/;
/**
* Regular expression to match the construct() method definition.
*/
private static CONSTRUCT_METHOD_REGEX = /^\s*def\s+construct\s*\(self\)\s*(->\s*None)?\s*:/;
/**
* The 0-based line number where the Manim Class is defined.
*/
lineNumber: number;
/**
* The name of the Manim Class.
*/
className: string;
/**
* The indentation level of the class definition.
*/
classIndent: number;
/**
* Information about the construct() method of the Manim Class.
*/
constructMethod: MethodInfo;
constructor(lineNumber: number, className: string, classIndent: number) {
this.lineNumber = lineNumber;
this.className = className;
this.classIndent = classIndent;
this.constructMethod = { bodyRange: { start: -1, end: -1 }, bodyIndent: -1 };
}
/**
* Returns all ManimClasses in the given document.
*
* @param document The document to search in.
*/
public static findAllIn(document: TextDocument): ManimClass[] {
const cachedClasses = manimClassesCache.get(document);
if (cachedClasses) {
return cachedClasses;
}
const lines = document.getText().split("\n");
const classes: ManimClass[] = [];
let manimClass: ManimClass | null = null;
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
const line = lines[lineNumber];
const match = line.match(this.INHERITED_CLASS_REGEX);
if (match) {
manimClass = new ManimClass(lineNumber, match[1], line.search(/\S/));
classes.push(manimClass);
continue;
}
if (line.match(this.CLASS_REGEX)) {
// only trigger when not a nested class
if (manimClass && line.search(/\S/) <= manimClass.classIndent) {
manimClass = null;
}
continue;
}
if (manimClass && line.match(this.CONSTRUCT_METHOD_REGEX)) {
manimClass.constructMethod = this.makeConstructMethodInfo(lines, lineNumber);
manimClass = null;
}
}
const filtered = classes.filter(c => c.constructMethod.bodyRange.start !== -1);
manimClassesCache.add(document, filtered);
return filtered;
}
/**
* Calculates the range of the construct() method of the Manim class and
* returns it along with the indentation level of the method body.
*
* The construct method is said to end when the indentation level of one
* of the following lines is lower than the indentation level of the first
* line of the method body.
*
* @param lines The lines of the document.
* @param lineNumber The line number where the construct() method is defined.
*/
private static makeConstructMethodInfo(lines: string[], lineNumber: number): MethodInfo {
const bodyIndent = lines[lineNumber + 1].search(/\S/);
const bodyRange = { start: lineNumber + 1, end: lineNumber + 1 };
// Safety check: not even start line of body range accessible
if (bodyRange.start >= lines.length) {
return { bodyRange, bodyIndent };
}
for (let i = bodyRange.start; i < lines.length; i++) {
const line = lines[i];
if (!line.trim()) {
continue; // skip empty lines
}
const indent = line.search(/\S/);
if (indent < bodyIndent) {
break; // e.g. next class or method found
}
bodyRange.end = i;
}
return { bodyRange, bodyIndent };
}
/**
* Returns the ManimClass at the given cursor position (if any).
*
* @param document The document to search in.
* @param cursorLine The line number of the cursor.
* @returns The ManimClass at the cursor position, or undefined if not found.
*/
public static getManimClassAtCursor(document: TextDocument, cursorLine: number):
ManimClass | undefined {
const manimClasses = this.findAllIn(document);
return manimClasses.reverse().find(({ lineNumber }) => lineNumber <= cursorLine);
}
}