|
83 | 83 | $selected = file;
|
84 | 84 | },
|
85 | 85 |
|
86 |
| - add: async (stubs) => { |
87 |
| - const illegal_create = stubs.some( |
| 86 | + add: async (name, type) => { |
| 87 | + const new_stubs = add_stub(name, type, current_stubs); |
| 88 | +
|
| 89 | + const illegal_create = new_stubs.some( |
88 | 90 | (s) => !editing_constraints.create.some((c) => s.name === c)
|
89 | 91 | );
|
90 |
| -
|
91 | 92 | if (illegal_create) {
|
92 | 93 | modal_text =
|
93 | 94 | 'Only the following files and folders are allowed to be created in this tutorial chapter:\n' +
|
94 | 95 | editing_constraints.create.join('\n');
|
95 | 96 | return;
|
96 | 97 | }
|
97 | 98 |
|
98 |
| - current_stubs = [...current_stubs, ...stubs]; |
99 |
| -
|
| 99 | + current_stubs = [...current_stubs, ...new_stubs]; |
100 | 100 | await load_files(current_stubs);
|
101 | 101 |
|
102 |
| - if (stubs[0].type === 'file') { |
103 |
| - select(stubs[0]); |
| 102 | + if (new_stubs[0].type === 'file') { |
| 103 | + select(new_stubs[0]); |
104 | 104 | }
|
105 | 105 | },
|
106 | 106 |
|
107 | 107 | edit: async (to_rename, new_name) => {
|
108 |
| - /** @type {Array<[import('$lib/types').Stub, import('$lib/types').Stub]>}*/ |
109 |
| - const changed = []; |
110 |
| - const updated_stubs = current_stubs.map((s) => { |
111 |
| - if (!s.name.startsWith(to_rename.name)) { |
112 |
| - return s; |
113 |
| - } |
114 |
| -
|
| 108 | + // treat edit as a remove followed by an add |
| 109 | + const out = current_stubs.filter((s) => s.name.startsWith(to_rename.name)); |
| 110 | + const updated_stubs = current_stubs.filter((s) => !out.includes(s)); |
| 111 | + /** @type {Map<string, import('$lib/types').Stub>} */ |
| 112 | + const new_stubs = new Map(); |
| 113 | + for (const s of out) { |
115 | 114 | const name =
|
116 | 115 | s.name.slice(0, to_rename.name.length - to_rename.basename.length) +
|
117 | 116 | new_name +
|
118 | 117 | s.name.slice(to_rename.name.length);
|
119 |
| - const basename = s === to_rename ? new_name : s.basename; |
120 |
| - const new_stub = { ...s, name, basename }; |
121 |
| -
|
122 |
| - changed.push([s, new_stub]); |
123 |
| - return new_stub; |
124 |
| - }); |
| 118 | + // deduplicate |
| 119 | + for (const to_add of add_stub(name, s.type, updated_stubs)) { |
| 120 | + new_stubs.set(to_add.name, to_add); |
| 121 | + } |
| 122 | + } |
125 | 123 |
|
126 | 124 | const illegal_rename =
|
127 | 125 | !editing_constraints.remove.some((r) => to_rename.name === r) ||
|
128 |
| - changed.some(([, s]) => !editing_constraints.create.some((c) => s.name === c)); |
| 126 | + [...new_stubs.keys()].some((name) => !editing_constraints.create.some((c) => name === c)); |
129 | 127 | if (illegal_rename) {
|
130 | 128 | modal_text =
|
131 | 129 | 'Only the following files and folders are allowed to be renamed in this tutorial chapter:\n' +
|
|
135 | 133 | return;
|
136 | 134 | }
|
137 | 135 |
|
138 |
| - current_stubs = updated_stubs; |
| 136 | + current_stubs = updated_stubs.concat(...new_stubs.values()); |
139 | 137 | await load_files(current_stubs);
|
140 | 138 |
|
141 |
| - if (to_rename.type === 'file') { |
142 |
| - select(/** @type {any} */ (changed.find(([old_s]) => old_s === to_rename))[1]); |
| 139 | + if (to_rename.type === 'file' && $selected?.name === to_rename.name) { |
| 140 | + select(/** @type {any} */ ([...new_stubs.values()].find((s) => s.type === 'file'))); |
143 | 141 | }
|
144 | 142 | },
|
145 | 143 |
|
|
165 | 163 | selected
|
166 | 164 | });
|
167 | 165 |
|
| 166 | + /** |
| 167 | + * @param {string} name |
| 168 | + * @param {'file' | 'directory'} type |
| 169 | + * @param {import('$lib/types').Stub[]} current |
| 170 | + */ |
| 171 | + function add_stub(name, type, current) { |
| 172 | + // find directory which contains the new file |
| 173 | + /** @type {import('$lib/types').DirectoryStub} */ |
| 174 | + let dir = /** @type {any} we know it will be assigned after the loop */ (null); |
| 175 | + for (const stub of current) { |
| 176 | + if ( |
| 177 | + stub.type === 'directory' && |
| 178 | + name.startsWith(stub.name) && |
| 179 | + (!dir || dir.name.length < stub.name.length) |
| 180 | + ) { |
| 181 | + dir = stub; |
| 182 | + } |
| 183 | + } |
| 184 | +
|
| 185 | + const new_name = name.slice(dir.name.length + 1); |
| 186 | + const prefix = dir.name + '/'; |
| 187 | + const depth = prefix.split('/').length - 2; |
| 188 | + const parts = new_name.split('/'); |
| 189 | + /** @type {import('$lib/types').Stub[]} */ |
| 190 | + const stubs = []; |
| 191 | +
|
| 192 | + for (let i = 1; i <= parts.length; i++) { |
| 193 | + const part = parts.slice(0, i).join('/'); |
| 194 | + const basename = /** @type{string} */ (part.split('/').pop()); |
| 195 | + const name = prefix + part; |
| 196 | + if (!current.some((s) => s.name === name)) { |
| 197 | + if (i < parts.length || type === 'directory') { |
| 198 | + stubs.push({ type: 'directory', name, depth: depth + i, basename }); |
| 199 | + } else if (i === parts.length && type === 'file') { |
| 200 | + stubs.push({ |
| 201 | + type: 'file', |
| 202 | + name, |
| 203 | + depth: depth + i, |
| 204 | + basename, |
| 205 | + text: true, |
| 206 | + contents: '' |
| 207 | + }); |
| 208 | + } |
| 209 | + } |
| 210 | + } |
| 211 | +
|
| 212 | + return stubs; |
| 213 | + } |
| 214 | +
|
168 | 215 | /** @type {import('$lib/types').Adapter | undefined} */
|
169 | 216 | let adapter;
|
170 | 217 |
|
|
0 commit comments