Press n or j to go to the next uncovered block, b, p or k for the previous block.
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 | 1x 1x 1x 1x 11x 11x 11x 11x 22x 11x 11x 11x 11x 11x 11x 14x 5x 5x 6x 6x 11x 11x 11x 11x 11x 9x 9x 8x 1x 11x 11x 11x 22x 22x 22x 22x 11x 20x 11x 11x 11x 2x 2x 2x 9x 9x 2x 2x 2x 2x 13x 13x 26x 26x 69x 69x 138x 16x 16x 122x 105x 17x 17x 69x 69x 69x 69x 69x 69x | import * as P from 'pretty-fast-pretty-printer'; import {warn, poscmp} from '../utils'; import {Required, Optional, List, Value} from '../nodeSpec'; import {ASTNode} from '../ast'; // Say that you have the source code `[1, 3]`, representing a list, and you // insert `2` at a drop target in between `1` and `3`. We ultimately represent // all edits as text edits, so what text edit shoudl this be? // // It can't just be to insert `2`, because that would be a syntax error. // Instead, the insertion needs to include a comma, like `, 2` or `2, `. // // How do we get this edit? We don't want to ask every AST node to know how to // deal with edits like this: that would be a lot to ask. // // Instead, we rely entirely on the existing `.pretty()` method. To make this // edit, we will: // // 1. Clone the list node (whose source code is `[1, 3]`). // 2. Create a FakeInsertNode with the text `2`. // 3. Add this FakeInsertNode to the cloned list node. This cloned list node now // has three children. // 4. Call `.pretty()` on the cloned list node, producing the text `[1, 2, 3]`. // (The FakeInsertNode's `.pretty()` method will just return "2".) // 5. Construct a text edit from this: `[1, 3] -> [1, 2, 3]`. // 6. Perform the text edit. // 7. Re-parse the document. If the parse is successful, we replace the old AST // with the newly parsed AST. If unsuccessful, we don't perform the edit. // (Note that the cloned list node and the FakeInsertNode never appeared in // any real AST: they weren't in the old AST, and they're not in the newly // parsed AST either.) // // This file knows how to perform fake AST edits (steps 2&3). // Knows how to perform a fake insertion operation, and how to find the inserted // child after the AST is re-parsed. export class FakeAstInsertion { constructor(parent, fieldName, pos) { this.parent = parent; this.pos = pos; // Find the spec that matches the supplied field name. let spec = null; for (const s of parent.spec.childSpecs) { if (s instanceof List && s.fieldName === fieldName) { spec = s; } } Iif (spec === null) { warn('fakeAstEdits', "Failed to find list to insert child into."); } this.spec = spec; // If `pos` is null, that means the list is empty. Iif (pos === null) { this.index = 0; return; } // `pos` lies inside the list. Find out where. const list = parent[fieldName]; // ideally, we'd use for(i in list) {...} here, but some badly-behaved // IDEs monkeypatch the Array prototype, causing that to fail for (var i = 0; i < list.length; i++) { if (poscmp(pos, list[i].srcRange().from) <= 0) { this.index = i; return; } } this.index = list.length; return; } insertChild(clonedParent, text) { const newChildNode = new FakeInsertNode(this.pos, this.pos, text); clonedParent[this.spec.fieldName].splice(this.index, 0, newChildNode); } // Find the inserted child. If more than one was inserted at once, find the // _last_. // Since nodeIds are not stable across edits, findChild may fail if the // parent node has been changed. In that case, return false to trigger // the fallback focusHint handler. findChild(newAST) { try { const newParent = newAST.getNodeById(this.parent.id); if (!newParent) return null; const indexFromEnd = this.parent[this.spec.fieldName].length - this.index; const newIndex = newParent[this.spec.fieldName].length - indexFromEnd - 1; return newParent[this.spec.fieldName][newIndex]; } catch (e) { return false; } } } // Knows how to perform a fake replacement or deletion operation, and how to // find the replaced child. export class FakeAstReplacement { constructor(parent, child) { this.parent = parent; this.child = child; for (const spec of parent.spec.childSpecs) { const field = parent[spec.fieldName]; Iif (spec instanceof Required && field.id === child.id) { this.spec = spec; return; } else Iif (spec instanceof Optional && field && field.id === child.id) { this.spec = spec; return; } else if (spec instanceof List) { for (const i in field) { if (field[i].id === child.id) { this.spec = spec; this.index = i; return; } } } } warn('new ReplacementPoint', "Failed to find child to be replaced/deleted."); } replaceChild(clonedParent, text) { const newChildNode = new FakeInsertNode(this.child.from, this.child.to, text); Eif (this.index) { clonedParent[this.spec.fieldName][this.index] = newChildNode; } else { clonedParent[this.spec.fieldName] = newChildNode; } } deleteChild(clonedParent) { Eif (this.index) { clonedParent[this.spec.fieldName].splice(this.index, 1); // Remove the i'th element. } else if (this.spec instanceof Optional) { clonedParent[this.spec.fieldName] = null; } else { clonedParent[this.spec.fieldName] = new FakeBlankNode(this.child.from, this.child.to); } } // Call only if you used `replaceChild`, not `deleteChild`. findChild(newAST) { const newParent = newAST.getNodeById(this.parent.id); Iif (!newParent) return null; Eif (this.index) { return this.parent[this.spec.fieldName][this.index]; } else { return this.parent[this.spec.fieldName]; } } } // A fake ASTNode that just prints itself with the given text. class FakeInsertNode extends ASTNode { constructor(from, to, text, options={}) { super(from, to, 'fakeInsertNode', options); this.text = text; } toDescription(_level) { return ""; } pretty() { let lines = this.text.split("\n"); return P.vertArray(lines.map(P.txt)); } render(_props) { warn('fakeAstEdits', "FakeInsertNode didn't expect to be rendered!"); } } // A fake ASTNode that just prints itself like a Blank. class FakeBlankNode extends ASTNode { constructor(from, to, options={}) { super(from, to, 'fakeBlankNode', options); } toDescription(_level) { return ""; } pretty() { return P.txt("..."); } render(_props) { warn('fakeAstEdits', "FakeBlankNode didn't expect to be rendered!"); } } // Make a copy of a node, to perform fake edits on (so that the fake edits don't // show up in the real AST). This copy will be deep over the ASTNodes, but // shallow over the non-ASTNode values they contain. export function cloneNode(oldNode) { let newNode = new ASTNode(oldNode.from, oldNode.to, oldNode.type, oldNode.options); for (const spec of oldNode.spec.childSpecs) { if (spec instanceof Required || spec instanceof Optional) { Eif (oldNode[spec.fieldName]) { newNode[spec.fieldName] = cloneNode(oldNode[spec.fieldName]); } else { newNode[spec.fieldName] = null; } } else if (spec instanceof Value) { newNode[spec.fieldName] = oldNode[spec.fieldName]; } else Eif (spec instanceof List) { newNode[spec.fieldName] = oldNode[spec.fieldName].map(cloneNode); } } newNode.type = oldNode.type; newNode.id = oldNode.id; newNode.hash = oldNode.hash; newNode.spec = oldNode.spec; newNode.pretty = oldNode.pretty; return newNode; } |