1/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 * LES FICHIERS DU STUDIO CODE â LA RETOUCHE, LA FUSION, ET L'HISTOIRE 3 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 4 * 5 * DOUZIÃME MORCEAU DÃTACHà DE `src/app.js`. 6 * 7 * LE CALCUL EST PUR, ET C'EST CE QUI SE MESURE. Les fonctions qui décident â 8 * lire une retouche, la poser, fusionner, noter un état, y remonter â ne 9 * touchent ni l'écran ni `STATE`, et ne rendent pas l'heure sans qu'on la leur 10 * donne : on les rejoue dans Node telles qu'elles sont écrites 11 * (`tests/retouche-code-smoke.mjs`). 12 * 13 * DEUX FONCTIONS FONT EXCEPTION, ET ELLES SONT NOMMÃES : `vueDesVersions` met 14 * l'historique en forme pour l'écran, et `window.codeRestaurerVersion` est le 15 * geste de la personne. Elles vivent ici parce que c'est le sujet de ce 16 * fichier â les fichiers du Studio Code â et parce que le tronc ne peut plus 17 * grossir. Elles lisent `escHtml`, `timeAgo`, `projetCodeActif`, `save` et 18 * `render` du tronc, au moment de l'appel : la règle du découpage tient 19 * (déclarer, ne rien exécuter au chargement), et la garde ne les rejoue pas. 20 * 21 * ââ 1. UNE GÃNÃRATION PARTIELLE REMPLAÃAIT LE PROJET ENTIER âââââââââââââââ 22 * 23 * `envoyerCode` faisait `proj.files = obj.files`. Une réponse qui ne réécrit 24 * qu'un fichier faisait tomber le projet à ce fichier ; l'accord du registre 25 * les repoussait ensuite en fin de liste, dans un ordre qui n'était plus le 26 * sien. Ce n'était pas une perte, c'était un désordre â et il était silencieux. 27 * 28 * ââ 2. ET CE QU'ELLE ÃCRIVAIT SE FAISAIT ANNULER AU RENDU SUIVANT âââââââââ 29 * 30 * PROUVà EN EXÃCUTANT L'ACCORD, pas en le relisant. Les objets rendus par le 31 * parseur sont des `{path, lang, content}` : ni `fid`, ni `majAt`. 32 * `synchroniserStudioCode` compare `f.majAt || 0` à `e.majAt` et, à égalité 33 * comme en dessous, LE REGISTRE GAGNE â c'est écrit dans sa note, et c'est 34 * juste pour un renommage. Résultat : régénérer un fichier qui existait déjà 35 * remettait l'ANCIEN contenu, pendant que le fil affichait « â 1 fichier(s) 36 * généré(s) ». La génération était produite, facturée, et annulée. 37 * 38 * D'où la seule règle de `fusionnerFichiersCode` : ce qu'on écrit est DATÃ. 39 * Une frappe dans l'éditeur est datée, une génération doit l'être aussi â 40 * sinon elle n'existe pas. 41 * 42 * ââ 3. IL FALLAIT RÃÃCRIRE DEUX MILLE LIGNES POUR EN CHANGER TROIS ââââââââ 43 * 44 * Le studio ne savait rendre que des fichiers ENTIERS. `retouchesDuTexte` lit 45 * la forme que `STUDIOS.code.defautModif` demande, et `appliquerRetouches` la 46 * pose. Les deux bouts sont tenus ensemble par la garde : un contrat que le 47 * parseur ne relit pas est un gabarit imposé pour rien. 48 * 49 * ââ CE QUE CES TROIS FONCTIONS NE TIENNENT PAS âââââââââââââââââââââââââââ 50 * 51 * Que le modèle recopie le texte AVANT sans faute. Ãa ne se prouve pas hors 52 * ligne. Ce qui se prouve, et qui est tenu ici : une retouche qui ne retrouve 53 * pas son texte n'est PAS appliquée, elle est NOMMÃE, et le fichier ressort 54 * intact. Une retouche à moitié posée serait pire que pas de retouche. 55 * 56 * ââ LA RÃGLE DU DÃCOUPAGE ââââââââââââââââââââââââââââââââââââââââââââââââ 57 * 58 * Comme les onze autres morceaux : ce fichier se charge APRÃS le tronc et ne 59 * fait que DÃCLARER. Rien ne s'exécute à son chargement, et le tronc ne 60 * l'appelle pas pendant le sien. Tenu par `tests/scripts-charges-smoke.mjs`. 61 */ 62 63/* eslint-disable no-unused-vars */ 64 65/* LA FORME, ÃCRITE UNE FOIS. C'est elle que `STUDIOS.code.defautModif` décrit 66 au modèle et que la garde relie aux deux bouts. Les bornes sont celles d'un 67 conflit de fusion â sept chevrons, sept signes égal â parce que tout modèle 68 les a déjà vues mille fois et qu'aucun code source n'en porte par accident. 69 70 `[^\s`]+` pour le chemin : ni espace ni accent grave, donc le bloc ne peut 71 pas manger sa propre clôture. Le `?` après \n avant la clôture laisse passer 72 un fichier qui se termine sans retour à la ligne. */ 73const RETOUCHE_BLOC = /```modif:([^\s`]+)\n<<<<<<< AVANT\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> APR[ÃE]S\n?```/g 74 75function retouchesDuTexte(contenu) { 76 const t = String(contenu == null ? '' : contenu) 77 const sorties = [] 78 /* `lastIndex` remis à zéro. UNE REGEX /g DE HAUT NIVEAU GARDE SA POSITION 79 d'un appel à l'autre, et c'est le piège classique â mais il n'est PAS 80 atteignable ici, et le dire autrement serait mentir : la boucle `while` 81 va toujours jusqu'au `null` final, qui remet `lastIndex` à zéro tout seul. 82 La ligne tient donc contre le `break` que quelqu'un ajoutera un jour, pas 83 contre un défaut d'aujourd'hui. Ãprouvée : je l'ai retirée, et la garde 84 est restée verte â c'est ce qui a fait corriger ce commentaire-ci. */ 85 RETOUCHE_BLOC.lastIndex = 0 86 let m 87 while ((m = RETOUCHE_BLOC.exec(t)) !== null) { 88 sorties.push({ chemin: m[1], avant: m[2], apres: m[3] }) 89 } 90 return sorties 91} 92 93/* POSER LES RETOUCHES â ET DIRE CE QU'ON N'A PAS POSÃ. 94 * 95 * Elle rend une LISTE NEUVE ; les fichiers qu'elle touche sont des objets 96 * neufs, ceux qu'elle ne touche pas sont les mêmes. On ne mute pas l'entrée : 97 * l'appelant a besoin de l'état d'avant pour la vue Changements. 98 * 99 * TROIS REFUS, ET CHACUN A SA RAISON ÃCRITE : 100 * · le fichier n'existe pas â on ne le CRÃE pas depuis une retouche : une 101 * retouche dont le fichier manque est un chemin faux, pas un fichier neuf ; 102 * · le texte AVANT ne s'y trouve pas â le modèle l'a écrit de mémoire ; 103 * · il s'y trouve DEUX FOIS â on ne devine pas laquelle des deux, et poser 104 * la première serait juste une fois sur deux. */ 105/* PLUS RÃCENT, PAS « AUSSI RÃCENT ». `synchroniserStudioCode` compare 106 `f.majAt || 0` à `e.majAt` et, à ÃGALITÃ, LE REGISTRE GAGNE â c'est écrit 107 dans sa note, et c'est juste : renommer dans la page Fichiers et accorder 108 dans la foulée tient dans la même milliseconde. 109 Donc `Date.now()` ne suffit pas. Une génération qui retombe sur la même 110 milliseconde que le dernier accord serait ANNULÃE â et c'est arrivé, en 111 écrivant la garde : elle a échoué sur ce cas-là avant d'échouer sur rien. 112 On date d'un cran au-dessus de ce que le fichier portait : ce qu'on vient 113 d'écrire est plus récent que ce qu'il y avait, par construction. */ 114function _plusRecentQue(fichier, quand) { 115 return Math.max(quand, ((fichier && fichier.majAt) || 0) + 1) 116} 117 118function appliquerRetouches(fichiers, retouches, maintenant) { 119 const liste = Array.isArray(fichiers) ? fichiers : [] 120 const quand = maintenant || Date.now() 121 const parChemin = Object.create(null) 122 liste.forEach(function (f, i) { parChemin[String(f.path || f.name || '')] = i }) 123 124 const sortie = liste.slice() 125 const posees = [] 126 const refusees = [] 127 128 ;
128(retouches || []).forEach(function (r) { 129 const chemin = String(r.chemin || '') 130 const i = parChemin[chemin] 131 if (i === undefined) { 132 refusees.push({ chemin: chemin, pourquoi: "ce fichier n'existe pas dans le projet" }) 133 return 134 } 135 const avant = String(sortie[i].content == null ? '' : sortie[i].content) 136 const cherche = String(r.avant == null ? '' : r.avant) 137 if (!cherche) { refusees.push({ chemin: chemin, pourquoi: 'le texte AVANT est vide' }); return } 138 const premier = avant.indexOf(cherche) 139 if (premier < 0) { 140 refusees.push({ chemin: chemin, pourquoi: 'le texte AVANT ne se trouve pas dans le fichier' }) 141 return 142 } 143 if (avant.indexOf(cherche, premier + 1) >= 0) { 144 refusees.push({ chemin: chemin, pourquoi: 'le texte AVANT s\'y trouve plusieurs fois â il faut en prendre plus pour le rendre unique' }) 145 return 146 } 147 /* LA DATE, ENCORE. Même raison que dans la fusion : sans elle, l'accord du 148 registre rend l'ancien contenu au rendu suivant et la retouche disparaît 149 sans un mot. `fid` est conservé â c'est par lui que l'accord retrouve 150 l'entrée au lieu d'en fabriquer une seconde. */ 151 sortie[i] = Object.assign({}, sortie[i], { 152 content: avant.slice(0, premier) + String(r.apres == null ? '' : r.apres) + avant.slice(premier + cherche.length), 153 majAt: _plusRecentQue(sortie[i], quand), 154 }) 155 posees.push(chemin) 156 }) 157 158 return { fichiers: sortie, posees: posees, refusees: refusees } 159} 160 161/* FUSIONNER â un fichier rendu remplace le sien, les autres restent en place. 162 * 163 * L'ORDRE EST CELUI DU PROJET, pas celui de la réponse. Un projet dont les 164 * fichiers changent de place à chaque génération est un projet qu'on ne 165 * retrouve plus ; les fichiers VRAIMENT neufs vont à la fin, dans l'ordre où 166 * le modèle les a écrits. 167 * 168 * `fid` SURVIT au remplacement : sans lui, l'accord du registre ne reconnaît 169 * plus l'entrée par son identifiant et retombe sur une recherche par nom â 170 * qui marche, jusqu'au jour où deux fichiers portent le même. 171 * 172 * ââ ET QUAND LES DEUX ONT TRAVAILLà SUR LE MÃME FICHIER âââââââââââââââââââ 173 * 174 * « Le mode manuel, ça veut dire la personne travaille. » 175 * 176 * Le Studio Code n'a pas de sélecteur IA/Manuel, et il n'en a pas besoin : le 177 * GESTE sépare déjà les deux â taper dans l'éditeur dit « manuel », envoyer un 178 * message dit « IA ». Ce qui manquait n'était pas une bascule, c'était 179 * l'ARBITRAGE quand les deux gestes portent sur le même fichier. 180 * 181 * ââ CE QUI SE PASSAIT, ET C'ÃTAIT SILENCIEUX âââââââââââââââââââââââââââââ 182 * 183 * Cette ligne : `Object.assign({}, sortie[i], n, â¦)`. `n` est la version de 184 * Créa ; elle écrase `content`, et `_plusRecentQue` lui donne une date 185 * STRICTEMENT plus récente â donc elle regagne aussi l'accord suivant avec le 186 * registre. Une génération dure quelques secondes ; ce qu'on tape pendant ce 187 * temps-là dans le fichier qu'elle réécrit disparaît à l'atterrissage. Pas de 188 * conflit affiché, pas de refus, pas une ligne dans le fil. 189 * 190 * ââ LA RÃGLE âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 191 * 192 * `luAt` est le moment où Créa a LU le projet â l'instant où son état est parti 193 * dans le prompt. Un fichier dont la date dépasse ce moment a été touché par la 194 * personne APRÃS que Créa l'a lu : ce que Créa rend est alors une PROPOSITION 195 * (`f.propose`), posée à côté du fichier, jamais à sa place. Elle se lit dans la 196 * vue Changements, ligne à ligne, et la personne accepte ou refuse. 197 * 198 * ââ ET UNE RETOUCHE, ELLE, S'APPLIQUE QUAND MÃME âââââââââââââââââââââââââ 199 * 200 * `appliquerRetouches` n'est PAS arbitrée, et c'est une différence de nature, 201 * pas un oubli. Une retouche est ancrée sur son texte AVANT : si la personne a 202 * changé cette région-là , elle est déjà refusée par le contrôle qui existe 203 * (« le texte AVANT ne se trouve pas ») ; si elle a changé ailleurs, la 204 * retouche se pose proprement sur SA version. Un correctif ciblé n'écrase rien. 205 * C'est le fichier réécrit en entier qui écrase. 206 * 207 * ââ SANS `luAt`, RIEN NE CHANGE ââââââââââââââââââââââââââââââââââââââââââ 208 * 209 * `luAt` vaut zéro tant que personne ne l'a posé, et l'arbitrage se tait alors. 210 * Une lecture jamais datée ferait sinon de CHAQUE fichier une proposition â le 211 * studio n'écrirait plus rien, et ce serait pire que le défaut d'origine. */ 212function fusionnerFichiersCode(anciens, neufs, maintenant, luAt) { 213 const base = Array.isArray(anciens) ? anciens : [] 214 const quand = maintenant || Date.now() 215 const lu = Number(luAt) > 0 ? Number(luAt) : 0 216 const sortie = base.slice() 217 const parChemin = Object.create(null) 218 sortie.forEach(function (f, i) { parChemin[String(f.path || f.name || '')] = i }) 219 220 ;
220(neufs || []).forEach(function (n) { 221 const chemin = String(n.path || n.name || '') 222 if (!chemin) return 223 const i = parChemin[chemin] 224 if (i === undefined) { 225 parChemin[chemin] = sortie.length 226 sortie.push(Object.assign({}, n, { path: chemin, majAt: quand })) 227 return 228 } 229 const toucheDepuis = lu > 0 && Number(sortie[i].majAt || 0) > lu 230 if (toucheDepuis) { 231 /* ON NE DATE PAS LE FICHIER. Poser une proposition n'est pas l'écrire : 232 si `majAt` bougeait ici, la version de Créa gagnerait l'accord du 233 registre sans que personne l'ait acceptée â c'est-à -dire par la porte 234 de derrière, exactement ce que l'arbitrage empêche par la grande. */ 235 sortie[i] = Object.assign({}, sortie[i], { 236 propose: { content: String(n.content == null ? '' : n.content), quand: quand }, 237 }) 238 return 239 } 240 sortie[i] = Object.assign({}, sortie[i], n, { path: chemin, majAt: _plusRecentQue(sortie[i], quand) }) 241 /* UNE PROPOSITION EN ATTENTE NE SURVIT PAS à SON PROPRE FICHIER RÃÃCRIT. 242 Sans cette ligne, `Object.assign` la recopie telle quelle : on garderait 243 une proposition qui compare l'ancien contenu à une suggestion périmée, et 244 l'accepter ramènerait un état que plus personne n'a demandé. */ 245 if (sortie[i].propose) delete sortie[i].propose 246 if (base[i] && base[i].fid) sortie[i].fid = base[i].fid 247 }) 248 249 return sortie 250} 251 252/* ACCEPTER, OU REFUSER. Les deux gestes vivent ici, avec la règle qui a créé la 253 proposition â un geste rangé loin de sa mécanique finit par ne plus la 254 décrire. 255 ACCEPTER, C'EST ÃCRIRE, donc c'est daté : sans date, l'accord du registre 256 rend l'ancien contenu au rendu suivant et l'acceptation disparaît sans un 257 mot. C'est la même raison que partout ailleurs dans ce fichier. */ 258function accepterLaProposition(fichier, maintenant) { 259 if (!fichier || !fichier.propose) return false 260 fichier.content = String(fichier.propose.content == null ? '' : fichier.propose.content) 261 fichier.majAt = _plusRecentQue(fichier, maintenant || Date.now()) 262 delete fichier.propose 263 return true 264} 265 266function refuserLaProposition(fichier) { 267 if (!fichier || !fichier.propose) return false 268 delete fichier.propose 269 return true 270} 271 272/* CE QUE LE FIL DIT QUAND UNE RETOUCHE EST REFUSÃE. 273 * 274 * Une ligne par refus, avec le chemin et la raison. Elle est ici, et pas dans 275 * le tronc, pour la même raison que le reste : c'est la retouche qui sait ce 276 * qu'elle n'a pas pu faire, et une phrase écrite à côté de la mécanique finit 277 * par ne plus la décrire. */ 278function phraseDesRefus(refusees) { 279 if (!refusees || !refusees.length) return '' 280 return '\n\nâ ï¸ ' + refusees.length + ' retouche(s) non appliquée(s) â le fichier est resté tel quel :\n' 281 + refusees.map(function (r) { return '· ' + r.chemin + ' â ' + r.pourquoi }).join('\n') 282} 283 284/* ââ L'ATTERRISSAGE D'UNE RÃPONSE DE CODE, EN UN SEUL ENDROIT ââââââââââââââ 285 * 286 * Ce que le tronc faisait à la main, et qu'il ne doit pas tenir : dans quel 287 * ordre poser les retouches et les fichiers, quel fichier ouvrir ensuite, et 288 * quoi dire dans le fil. Trois décisions de métier, et le métier est ici. 289 * 290 * L'ORDRE, ET IL N'EST PAS INTERCHANGEABLE. Une retouche vise le fichier TEL 291 * QU'IL EST. Si un fichier entier rendu dans la même réponse écrasait d'abord 292 * ce fichier-là , le texte AVANT ne s'y trouverait plus et la retouche serait 293 * refusée pour une raison qu'on aurait fabriquée soi-même. 294 * 295 * ON OUVRE SUR CE QU'ON VIENT DE TOUCHER. Le tronc posait `activeCodeFile = 0` 296 * â le premier fichier du PROJET, qui n'est plus celui de la réponse dès lors 297 * qu'on fusionne au lieu de remplacer. */ 298/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 299 * CE QUI ATTERRIT EST-IL SEULEMENT LISIBLE ? 300 * 301 * Rien ne le vérifiait. Un fichier arrivait cassé â une accolade en trop, un 302 * `package.json` avec une virgule finale â et il se posait sans un mot. On le 303 * découvrait en cliquant sur Exécuter, devant un cadre blanc, et il fallait
304 * remonter soi-même jusqu'au fichier fautif. Un studio professionnel dit 305 * l'erreur AU MOMENT où elle entre, pas trois gestes plus tard. 306 * 307 * ââ ON NE JUGE QUE CE QU'ON PEUT JUGER SANS SE TROMPER ââââââââââââââââââââ 308 * 309 * Une fausse alerte de syntaxe est pire que pas d'alerte : elle envoie 310 * chercher un défaut qui n'existe pas, dans du code qui marche. Donc : 311 * 312 * · JSON â `JSON.parse`. Exact, et le moteur donne la POSITION, qu'on 313 * convertit en numéro de ligne. Un `package.json` cassé est le cas le plus 314 * silencieux du lot : plus rien ne s'installe et rien ne le dit. 315 * · JS classique â `new Function(code)` COMPILE sans exécuter (vérifié : 316 * un appel posé dans le corps ne part pas). C'est donc un analyseur 317 * syntaxique gratuit, déjà dans le navigateur. 318 * · Le reste â SILENCE. Voir ci-dessous. 319 * 320 * ââ LES DEUX FAUX POSITIFS, ET CE QU'ON EN FAIT âââââââââââââââââââââââââââ 321 * 322 * · `await` au premier niveau lève dans une fonction ordinaire alors que le 323 * code est juste. On rejoue en fonction ASYNC, et on ne conclut que si les 324 * DEUX échouent. 325 * · `import` / `export` lèvent toujours â « Cannot use import statement 326 * outside a module » â sur du code parfaitement valide. Rien ne rattrape 327 * ça sans un vrai analyseur, donc ces fichiers ne sont PAS jugés. Idem 328 * pour JSX et TypeScript, qui ne sont pas du JavaScript. 329 * 330 * Le silence est ici la bonne réponse : ce qui est dit est vrai, et ce qui 331 * n'est pas dit n'est pas affirmé faux. 332 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 333
334/* Au-delà , on ne compile pas : un fichier de cette taille dans le studio est 335 déjà un cas limite, et l'analyse bloquerait le fil au moment du rendu. */ 336const SYNTAXE_SIGNES_MAX = 400000 337const _RE_MODULE = /^[ \t]*(import|export)[\s{*]/m 338const _AsyncFunction = Object.getPrototypeOf(async function () {}).constructor 339 340function _ligneDePosition(texte, position) { 341 const n = Number(position) 342 if (!Number.isFinite(n) || n < 0) return null 343 return String(texte).slice(0, n).split('\n').length 344} 345 346/** Rend `[{ chemin, ligne, message }]` â vide quand tout se compile ou que 347 rien n'est jugeable. Pure : aucune écriture, aucun DOM. */ 348function verifierLaSyntaxe(fichiers) { 349 const constats = [] 350 for (const f of (fichiers || [])) { 351 if (!f) continue 352 const chemin = String(f.path || f.name || '') 353 const code = String(f.content == null ? '' : f.content) 354 if (!code.trim() || code.length > SYNTAXE_SIGNES_MAX) continue 355 356 if (/\.json$/i.test(chemin)) { 357 try { JSON.parse(code) } catch (e) { 358 const m = String((e && e.message) || e) 359 /* LE NUMÃRO DE LIGNE N'EST CALCULà QUE SI LE MOTEUR NE LE DONNE PAS. 360 V8 récent écrit déjà « (line 4 column 1) » dans le message ; le 361 répéter donnerait « ligne 4 â ⦠(line 4 column 1) », qui se lit 362 comme deux mesures indépendantes qui tomberaient d'accord. D'autres 363 moteurs ne donnent que la position : c'est pour eux qu'on convertit. */ 364 const pos = m.match(/position (\d+)/) 365 const deja = /\bline \d+/.test(m) 366 constats.push({ chemin, ligne: !deja && pos ? _ligneDePosition(code, pos[1]) : null, message: m }) 367 } 368 continue 369 } 370 if (!/\.(js|mjs|cjs)$/i.test(chemin)) continue 371 if (_RE_MODULE.test(code)) continue 372 try { new Function(code) } catch (e1) { 373 try { new _AsyncFunction(code) } catch (e2) { 374 constats.push({ chemin, ligne: null, message: String((e2 && e2.message) || e2) }) 375 } 376 } 377 } 378 return constats 379} 380 381/** Ce que le fil en dit. Vide quand il n'y a rien à dire. */ 382function phraseDesSyntaxes(constats) { 383 if (!constats || !constats.length) return '' 384 return '\nâ ' + constats.length + ' fichier(s) ne se compile(nt) pas :\n' 385 + constats.map(function (c) { 386 return '· ' + c.chemin + (c.ligne ? ' ligne ' + c.ligne : '') + ' â ' + c.message 387 }).join('\n') 388} 389 390/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 391 * LA MÃME MACHINE, POUR UN ATELIER QUI N'EST PAS UN PROJET 392 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 393 * 394 * Le Studio Web tient UNE page, pas un arbre de fichiers. Il avait donc, seul 395 * contrat de sortie, « rends un document complet » â et « change la couleur du 396 * bouton » faisait réémettre vingt-trois mille signes, dont le modèle 397 * réinventait au passage tout ce qu'il n'avait pas relu. 398 * 399 * On ne lui écrit PAS un second poseur. Un atelier d'un seul document est un 400 * projet d'un seul fichier : on le présente tel quel à `appliquerRetouches`, 401 * qui garde ses trois refus et sa règle d'unicité. Deux poseurs finiraient par 402 * ne plus refuser les mêmes choses, et personne ne s'en apercevrait avant 403 * qu'une page soit à moitié posée. 404 * 405 * Le chemin est fixe â c'est celui que `STUDIOS.web.defautModif` écrit au 406 * modèle, et une retouche qui en vise un autre est REFUSÃE avec son nom, 407 * plutôt que posée au hasard sur le seul document présent. 408 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 409const ATELIER_UN_SEUL_DOCUMENT = 'page.html' 410 411/* ââ UN ATELIER N'EST PAS TOUJOURS UNE PAGE âââââââââââââââââââââââââââââââ 412 * 413 * MESURà : `defautModif` n'existait que sur DEUX studios sur quinze â Code et 414 * Web. Partout ailleurs, « change juste ce paragraphe » faisait réémettre le 415 * document entier : c'est exactement le défaut que ce contrat existe pour 416 * fermer, laissé ouvert sur treize lieux. 417 * 418 * ET ON NE PEUT PAS SE CONTENTER DE RECOPIER LE CONTRAT. Un contrat que rien 419 * ne relit est un mensonge de plus â la doctrine de ce fichier. Le poseur
420 * était bordé à `surface !== 'web'` : il fallait d'abord lui apprendre les 421 * autres ateliers. 422 * 423 * TROIS FAMILLES, ET LA DIFFÃRENCE N'EST PAS COSMÃTIQUE â c'est Oà VIT LA 424 * VÃRITà du document : 425 * 426 * · `html` â Web, Design. Le document EST son HTML. On retouche `html`. 427 * · `md` â Document, Business, Coworker. La vérité est le MARKDOWN ; 428 * `html` en est DÃRIVà (`renderMarkdown`). Retoucher le html serait 429 * retoucher une ombre : le rendu suivant le refabrique depuis le markdown 430 * inchangé, et la retouche disparaît sans un mot. 431 * · STRUCTURÃE â Slides, Vidéo, Quiz, Graphique, Tableau de bord. La vérité 432 * est un JSON (`deck`, `quiz`, `chart`â¦) dont le html est construit. Ces 433 * ateliers-là N'ONT PAS de retouche de texte, et n'en reçoivent pas le 434 * contrat : ce serait la même ombre, en pire. 435 * 436 * LE CHEMIN SUIT LA FAMILLE, parce que c'est lui que le contrat écrit au 437 * modèle et que le poseur REFUSE quand il ne correspond pas. `page.html` pour 438 * un markdown ferait refuser toutes les retouches, avec un motif juste et 439 * incompréhensible. */ 440const FAMILLES_D_ATELIER = [ 441 { champ: 'html', chemin: ATELIER_UN_SEUL_DOCUMENT }, 442 { champ: 'md', chemin: 'document.md' }, 443] 444/* CE QUI DIT QU'UN DOCUMENT EST STRUCTURà : il porte sa donnée. On liste les 445 clés plutôt qu'un `kind`, parce que c'est la donnée que `detectStudioArtifact` 446 pose ET relit â un `kind` peut manquer, la clé non. */ 447const DONNEES_STRUCTUREES = ['deck', 'quiz', 'cards', 'chart', 'dashboard', 'storyboard'] 448 449/** La famille de l'atelier courant, ou `null` s'il n'y a rien à retoucher. 450 * `null` couvre trois cas qui se ressemblent et ne sont pas les mêmes : 451 * pas de document, un document structuré, un document vide. */ 452function familleDeLAtelier(doc) { 453 if (!doc) return null 454 for (const k of DONNEES_STRUCTUREES) if (doc[k]) return null 455 /* L'ORDRE COMPTE : un document markdown porte AUSSI un `html` dérivé. Si on 456 testait `html` d'abord, tous les documents du Studio Document tomberaient 457 dans la famille HTML â et chaque retouche serait posée sur l'ombre. */ 458 if (typeof doc.md === 'string' && doc.md.trim()) return FAMILLES_D_ATELIER[1] 459 if (typeof doc.html === 'string' && doc.html.trim()) return FAMILLES_D_ATELIER[0] 460 return null 461} 462 463function poserLaRetoucheDeLAtelier(avant, retouches, maintenant, chemin) { 464 const liste = Array.isArray(retouches) ? retouches : [] 465 if (!liste.length) return null 466 const pose = appliquerRetouches( 467 [{ path: chemin || ATELIER_UN_SEUL_DOCUMENT, content: String(avant == null ? '' : avant) }], 468 liste, 469 maintenant || Date.now() 470 ) 471 return { 472 /* AUCUNE RETOUCHE POSÃE â on rend `null` pour le document : l'appelant 473 garde la page telle quelle. Rendre le contenu inchangé marcherait aussi, 474 mais ferait passer un tour blanc pour une écriture, et l'historique de 475 versions garderait une version identique à la précédente. */ 476 html: pose.posees.length ? pose.fichiers[0].content : null, 477 posees: pose.posees.length, 478 dit: 'â ' + (pose.posees.length ? pose.posees.length + ' retouche(s) posée(s)' : 'rien à écrire'), 479 refus: phraseDesRefus(pose.refusees), 480 } 481} 482 483/* CE QUE LE TRONC APPELLE â trois lignes, pas trente. 484 * 485 * `src/app.js` a un plafond de lignes qui ne peut que descendre, et il a 486 * raison : ce qui s'écrit de neuf vit dans un morceau. Le tronc garde donc 487 * l'appel, et la lecture, la pose et la phrase du fil vivent ici, avec le 488 * poseur qu'elles emploient. */ 489 490/** Ce studio a-t-il retouché son document ? Rend l'artefact, ou null. 491 * On lit AVANT le bloc ```html : le contrat dit « ou tu retouches, ou tu 492 * refais », et devant les deux formes on prend la retouche, qui est la 493 * lecture la moins destructrice. 494 * 495 * CE N'EST PLUS `surface !== 'web'`. La borne n'était pas le LIEU, c'était ce 496 * que l'atelier CONTIENT : un document dont la vérité est du texte se 497 * retouche, un document structuré non. `familleDeLAtelier` le dit, et elle le 498 * dit pour les quinze studios d'un coup â au lieu d'une liste de noms qu'il 499 * faudrait rallonger à chaque lieu ajouté. */ 500function artefactDeRetoucheDeLAtelier(contenu, docAvant) { 501 if (!familleDeLAtelier(docAvant)) return null 502 const retouches = retouchesDuTexte(contenu) 503 return retouches.length ? { kind: 'web', retouches: retouches, html: null, md: contenu } : null 504} 505
506/** Poser cette retouche sur l'atelier qui est PARTI DANS LE PROMPT â c'est de 507 * ce texte-là que le modèle a recopié son AVANT, d'aucun autre. Ãcrit 508 * `artifact.html`, et rend ce qu'il faut dire dans le fil. */ 509function atterrirLaRetoucheDeLAtelier(artifact, docAvant, maintenant) { 510 const famille = familleDeLAtelier(docAvant) 511 const avant = famille ? String(docAvant[famille.champ] || '') : '' 512 if (!avant) { 513 artifact.html = '' 514 return { dit: '', posees: 0, refus: "\n\nâ ï¸ Aucun document n'est ouvert dans l'atelier : il n'y a rien à retoucher.", phrase: "â ï¸ Aucun document n'est ouvert dans l'atelier : il n'y a rien à retoucher." } 515 } 516 const pose = poserLaRetoucheDeLAtelier(avant, artifact.retouches, maintenant, famille.chemin) 517 /* AUCUNE RETOUCHE POSÃE â le document d'avant reste. Le remplacer par du vide 518 effacerait le travail au moment précis où l'on vient de dire pourquoi on 519 n'y a pas touché. */ 520 const apres = (pose && pose.html) || avant 521 if (famille.champ === 'md') { 522 /* LE MARKDOWN EST LA VÃRITÃ, LE HTML EN EST TIRà â et il faut le retirer 523 ici, sinon le document afficherait l'ancien texte en gardant le nouveau 524 en mémoire : le pire des deux états, parce qu'il a l'air de marcher. 525 `renderMarkdown` vit dans le tronc, qui charge AVANT ce morceau ; on le 526 demande par `typeof` plutôt qu'à nu â le cliquet des portées compte les 527 emprunts implicites, et il ne peut que descendre. Sans lui, on garde le 528 markdown à jour et le html d'avant : dégradé, jamais faux. */ 529 artifact.md = apres 530 if (typeof renderMarkdown === 'function') { try { artifact.html = renderMarkdown(apres) } catch (e) { artifact.html = docAvant.html || '' } } 531 else artifact.html = docAvant.html || '' 532 } else { 533 artifact.html = apres 534 } 535 /* LA PHRASE REVIENT AVEC LA POSE, et ce n'est pas de la commodité : le tronc 536 n'emprunte alors qu'UN nom à ce fichier au lieu de deux. Un emprunt 537 implicite entre morceaux tient tant que l'ordre des <script> tient â le 538 cliquet des portées les compte, et il ne peut que descendre. */ 539 pose.phrase = phraseDuTourDAtelier(pose, artifact) 540 return pose 541} 542 543/** Ce que le fil dit d'un tour. Une retouche n'est pas une création : « ouvert 544 * dans le navigateur » devant deux lignes changées fait chercher un nouveau 545 * document, et surtout ferait passer un REFUS inaperçu. */ 546function phraseDuTourDAtelier(pose, artifact) { 547 if (!pose) return 'â **' + ((artifact && (artifact.file || artifact.title)) || 'Création') + '** â ouvert dans le navigateur (et rangé dans la Médiathèque).' 548 return pose.dit + (pose.posees ? ' â la page est à jour dans le navigateur.' : '') + pose.refus 549} 550 551/* CE QUE LE MODÃLE A ÃCRIT NE SE JETTE PAS. 552 * 553 * « Des fois il ne fait pas de récapitulatif de la tâche, rien, mais il vient 554 * m'afficher un fichier écrit, diff affiché. C'est quoi ça ? » 555 * 556 * Le tronc posait `content = atterri.dit` : le texte du modèle â son 557 * explication, ce qu'il a changé, ce qu'il reste à faire â était REMPLACà par 558 * une phrase mécanique. Le comble : quand la réponse ne contient AUCUN fichier, 559 * le même code garde ce texte EN ENTIER. Produire du travail faisait donc 560 * perdre le compte rendu de ce travail. 561 * 562 * `texte` est la réponse brute. On en retire les blocs de fichiers â ils sont 563 * déjà dans l'atelier, les relire dans le fil serait les lire deux fois â et ce 564 * qui reste EST le récit. La ligne de livraison vient après, en pied, comme une 565 * mesure : elle ne remplace plus rien. 566 */ 567function recitDeLaReponse(texte) { 568 return String(texte || '') 569 /* Les blocs NOMMÃS et les retouches sont le LIVRABLE : ils vivent dans 570 l'atelier, pas dans la conversation. Les mêmes motifs que 571 detectStudioArtifact â si l'un change, l'autre doit suivre, et la garde 572 le tient. */ 573 .replace(/```\w+:[^\s]+\n[\s\S]*?```/g, '') 574 .replace(/```modif\n[\s\S]*?```/g, '') 575 /* Un bloc anonyme assez long est traité comme un fichier par 576 detectStudioArtifact (le seuil de cent caractères vient de là ) : il part 577 avec les autres, pour la même raison. */ 578 .replace(/```\w*\n[\s\S]{100,}?```/g, '') 579 .replace(/\n{3,}/g, '\n\n') 580 .trim() 581} 582 583/* Un geste par fichier touché, dans la forme d'une étape d'outil : le nom, le 584 chemin, et le contenu â c'est lui qui porte le compte des lignes, lu par 585 `diffDeLetape` dans le tronc. Les fichiers PROPOSÃS n'en sont pas : rien n'a 586 été écrit tant que la personne n'a pas accepté. */ 587function gestesDeLaPose(avant, apres, posees, neufs) { 588 var touches = {} 589 ;
589(posees || []).forEach(function (c) { if (c) touches[c] = true }) 590 ;(neufs || []).forEach(function (f) { var c = f && (f.path || f.name); if (c) touches[c] = true }) 591 var ancien = {} 592 ;(avant || []).forEach(function (f) { var c = f && (f.path || f.name); if (c) ancien[c] = String(f.content == null ? '' : f.content) }) 593 var gestes = [] 594 ;(apres || []).forEach(function (f) { 595 var chemin = f && (f.path || f.name) 596 if (!chemin || !touches[chemin] || f.propose) return 597 var contenu = String(f.content == null ? '' : f.content) 598 var lignesAvant = ancien[chemin] ? ancien[chemin].split('\n').length : 0 599 var lignesApres = contenu ? contenu.split('\n').length : 0 600 gestes.push({ 601 chemin: chemin, 602 contenu: contenu, 603 /* CE QU'ON SAIT VRAIMENT : un fichier neuf n'enlève rien ; un fichier 604 réécrit gagne ou perd la différence. On ne prétend pas à un diff ligne 605 à ligne â « Changements » le fait, avec les deux versions. */ 606 plus: lignesAvant ? Math.max(0, lignesApres - lignesAvant) : lignesApres, 607 moins: lignesAvant ? Math.max(0, lignesAvant - lignesApres) : 0, 608 }) 609 }) 610 return gestes 611} 612 613/* L'ATELIER FAIT VIVRE SES PASTILLES â ET C'EST LE POSTE DU FLUX QUI LE FAIT, 614 POUR LES DIX STUDIOS à LA FOIS. `gestesEnCours` et `suivreLesGestesDeLAtelier` 615 vivaient ici et ne lisaient qu'UNE forme : ```lang:chemin, celle du Studio 616 Code. Les neuf autres n'avaient donc aucune pastille â le même défaut, neuf 617 fois. `regionsDuTravail` sait découper les TROIS natures depuis toujours : 618 `gestesDuFlux` (src/app-flux-de-studio.js) le lui demande, et ces deux 619 fonctions-ci n'avaient plus qu'à disparaître. Deux méthodes de moins. */ 620 621function poserLaReponseDeCode(fichiersDuProjet, art, maintenant, luAt, texte) { 622 const quand = maintenant || Date.now() 623 const neufs = art && Array.isArray(art.files) ? art.files : [] 624 const retouches = art && Array.isArray(art.retouches) ? art.retouches : [] 625 626 const pose = appliquerRetouches(fichiersDuProjet || [], retouches, quand) 627 const fichiers = fusionnerFichiersCode(pose.fichiers, neufs, quand, luAt) 628 629 const touche = pose.posees[0] || (neufs[0] && (neufs[0].path || neufs[0].name)) || '' 630 const ouvrir = fichiers.findIndex(function (f) { return (f.path || f.name) === touche }) 631 632 /* LES PROPOSITIONS SE COMPTENT SUR LE RÃSULTAT, PAS SUR UN SECOND RETOUR. 633 `fusionnerFichiersCode` a trois appelants ; leur faire tous lire une forme 634 de retour nouvelle pour une information déjà lisible dans les fichiers 635 eux-mêmes serait un canal de plus pour rien. */ 636 const proposees = fichiers.filter(function (f) { return f && f.propose }) 637 638 const dit = [] 639 const ecrits = neufs.length - proposees.length 640 if (ecrits > 0) dit.push(ecrits + ' fichier(s) écrit(s)') 641 if (pose.posees.length) dit.push(pose.posees.length + ' retouche(s) posée(s)') 642 643 return { 644 fichiers: fichiers, 645 ouvrir: ouvrir >= 0 ? ouvrir : 0, 646 /* ââ LES GESTES, POUR QUE CETTE ROUTE PASSE PAR LES PASTILLES ââââââââââ
647 * « Il y a toujours deux systèmes. Le travail normal passe à travers les 648 * pastilles. Mais quand il dit "je construis dans l'atelier, regarde à 649 * droite", il n'utilise pas les pastilles ni rien. » 650 * 651 * C'était vrai, et mesuré : cette route posait des fichiers et le fil les 652 * montrait en CARTES â un affichage à part, rendu seulement une fois le 653 * tour fini. Deux systèmes pour un seul travail. 654 * 655 * Elle rend donc ses actes sous la forme que la frise sait déjà lire : un 656 * geste par fichier, avec ce qu'il a changé. Le tronc n'a qu'à les ouvrir 657 * comme il ouvre ceux d'un outil â même pastille, même diff, même place. */ 658 gestes: gestesDeLaPose(fichiersDuProjet || [], fichiers, pose.posees, neufs), 659 dit: 'â ' + (dit.join(' · ') || 'rien à écrire'), 660 /* LE RÃCIT DU MODÃLE, ET IL PASSE DEVANT LA LIVRAISON. Ce qu'il a compris 661 et fait se lit d'abord ; la ligne de livraison est le PIED de cette 662 phrase-là , pas son remplacement. Vide quand le modèle n'a rien raconté â 663 et là , la livraison reste seule, exactement comme avant. */ 664 recit: recitDeLaReponse(texte), 665 /* LA SYNTAXE SE DIT AVEC LES REFUS, dans la même phrase du fil : ce sont 666 deux façons pour un tour de n'avoir pas abouti, et les séparer ferait 667 lire l'une sans l'autre. */ 668 refus: phraseDesRefus(pose.refusees) + phraseDesPropositions(proposees) 669 + phraseDesSyntaxes(verifierLaSyntaxe(fichiers)), 670 } 671} 672 673/* CE QUE LE FIL DIT QUAND CRÃA N'A PAS ÃCRIT PARCE QUE VOUS ÃCRIVIEZ. 674 * 675 * Sans cette phrase, l'arbitrage serait aussi silencieux que l'écrasement qu'il 676 * remplace : le fichier ne changerait pas, Créa dirait « fichier écrit », et 677 * personne ne saurait qu'une version attend dans la vue Changements. Un refus 678 * muet et un écrasement muet se ressemblent trait pour trait à l'écran. */ 679function phraseDesPropositions(proposees) { 680 if (!proposees || !proposees.length) return '' 681 return '\n\nâ ' + proposees.length + ' fichier(s) que vous avez modifié(s) pendant que je travaillais â ' 682 + 'je ne les ai pas écrasés. Ma version vous attend dans « Changements », à accepter ou à refuser :\n' 683 + proposees.map(function (f) { return '· ' + (f.path || f.name || 'sans nom') }).join('\n') 684} 685 686/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 687 * L'HISTOIRE DU PROJET â CE QU'IL N'Y AVAIT PAS 688 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 689 * 690 * La vue « Changements » le disait elle-même, en toutes lettres à l'écran : 691 * « CRÃA ne garde pas plus loin : il n'y a pas d'historique de versions. » 692 * `_prevFiles` retenait UN cran â l'état d'avant la dernière génération. Deux 693 * générations de suite, et ce qui marchait il y a dix minutes n'existait plus 694 * nulle part. Pas de retour en arrière, pas de branche, pas de « reviens à 695 * avant ». 696 * 697 * ââ CE QU'ON GARDE, ET CE QU'ON NE GARDE PAS âââââââââââââââââââââââââââââ 698 * 699 * Ce n'est PAS un dépôt Git, et le prétendre serait pire que de ne rien avoir. 700 * Pas de branches, pas de fusion, pas de partage : une pile d'états datés, dans 701 * le projet, qu'on peut remonter. Ce qui tient dans le navigateur de la 702 * personne et rien de plus. 703 * 704 * D'Oà DEUX BORNES, ET ELLES SONT Là POUR LE STOCKAGE. Le projet vit dans 705 * `STATE`, qui est sauvegardé en entier : un historique sans borne remplirait 706 * le quota du navigateur, et la sauvegarde échouerait â en emportant tout le 707 * reste, pas seulement l'historique. On borne donc au NOMBRE et aux OCTETS, et 708 * la plus ancienne cède la place. Le nombre seul ne suffit pas : vingt versions 709 * d'un projet de trois mégaoctets, c'est soixante mégaoctets. 710 * 711 * ââ ET LA DATE, ENCORE âââââââââââââââââââââââââââââââââââââââââââââââââââ 712 * 713 * Restaurer, c'est écrire. `synchroniserStudioCode` donne la victoire au 714 * registre à égalité de date : une restauration non datée serait ANNULÃE au 715 * rendu suivant, exactement comme l'était une génération. C'est le même 716 * `_plusRecentQue` qui la tient, et c'est la raison pour laquelle l'histoire
717 * vit ici et pas dans un treizième morceau. 718 */ 719 720/* Combien d'états on remonte, et combien de place on y met. Vingt pas en 721 arrière couvrent une séance de travail ; quatre mégaoctets, c'est un projet 722 de code confortable répété plusieurs fois, et loin du quota d'un navigateur. */ 723const VERSIONS_MAX = 20 724const VERSIONS_OCTETS_MAX = 4 * 1024 * 1024 725 726function _octetsDesFichiers(fichiers) { 727 let n = 0 728 ;(fichiers || []).forEach(function (f) { n += String(f.content || '').length + String(f.path || f.name || '').length }) 729 return n 730} 731 732/* NOTER UN ÃTAT â l'état COURANT, avant qu'on y touche. 733 * 734 * Elle rend la NOUVELLE pile ; elle ne mute pas celle qu'on lui donne. Une 735 * version ne garde que ce qui la définit â chemin et contenu â jamais le `fid` 736 * du registre : une entrée supprimée depuis laisserait un identifiant mort qui 737 * ferait recoller la restauration à un fichier qui n'existe plus. 738 * 739 * ON NE NOTE PAS DEUX FOIS LE MÃME ÃTAT. Deux messages de suite sans le moindre 740 * changement rempliraient la pile de copies identiques et pousseraient dehors 741 * les états qui, eux, disaient quelque chose. */ 742function noterVersion(versions, fichiers, dit, quand) { 743 const pile = Array.isArray(versions) ? versions.slice() : [] 744 const etat = (fichiers || []).map(function (f) { 745 return { path: String(f.path || f.name || ''), content: String(f.content == null ? '' : f.content) } 746 }) 747 if (!etat.length) return pile 748 749 const empreinte = etat.map(function (f) { return f.path + '\u0000' + f.content }).join('\u0001') 750 if (pile.length && pile[0]._empreinte === empreinte) return pile 751 752 pile.unshift({ 753 id: 'v' + (quand || Date.now()) + '-' + pile.length, 754 at: quand || Date.now(), 755 dit: String(dit || '').slice(0, 120), 756 fichiers: etat, 757 _empreinte: empreinte, 758 }) 759 760 while (pile.length > VERSIONS_MAX) pile.pop() 761 /* LA BORNE D'OCTETS SE COMPTE APRÃS COUP, et on garde TOUJOURS la plus 762 récente : un projet plus gros que la borne à lui seul viderait la pile 763 entière et ne garderait rien â un historique qui s'efface à mesure qu'on 764 s'en sert le plus. */ 765 let total = 0 766 const gardees = [] 767 for (const v of pile) { 768 total += _octetsDesFichiers(v.fichiers) 769 if (gardees.length && total > VERSIONS_OCTETS_MAX) break 770 gardees.push(v) 771 } 772 return gardees 773} 774 775/* CE QUE L'ÃCRAN MONTRE â sans les contenus, qui pèsent et qu'on n'affiche pas. 776 Elle existe pour que la vue n'ait pas à connaître la forme d'une version, et 777 pour que le morceau reste PUR : il rend des données, jamais du HTML. */ 778function versionsLisibles(versions) { 779 return (Array.isArray(versions) ? versions : []).map(function (v, i) { 780 return { 781 id: v.id, at: v.at, dit: v.dit, 782 rang: i, fichiers: (v.fichiers || []).length, 783 octets: _octetsDesFichiers(v.fichiers), 784 } 785 }) 786} 787 788/* REMONTER à UN ÃTAT. 789 * 790 * ON REMPLACE, ON NE FUSIONNE PAS â et c'est la différence avec 791 * `fusionnerFichiersCode`. Revenir en arrière veut dire que les fichiers créés 792 * DEPUIS s'en vont : les garder rendrait un état qui n'a jamais existé, et 793 * c'est précisément ce qu'on vient chercher ici. 794 * 795 * LE `fid` SUIT LE CHEMIN. Un fichier qui existe encore garde son entrée de 796 * registre ; sans ça, l'accord en fabriquerait une seconde à côté de la 797 * première et la page Fichiers montrerait le fichier en double. */ 798function restaurerVersion(fichiersActuels, version, quand) { 799 const actuels = Array.isArray(fichiersActuels) ? fichiersActuels : [] 800 const parChemin = Object.create(null) 801 actuels.forEach(function (f) { parChemin[String(f.path || f.name || '')] = f }) 802 const t = quand || Date.now() 803 return ((version && version.fichiers) || []).map(function (f) { 804 const chemin = String(f.path || '') 805 const ancien = parChemin[chemin] 806 const neuf = { path: chemin, content: String(f.content == null ? '' : f.content), majAt: _plusRecentQue(ancien, t) } 807 if (ancien && ancien.fid) neuf.fid = ancien.fid 808 return neuf 809 }) 810} 811 812/* REMONTER LE TEMPS. La vue disait « il n'y a pas d'historique de versions » â 813 c'était vrai, et c'est ce qui change. Elle ne montre RIEN quand il n'y a rien : 814 une section vide sous chaque projet neuf apprendrait qu'il n'y a jamais rien 815 à y voir. La forme des versions est lue par `versionsLisibles`, qui rend des 816 données et jamais du HTML : ce morceau-là reste pur. */ 817function _vueVersions(modeId) { 818 if (modeId !== 'code') return '' 819 const p = typeof projetCodeActif === 'function' ? projetCodeActif() : null 820 const liste = typeof versionsLisibles === 'function' ? versionsLisibles(p && p.versions) : [] 821 if (!liste.length) return '' 822 const ligne = (v) => '<div class="flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-[max(0.75rem,12px)]" style="background:var(--bg-secondary)">' 823 + '<span class="flex-1 truncate" style="color:var(--t-primary)" title="' + escHtml(v.dit || '') + '">' + escHtml(v.dit || 'sans description') + '</span>' 824 + '<span class="shrink-0 text-[max(0.625rem,10px)] tabular-nums" style="color:var(--t-faint)">' + v.fichiers + ' fic. \u00b7 ' + timeAgo(v.at) + '</span>' 825 + '<button onclick="window.codeFaireTournerVersion(' + jsAttr(v.id) + ')" class="shrink-0 rounded-lg px-2 py-1 text-[max(0.625rem,10px)] font-medium" style="background:var(--bg-tertiary);border:1px solid var(--border);color:var(--t-secondary)" title="Ouvrir cet \u00e9tat \u00e0 sa propre adresse, sans toucher au projet">Faire tourner</button>' 826 + '<button onclick="window.codeRestaurerVersion(' + jsAttr(v.id) + ')" class="shrink-0 rounded-lg px-2 py-1 text-[max(0.625rem,10px)] font-medium" style="background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent)" title="Remonter le projet \u00e0 cet \u00e9tat">Remonter</button>' 827 + '</div>' 828 return '<div class="mt-4 pt-3" style="border-top:1px solid var(--border)">' 829 + '<p class="text-[max(0.6875rem,11px)] font-semibold px-1 pb-1.5" style="color:var(--t-faint)">Remonter le temps \u2014 ' + liste.length + ' \u00e9tat(s) gard\u00e9(s) \u00b7 « Faire tourner » ouvre un \u00e9tat \u00e0 c\u00f4t\u00e9, sans toucher au projet</p>' 830 + '<div class="space-y-1">' + liste.map(ligne).join('') + '</div></div>' 831} 832 833/* REMONTER à UN ÃTAT. On note l'état COURANT avant de le remplacer : revenir
834 en arrière doit pouvoir se défaire, sinon c'est un aller simple et personne 835 n'ose s'en servir. Le calcul vit dans src/app-retouche-code.js â c'est lui 836 qui DATE ce qu'il écrit, sans quoi l'accord du registre annulerait la 837 restauration au rendu suivant. */ 838/* DERRIÃRE UN `typeof window`, comme les autres morceaux. Sans lui, rejouer ce 839 fichier hors navigateur â ce que fait sa propre garde â tombe sur « window is 840 not defined » à la ligne d'exposition, et plus rien n'est mesuré. */ 841/* PAS D'EXPOSITION SUR `window` POUR CES DEUX-LÃ. Elles sont déjà joignables 842 par leur nom â les morceaux sont des scripts CLASSIQUES et leurs fonctions de 843 haut niveau vivent dans l'environnement lexical global. Les reposer sur 844 `window` ferait deux noms pour une seule fonction, et c'est précisément le 845 défaut que ce studio a payé six fois. Le GESTE, lui â celui qu'un `onclick` 846 appelle â vit avec l'écran qui le porte : `window.trancherLaProposition`, 847 dans src/app-lecteur-changements.js. */ 848 849/* ââ FAIRE TOURNER UN ÃTAT PASSÃ, SANS TOUCHER AU PRÃSENT âââââââââââââââââ 850 * 851 * Le voisin de ce bouton, « Remonter », est DESTRUCTIF et le dit : « les 852 * fichiers créés depuis seront retirés ». Comparer deux états revenait donc à 853 * choisir entre lire un diff de TEXTE et détruire le présent pour voir le 854 * passé. Or comparer du texte est tout ce que les plateformes de code savent 855 * faire â et ici un état SE PUBLIE : il a une adresse, il tourne, et deux 856 * adresses tiennent côte à côte dans le navigateur du studio, qui a des 857 * onglets. 858 * 859 * CE GESTE NE TOUCHE à RIEN. Pas de `saveActiveCode`, pas de `p.files`, pas de 860 * `STATE.codePreview` â ce dernier EST le présent, et l'écraser pour montrer le 861 * passé serait remonter en croyant regarder. Le document de la version est 862 * construit à part, publié sous SA clé, et l'adresse s'ouvre dans un onglet. 863 * C'est ce qui distingue « faire tourner » de « remonter », et une garde le 864 * tient nommément. 865 * 866 * ET IL NE PUBLIE PAS SOUS LA CLà DU PROJET. Celle-là vaut `studio:code:<id>` 867 * et sa forme est gelée pour que les liens déjà partagés tiennent : y écrire un 868 * état passé remplacerait le projet courant à l'adresse que quelqu'un a reçue. 869 * `cleDeLaVersion` (src/crea-vitrine-du-projet.js) garantit la séparation. */ 870if (typeof window !== 'undefined') window.codeFaireTournerVersion = async function (id) { 871 const p = typeof projetCodeActif === 'function' ? projetCodeActif() : null 872 if (!p) return 873 const v = (p.versions || []).find(x => x.id === id) 874 /* `window.logNotice`, ET PAS `logNotice` NU â ici et dans les deux refus plus 875 bas. Le tronc la déclare `window.logNotice = functionâ¦` : l'appeler par son 876 nom nu en fait un LIEN IMPLICITE entre fichiers, que le cliquet des portées 877 compte et qu'il a refusé pour ce lot. Nommer la provenance coûte huit 878 signes et dit d'où vient la fonction. */ 879 if (!v) { window.logNotice('Cet état n\'est plus dans l\'historique.', 'info'); return } 880 const art = typeof projectToArtifact === 'function' 881 ? projectToArtifact({ name: p.name, files: v.fichiers }) 882 : null 883 /* UN ÃTAT SANS RIEN D'EXÃCUTABLE LE DIT. Un projet a pu n'avoir que des 884 fichiers de données à ce moment-là ; ouvrir un onglet vide ferait croire 885 que la page est cassée alors qu'il n'y en avait pas. */ 886 if (!art) { window.logNotice('Cet état n\'a aucun fichier exécutable (.html, .jsx, .js) : il n\'y a rien à faire tourner.', 'info'); return } 887 const doc = typeof buildArtifactDoc === 'function' ? buildArtifactDoc(art, null) : '' 888 const cle = typeof window.cleDeLaVersion === 'function' ? window.cleDeLaVersion(p, v) : null 889 const nom = typeof window.nomDeLaVersion === 'function' ? window.nomDeLaVersion(p, v) : 'Ãtat précédent' 890 /* UN SEUL REFUS POUR LES DEUX ÃCHECS DE PUBLICATION. « la publication n'est 891 pas disponible ici » et « elle a échoué » demandent la même chose à la 892 personne â réessayer ailleurs â et disent la même chose du projet : il 893 n'a pas bougé. Deux lignes d'historique pour un seul geste raté, c'est 894 précisément ce que le cliquet des portes douces compte. */ 895 const url = (doc && cle && typeof window.publierAtelier === 'function') 896 ? await window.publierAtelier(doc, nom, cle) 897 : null 898 if (!url) { window.logNotice('Cet état n\'a pas pu être publié â rien n\'a changé dans le projet.', 'info'); return } 899 /* `window.location`, ET PAS `location` NU. Un nom global emprunté sans le 900 dire est un LIEN IMPLICITE entre fichiers â le cliquet des portées les 901 compte, et il a refusé celui-ci. `window` n'appartient à aucun fichier : 902 le passer par lui nomme la provenance au lieu de l'emprunter. */ 903 const adresse = window.location.origin + url 904 /* ON OUVRE DANS LE NAVIGATEUR DU STUDIO, ET à CÃTà DU PRÃSENT. 905 * 906 * Première version : un ONGLET. Le projet courant tournait dans le premier, 907 * l'état passé dans le second, et on comparait en basculant â de mémoire. La 908 * garde de ce lot-là le disait franchement : « la vue côte à côte n'est pas 909 * tenue, deux onglets sont deux onglets ». 910 * 911 * `CREA_COTE_A_COTE.ouvrir` rend le nom du SECOND espace de noms, et l'état 912 * passé y va : le présent reste à gauche, sous les yeux, pendant qu'on 913 * regarde le passé à droite. Deux pages VIVANTES, pas deux textes. 914 * 915 * ET S'IL REND '' , ON RETOMBE SUR L'ONGLET. C'est le comportement d'avant, 916 * jamais pire â et surtout on ne retombe PAS sur « preview », qui est le 917 * navigateur de l'espace chat : y envoyer un état passé écraserait la page 918 * de quelqu'un qui ne regarde pas ce studio. */ 919 const cote = (window.CREA_COTE_A_COTE && window.CREA_COTE_A_COTE.ouvrir('code')) || 'code' 920 STATE.studioFilesPanelOpen = null 921 STATE.studioListeFichiers = null 922 STATE.studioBrowserPanelOpen = 'code' 923 save() 924 /* PAS DE NOTICE DE SUCCÃS : L'ONGLET QUI S'OUVRE EST LE RETOUR. Le cliquet 925 des portes douces du journal l'a refusée, et il a raison â « c'est un 926 retour que le bouton lui-même peut donner sans occuper une ligne 927 d'historique ». Que le projet n'ait pas bougé est dit sur le bouton, dans 928 son `title`. Les deux notices qui restent ici disent un REFUS : celles-là , 929 rien d'autre ne les dirait. */ 930 /* `window._rbNavigate`, ET PAS `_rbNavigate` NU â même raison que 931 `window.logNotice` et `window.location` plus haut : un nom global emprunté 932 sans le dire est un lien implicite entre fichiers, et le cliquet des 933 portées les compte. */ 934 if (typeof window._rbNavigate === 'function') await window._rbNavigate(cote, adresse, nom) 935 else render() 936} 937 938if (typeof window !== 'undefined') window.codeRestaurerVersion = async function (id) { 939 const p = projetCodeActif(); if (!p) return 940 const v = (p.versions || []).find(x => x.id === id); if (!v) return 941 if (!await confirmModal('Remonter le projet à cet état ? Les fichiers créés depuis seront retirés.', { confirmLabel: 'Remonter' })) return 942 p.versions = noterVersion(p.versions, p.files, 'avant de remonter', Date.now()) 943 p._prevFiles = (p.files || []).map(f => ({ path: f.path || f.name, content: f.content || '' })) 944 const apres = restaurerVersion(p.files, v, Date.now()) 945 /* CE QUI DISPARAÃT DOIT LE DIRE AU REGISTRE â sinon on n'a rien retiré. 946 * 947 * MESURÃ, en rejouant `noterVersion` et `restaurerVersion` telles qu'elles 948 * sont écrites contre un vrai registre : trois fichiers, sept ajoutés, 949 * « Remonter » â `p.files` retombe bien à 3. Puis l'accord suivant les 950 * REMET TOUS LES DIX, parce que les sept entrées vivent encore et qu'une 951 * entrée sans fichier redevient un fichier. Le geste s'annulait tout seul 952 * au rendu d'après, et le compteur du studio ne descendait jamais. 953 *
954 * `codeProjDeleteFile` et `codeProjetSupprimer` posent la pierre tombale, 955 * chacun avec la même phrase écrite à côté â « le registre est la vérité ». 956 * Celui-ci est le troisième geste qui retire des fichiers, et le seul qui 957 * l'avait oublié. La boîte de dialogue, elle, promettait déjà . */ 958 const restent = new Set(apres.map((f) => String(f.path || f.name || ''))) 959 for (const f of p.files || []) { 960 if (restent.has(String(f.path || f.name || ''))) continue 961 try { if (typeof window.fichiersOublierFichierStudio === 'function') window.fichiersOublierFichierStudio(f) } 962 catch (e) { /* registre absent : le retrait ne vaut que pour la session, comme les deux autres gestes */ } 963 } 964 p.files = apres 965 /* REMONTER LE TEMPS OUVRE CE QUI A CHANGà â et c'est maintenant une VUE, 966 plus un drapeau. Cette ligne a armé successivement `codeDiffMode` puis 967 `codeDiffFichier` : deux états qui transformaient l'éditeur de l'atelier 968 en comparaison côte à côte. Le diff a sa surface. `_prevFiles` vient 969 d'être posé juste au-dessus, donc la vue « Changements » a tout ce qu'il 970 lui faut ; on l'ouvre, au lieu d'armer un état que quelqu'un devra 971 désarmer. */ 972 STATE.activeCodeFile = 0 973 if (typeof window !== 'undefined' && typeof window.poserVueStudio === 'function') window.poserVueStudio('code', 'changements') 974 save(); render() 975} 976 977/* UN PROJET DE CODE PAR SON IDENTIFIANT â LA QUESTION APPARTIENT AU STUDIO. 978 * 979 * La page des fichiers doit savoir si un projet existe encore avant d'y sauter : 980 * une entrée du registre survit à la suppression de son projet, et poser un 981 * mort sur la session la laisserait pointée dans le vide. Mais la règle du 982 * registre est qu'une SURFACE ne lit pas les réserves en direct â c'est ce que 983 * tient `tests/fichiers-projet-smoke.mjs`, et elle a raison : deux lectures de 984 * `STATE.codeProjects` divergent au premier renommage. La question se pose donc 985 * ici, au poste qui tient les projets â ce fichier, qui tient déjà l'historique 986 * des versions et le geste qui y remonte. */ 987if (typeof window !== 'undefined') window.projetCodeParId = function (id) { 988 if (!id) return null 989 return (STATE.codeProjects || []).find((p) => p && p.id === id) || null 990} 991 992/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 993 * LES QUATRE GESTES DU PROJET DE CODE â ILS N'AVAIENT AUCUNE PORTE. 994 * 995 * MESURÃ, ET C'EST LE PLUS GROS TROU DU STUDIO. Trois brancheurs vivaient dans 996 * `bindEvents()` â `[data-code-project]`, `[data-code-proj-rename]`, 997 * `[data-code-proj-del]` â et le dépôt ENTIER ne rendait aucun de ces trois 998 * attributs. Pas « rarement » : zéro fois, en comptant les gabarits, les 999 * morceaux détachés, l'extension et le bureau. On ne pouvait donc ni CHANGER de 1000 * projet, ni le RENOMMER, ni le SUPPRIMER â le code des trois gestes était 1001 * complet et attendait des boutons qui n'ont jamais existé. 1002 * 1003 * Et créer le SECOND projet était impossible aussi : `btn-new-code-project-3` 1004 * n'est dessiné que dans la branche `!active` de l'atelier, c'est-à -dire sur 1005 * l'écran qui dit « votre code apparaîtra dans Fichiers ». Dès qu'un projet 1006 * existe, cet écran disparaît, et avec lui la seule porte. 1007 * 1008 * C'EST EXACTEMENT CE QUE LE DÃPÃT AVAIT DÃJà NOMMÃ, deux fois, dans ce même 1009 * fichier : « un brancheur sans bouton ne lève rien, et c'est ce qui le laisse 1010 * en place des années ». Les trois brancheurs partent ; les gestes se posent 1011 * ici, en fonctions nommées, et le menu â® leur ouvre la porte â le seul menu du 1012 * studio dont on est sûr qu'il s'affiche, celui qui porte déjà « Nouveau 1013 * fichier » et « Exécuter dans le navigateur ». 1014 * 1015 * CE QUE LE BRANCHEUR MORT FAISAIT DE FAUX, ET QU'ON NE RECOPIE PAS. Sa 1016 * suppression retirait le projet de `STATE.codeProjects` et s'arrêtait là . Or
1017 * le registre des fichiers est la vérité de ce studio â `codeProjDeleteFile` 1018 * l'écrit en toutes lettres trente lignes plus bas : un fichier retiré sans son 1019 * entrée REVIENT au prochain accord. Un projet supprimé aurait donc laissé tous 1020 * ses fichiers derrière lui. On oublie chaque fichier avant de retirer le 1021 * projet. 1022 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 1023/* LE PROJET QUE LA DEMANDE FAIT NAÃTRE. 1024 * 1025 * Le Studio Code sortait sans rien faire quand aucun projet n'était ouvert â 1026 * `if (!proj) return`, mesuré dans la vraie page : on écrit, on envoie, rien ne 1027 * part et rien ne le dit. Le Studio Design, lui, crée le sien à la volée. 1028 * 1029 * Il naît ICI, avec les autres projets de code, et pas dans le tronc : c'est ce 1030 * fichier qui sait ce qu'est un projet de code. La demande le NOMME, par la 1031 * règle partagée des titres (src/crea-titre-session.js) â pas par une coupe à 1032 * trente caractères, qui tombe au milieu d'un mot. Ce nom se lit ensuite en 1033 * haut de l'écran, puisque la session porte le titre du projet. */ 1034if (typeof window !== 'undefined') window.codeProjetDepuisDemande = function (demande) { 1035 const nom = (typeof titreDeSession === 'function' && titreDeSession(demande)) || 'Projet' 1036 const proj = { id: uid(), name: nom, files: [], createdAt: Date.now() } 1037 STATE.codeProjects = STATE.codeProjects || [] 1038 STATE.codeProjects.unshift(proj) 1039 if (typeof poserProjetActif === 'function') poserProjetActif(proj.id, 'code') 1040 return proj 1041} 1042 1043/* ON NE DEMANDE PLUS LE NOM AVANT DE CRÃER. La boîte « Nom du projet⦠» 1044 * s'ouvrait sur un projet qui n'existait pas encore, et Ãchap ne créait RIEN : 1045 * le geste le plus fréquent du studio était barré par la question à laquelle 1046 * on est le moins capable de répondre à cet instant. Le nom vient de ce dont on 1047 * parle (crea-nom-qui-vient.js) ; « Renommer », dans le â®, redevient une 1048 * correction qu'on fait quand on veut. */ 1049if (typeof window !== 'undefined') window.codeNouveauProjet = function () { 1050 STATE.codeProjects = STATE.codeProjects || [] 1051 const name = window.CREA_NOM.nomQuiVient('Projet', { existants: window.CREA_NOM.nomsDe(STATE.codeProjects) }) 1052 const proj = { id: uid(), name: name, files: [], createdAt: Date.now() } 1053 STATE.codeProjects.unshift(proj) 1054 poserProjetActif(proj.id) 1055 save(); render() 1056} 1057 1058if (typeof window !== 'undefined') window.codeProjetChoisir = function (id) { 1059 if (!id) return 1060 /* SAUVER AVANT DE PARTIR. L'éditeur écrit dans le fichier ouvert du projet 1061 COURANT ; changer de projet sans reposer ce qui est à l'écran perdrait la 1062 frappe des dernières secondes. `saveActiveCode` est le geste que 1063 `codeProjNewFile` appelle déjà pour la même raison. */ 1064 try { saveActiveCode() } catch (e) { /* pas d'éditeur monté : il n'y a rien à reposer */ } 1065 poserProjetActif(id) 1066 save(); render() 1067} 1068 1069if (typeof window !== 'undefined') window.codeProjetRenommer = async function (id) { 1070 const p = (STATE.codeProjects || []).find((x) => x.id === id) || projetCodeActif() 1071 if (!p) return 1072 const name = await promptModal('Renommer le projet', '', p.name, 'Renommer') 1073 if (!name || !name.trim()) return 1074 const avant = p.name 1075 p.name = name.trim() 1076 /* ET LES FICHIERS SUIVENT. Le Studio Code range au grain « projet » : le 1077 * dossier d'un fichier vient du NOM de la réalisation. Renommer sans déplacer 1078 * coupait le travail en deux dossiers, dont l'ancien portait un nom qui 1079 * n'existait plus nulle part. Même porte que les douze autres studios. */ 1080 try { 1081 if (window.fichiersRegistre && window.fichiersRegistre.suivreLeRenommage) { 1082 window.fichiersRegistre.suivreLeRenommage('code', avant, p.name) 1083 } 1084 } catch (e) { /* registre non monté : le nom change, le rangement se refera plus tard */ } 1085 save(); render() 1086} 1087 1088if (typeof window !== 'undefined') window.codeProjetSupprimer = async function (id) { 1089 const p = (STATE.codeProjects || []).find((x) => x.id === id) || projetCodeActif() 1090 if (!p) return 1091 const n = (p.files || []).length
1092 const ok = await confirmModal(`Supprimer le projet « ${escHtml(p.name)} » et ses ${n} fichier(s) ?`, { confirmLabel: 'Supprimer', danger: true }) 1093 if (!ok) return 1094 // Le registre est la vérité : un fichier retiré sans son entrée revient au 1095 // prochain accord â même règle que codeProjDeleteFile, juste en dessous. 1096 for (const f of p.files || []) { 1097 try { if (typeof window.fichiersOublierFichierStudio === 'function') window.fichiersOublierFichierStudio(f) } 1098 catch (e) { /* registre absent : la suppression ne vaut que pour la session */ } 1099 } 1100 // Inscrit avant de retirer : sans marque, l'autre appareil le ressuscite. 1101 if (window.CREA_RECONCILIATION) window.CREA_RECONCILIATION.marquerDans(STATE, 'projetsCodeSupprimes', p.id) 1102 STATE.codeProjects = (STATE.codeProjects || []).filter((x) => x.id !== p.id) 1103 /* LA SESSION QUI VISAIT CE PROJET NE DOIT PAS RESTER POINTÃE SUR UN MORT. 1104 Ce geste y arrivait déjà , par `poserProjetActif` ; le générique des douze 1105 autres studios, non. Les deux demandent maintenant la même porte 1106 (app-domaine-surface.js), qui lit son verbe dans la table des liens. 1107 `poserProjetActif` reste : il pose la SUIVANTE, ce que délier ne fait pas. */ 1108 try { window.delierLesSessionsDeLaRealisation('code', p.id) } catch (e) { /* poste absent : poserProjetActif délie déjà la session courante */ } 1109 poserProjetActif(STATE.codeProjects[0]?.id || null) 1110 save(); render() 1111} 1112 1113/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1114 * LES OUVRIERS DE MONACO â LE DÃFAUT QUI EXPLIQUAIT LES TROIS MANQUES 1115 * 1116 * MESURà DANS UN VRAI CHROMIUM, avant d'écrire une ligne. Un JSON à virgule 1117 * finale, un JS à parenthèse ouverte, un CSS sans accolade fermante, un Python 1118 * cassé : QUATRE langages, ZÃRO marqueur, et `editor.action.formatDocument` 1119 * non supporté partout. L'erreur nommait la cause : 1120 * 1121 * Failed to parse URL from /vs/language/typescript/tsWorker.js 1122 * 1123 * Monaco fait son travail de langage dans des WORKERS. Sans `MonacoEnvironment`, 1124 * il fabrique un worker qui résout ses adresses en RELATIF â et un worker n'a 1125 * pas de page pour base. Les workers ne démarraient donc jamais. Ce n'est pas 1126 * « il manque le soulignement des erreurs » : c'est que tout ce que Monaco sait 1127 * faire d'un langage était éteint. Les diagnostics, le formatage, la complétion 1128 * de TypeScript, le survol des types â rien de tout ça ne pouvait exister. 1129 * 1130 * ââ LE PIÃGE DU `baseUrl`, ET IL A COÃTà UNE MESURE âââââââââââââââââââââââ 1131 * 1132 * Premier jet : `baseUrl` = le dossier `vs`. Le worker a alors cherché 1133 * `/vendor/monaco/vs/language/typescript/â¦` â il RAJOUTE `vs/` lui-même. 1134 * `baseUrl` désigne le PARENT du dossier `vs`, pas `vs`. Mesuré sur l'erreur 1135 * `importScripts ⦠/vs/vs/language/typescript`. 1136 * 1137 * ââ POURQUOI UNE `data:` ET PAS UN FICHIER ââââââââââââââââââââââââââââââââ 1138 * 1139 * Le worker doit être amorcé avec une base ABSOLUE avant d'importer le vrai 1140 * code. Un fichier d'amorce demanderait une route de plus à servir, et il 1141 * devrait connaître l'adresse de Monaco â qui change (route locale, ou CDN si 1142 * elle rend 404). L'amorce est donc construite ici, où cette adresse est 1143 * connue, et passée en `data:`. Elle ne contient que deux instructions, et 1144 * aucune donnée de la personne. 1145 * 1146 * CE QUI EST ÃPROUVÃ, ET CE QUI NE L'EST PAS. Mesuré sur la route locale 1147 * (même origine) : JSON « Trailing comma » ligne 3, JS ligne 3, CSS 1148 * « } expected » ligne 2, Python muet â et le formatage supporté pour les 1149 * trois premiers, refusé pour Python. La bascule CDN n'a PAS été mesurée : un 1150 * `importScripts` d'origine croisée depuis un worker `data:` dépend du CORS du 1151 * CDN et de la politique de la page. Si elle ne passe pas, on retombe 1152 * exactement sur ce qu'on avait avant ce lot â l'éditeur écrit, sans les 1153 * services de langage. 1154 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 1155function amorceDesOuvriers(adresseVs, base) { 1156 var abs = String(adresseVs || '') 1157 if (!abs) return '' 1158 try { abs = new URL(abs, base || (typeof location !== 'undefined' ? location.href : undefined)).toString() } 1159 catch (e) { return '' } 1160 abs = abs.replace(/\/+$/, '') 1161 // `baseUrl` est le PARENT de `vs` : le worker ajoute `vs/` de lui-même. 1162 var parent = abs.replace(/\/[^/]+$/, '/') 1163 return 'self.MonacoEnvironment={baseUrl:' + JSON.stringify(parent) + '};'
1164 + 'importScripts(' + JSON.stringify(abs + '/base/worker/workerMain.js') + ');' 1165} 1166 1167if (typeof window !== 'undefined') window.brancherLesOuvriersDeMonaco = function (adresseVs) { 1168 /* ON NE REBRANCHE PAS SI QUELQU'UN A DÃJà POSà SON ENVIRONNEMENT : écraser 1169 le réglage d'un hôte (l'application de bureau, une intégration) casserait 1170 ce qui marche chez lui pour réparer ce qui manquait ici. */ 1171 if (window.MonacoEnvironment && window.MonacoEnvironment.getWorkerUrl) return false 1172 var amorce = amorceDesOuvriers(adresseVs) 1173 if (!amorce) return false 1174 /* `blob:` ET PAS `data:` â ET C'EST TOUT CE QUI MANQUAIT. 1175 * 1176 * La CSP que l'application se pose à elle-même dit `worker-src 'self' blob:` 1177 * (`appContentSecurityPolicy`, serve.js). `data:` n'y est PAS. Le navigateur 1178 * refusait donc de créer l'ouvrier, et Monaco retombait exactement là où il 1179 * était avant qu'on le branche : pas de diagnostic, pas de formatage, pas de 1180 * complétion, et l'entrée « Formater le fichier » qui ne s'affiche jamais. 1181 * Le commentaire au-dessus prévoyait le risque sans le mesurer ; c'est 1182 * mesuré maintenant, et ce n'était pas le CORS du CDN, c'était notre propre 1183 * politique. 1184 * 1185 * ET LE BLOB HÃRITE DE L'ORIGINE DE LA PAGE. `importScripts` vers 1186 * `/vendor/monaco/...` â le chemin normal, servi par CRÃA â est donc 1187 * same-origin ; un ouvrier `data:` a une origine opaque et n'aurait de toute 1188 * façon rien pu charger. Sur le repli CDN, l'origine de la page rend la 1189 * requête soumise au CORS, que jsDelivr accorde. 1190 * 1191 * UNE SEULE URL POUR TOUS LES OUVRIERS : Monaco appelle `getWorkerUrl` une 1192 * fois par langage. En fabriquer un blob à chaque appel en laisserait un par 1193 * langage derrière soi, vivants jusqu'au rechargement. */ 1194 var urlOuvrier = null 1195 window.MonacoEnvironment = { 1196 getWorkerUrl: function () { 1197 if (urlOuvrier) return urlOuvrier 1198 try { 1199 urlOuvrier = URL.createObjectURL(new Blob([amorce], { type: 'text/javascript' })) 1200 return urlOuvrier 1201 } catch (e) { 1202 /* Pas de `Blob` ni d'`URL.createObjectURL` â un hôte exotique, ou un 1203 banc de garde. On rend l'ancienne forme plutôt que `undefined` : 1204 elle sera peut-être refusée, mais rendre rien ferait tomber Monaco 1205 au lieu de le laisser se replier sur son ouvrier par défaut. */ 1206 return 'data:text/javascript;charset=utf-8,' + encodeURIComponent(amorce) 1207 } 1208 }, 1209 } 1210 return true 1211} 1212 1213/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1214 * FORMATER LE FICHIER â LA PORTE QUI N'EXISTAIT PAS, ET QUI N'AURAIT RIEN FAIT 1215 * 1216 * `editor.action.formatDocument` est dans Monaco depuis toujours. Aucun geste 1217 * du studio ne l'appelait â et l'eût-il appelée, elle n'aurait rien fait : 1218 * mesuré avant ce lot, `isSupported()` rendait FAUX pour json, javascript, css 1219 * ET python. Le formatage vit dans les workers, et les workers ne démarraient 1220 * pas (voir `amorceDesOuvriers` juste au-dessus). Brancher un bouton sur une 1221 * action morte aurait produit exactement le pire cas du dépôt : une porte qui 1222 * ne dit pas qu'elle ne mène nulle part. 1223 * 1224 * L'ENTRÃE NE S'AFFICHE QUE SI LE LANGAGE SAIT SE FORMATER. C'est Monaco qui 1225 * répond, pas une liste d'extensions écrite ici : une liste vieillirait, et 1226 * `isSupported()` est la question exacte. Mesuré après le branchement des 1227 * ouvriers : VRAI pour json, javascript et css ; FAUX pour python â et c'est 1228 * juste, aucun formateur Python n'est livré. Rien ne s'affiche pour Python, et 1229 * personne n'a besoin de lire un refus. 1230 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 1231function _actionDeFormatage() { 1232 if (typeof window === 'undefined' || typeof window.editeurDeCode !== 'function') return null 1233 var ed = null 1234 try { ed = window.editeurDeCode() } catch (e) { return null } 1235 if (!ed || typeof ed.getAction !== 'function') return null 1236 try { return ed.getAction('editor.action.formatDocument') || null } catch (e) { return null } 1237} 1238 1239if (typeof window !== 'undefined') window.codeFormatageDisponible = function () { 1240 var a = _actionDeFormatage() 1241 try { return !!(a && typeof a.isSupported === 'function' && a.isSupported()) } catch (e) { return false } 1242} 1243 1244if (typeof window !== 'undefined') window.codeFormaterLeFichier = function () { 1245 var a = _actionDeFormatage() 1246 if (!a || typeof a.run !== 'function') return 1247 /* APRÃS LE FORMATAGE, LE FICHIER EST REPOSÃ. Monaco réécrit son modèle ; le 1248 projet, lui, ne le sait pas. `saveActiveCode` est le geste qui recopie 1249 l'éditeur dans le fichier ET le date â sans lui, une fusion ultérieure 1250 comparerait une date d'avant le formatage et écraserait tout. */ 1251 try { 1252 var fin = a.run() 1253 if (fin && typeof fin.then === 'function') { 1254 fin.then(function () { try { saveActiveCode(); save() } catch (e) { /* studio démonté entre-temps */ } }) 1255 return 1256 } 1257 } catch (e) { return } 1258 try { saveActiveCode(); save() } catch (e) { /* studio démonté entre-temps */ } 1259} 1260 1261/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1262 * LE RUBAN DES FICHIERS â ON ÃCRIVAIT DANS UN PROJET SANS VOIR SES FICHIERS 1263 * 1264 * L'atelier montrait UN fichier, et rien ne disait qu'il y en avait d'autres. 1265 * Pour en changer il fallait sortir de l'atelier, ouvrir la page Fichiers, et 1266 * revenir : trois gestes pour l'action la plus fréquente de tout le studio. 1267 * 1268 * IL NE CRÃE AUCUN BRANCHEMENT. Les rangées portent `data-code-file="
1268<rang>"`, 1269 * qui est écouté depuis toujours dans `bindEvents()` et déjà rendu par la 1270 * liste de fichiers. C'est l'inverse exact du défaut réparé au lot précédent â 1271 * là il y avait un brancheur sans bouton ; ici il y avait un brancheur, un 1272 * bouton ailleurs, et pas de bouton où l'on travaille. 1273 * 1274 * LE RANG, PAS LE CHEMIN, parce que c'est ce que l'écouteur lit, et parce que 1275 * `STATE.activeCodeFile` est lui-même un rang. Passer par le chemin ferait une 1276 * seconde façon de désigner un fichier, et elles divergeraient au renommage. 1277 * 1278 * ON N'AFFICHE PAS UN RUBAN D'UN SEUL ONGLET : il ne mène qu'à l'endroit où 1279 * l'on est déjà , et il prendrait une rangée de hauteur à l'éditeur. Même règle 1280 * que « Changer de projet » dans le menu du studio. 1281 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 1282function _nomCourtDeFichier(chemin) { 1283 var s = String(chemin || '') 1284 var i = s.lastIndexOf('/') 1285 return i >= 0 ? s.slice(i + 1) : s 1286} 1287 1288function rubanDesFichiersCode(fichiers, actif) { 1289 var liste = fichiers || [] 1290 if (liste.length < 2) return '' 1291 var rang = Number(actif) 1292 if (!Number.isFinite(rang)) rang = 0 1293 var lignes = [] 1294 for (var i = 0; i < liste.length; i++) { 1295 var f = liste[i] || {} 1296 var chemin = String(f.path || f.name || '') 1297 var court = _nomCourtDeFichier(chemin) || 'sans nom' 1298 /* LE CHEMIN COMPLET EN INFOBULLE : deux `index.js` dans deux dossiers 1299 portent le même nom court, et le ruban seul ne les distinguerait pas. */ 1300 lignes.push('<button class="code-onglet' + (i === rang ? ' est-actif' : '') + '"' 1301 + ' data-code-file="' + i + '"' 1302 + ' title="' + escHtml(chemin) + '">' 1303 + escHtml(court) + '</button>') 1304 } 1305 return '<div class="code-onglets" role="tablist" aria-label="Fichiers du projet">' + lignes.join('') + '</div>' 1306} 1307if (typeof window !== 'undefined') window.rubanDesFichiersCode = rubanDesFichiersCode 1308 1309/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1310 * LE PROJET EST UN PROJET, PAS UNE PILE DE FICHIERS SÃPARÃS 1311 * 1312 * CE QUI ÃTAIT FAUX. `monaco.editor.create(host, { value: ⦠})` fabrique un 1313 * modèle ANONYME â une adresse `inmemory://model/1` tirée au compteur. Aucun 1314 * `createModel`, aucun `monaco.Uri` nulle part dans le dépôt. Le worker 1315 * TypeScript voyait donc chaque fichier SEUL au monde : 1316 * 1317 * · « aller à la définition » d'une fonction importée ne menait nulle part ; 1318 * · un `import './outils.js'` ne résolvait rien, donc aucune erreur d'import 1319 * n'était jamais signalée ; 1320 * · renommer un symbole ne pouvait pas suivre ses usages ; 1321 * · et le modèle étant refait à CHAQUE rendu â une notification suffit â 1322 * l'historique d'annulation repartait de zéro plusieurs fois par minute. 1323 * 1324 * MESURà DANS CHROMIUM, deux fichiers et un import entre eux : 1325 * getDefinitionAtPosition â file:///projet/outils.js (ça traverse) 1326 * getReferencesAtPosition â app.js ET outils.js (ça traverse) 1327 * marqueur ligne 4 â « Cannot find name 'manquante' » 1328 * Avant : rien de tout ça, parce qu'il n'y avait pas d'adresses. 1329 * 1330 * ââ LES DEUX RÃGLAGES QU'IL A FALLU MESURER âââââââââââââââââââââââââââââââ 1331 * 1332 * L'URI seule ne suffit pas. Deux options sont FAUSSES par défaut pour du 1333 * JavaScript, et chacune a été vérifiée à part : 1334 * · `checkJs` â sans lui, `manquante(1)` sur une fonction qui n'existe pas 1335 * ne dit rien du tout ; 1336 * · `noSemanticValidation: false` â avec `checkJs` seul, le worker TROUVE 1337 * l'erreur mais elle n'arrive JAMAIS à l'écran. Mesuré : le diagnostic 1338 * existait dans le worker et la liste des marqueurs restait à trois 1339 * avertissements de variable inutilisée. 1340 * 1341 * ââ L'ADRESSE PORTE LE PROJET, ET CE N'EST PAS DÃCORATIF ââââââââââââââââââ 1342 * 1343 * Deux projets ont chacun leur `index.js`. Sous une adresse qui ne porterait 1344 * que le chemin, ils seraient LE MÃME fichier pour le worker, et le second 1345 * écraserait le premier. 1346 * 1347 * ââ ON JETTE LES MODÃLES DES AUTRES PROJETS, ET C'EST UN ARBITRAGE ââââââââ 1348 * 1349 * Les garder ferait résoudre un `import './outils.js'` du projet A vers le 1350 * `outils.js` du projet B : une vérité fausse, pire qu'une absence. CE QU'ON 1351 * PERD : l'historique d'annulation d'un projet qu'on quitte. On le dit plutôt
1352 * que de le cacher â et c'est déjà infiniment plus que ce qu'il y avait, où 1353 * l'historique tombait à chaque rendu. 1354 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 1355 1356/* Le préfixe d'un projet dans l'espace des adresses. `encodeURIComponent` sur 1357 chaque tronçon : un nom de dossier avec un espace ou un « # » fabriquerait 1358 une adresse que Monaco ne relit pas comme on l'a écrite. */ 1359function uriDuFichierCode(idProjet, chemin) { 1360 var p = String(idProjet || 'projet') 1361 var c = String(chemin || '') 1362 if (!c) return '' 1363 var tronçons = c.split('/').filter(function (x) { return x && x !== '.' && x !== '..' }) 1364 if (!tronçons.length) return '' 1365 return 'file:///' + encodeURIComponent(p) + '/' + tronçons.map(encodeURIComponent).join('/') 1366} 1367 1368var _langagesRegles = false 1369 1370/** Une fois par page. Sans ces deux réglages, les adresses ne servent à rien : 1371 mesuré, l'erreur existe dans le worker et n'atteint jamais l'écran. */ 1372function reglerLesLangagesDeMonaco(m) { 1373 if (_langagesRegles || !m || !m.languages || !m.languages.typescript) return false 1374 var ts = m.languages.typescript 1375 var options = { 1376 target: ts.ScriptTarget.ESNext, 1377 module: ts.ModuleKind.ESNext, 1378 moduleResolution: ts.ModuleResolutionKind.NodeJs, 1379 allowJs: true, 1380 checkJs: true, 1381 allowNonTsExtensions: true, 1382 noEmit: true, 1383 } 1384 var diagnostics = { noSemanticValidation: false, noSyntaxValidation: false } 1385 try { 1386 ts.javascriptDefaults.setCompilerOptions(options) 1387 ts.javascriptDefaults.setDiagnosticsOptions(diagnostics) 1388 ts.typescriptDefaults.setCompilerOptions(options) 1389 ts.typescriptDefaults.setDiagnosticsOptions(diagnostics) 1390 } catch (e) { return false } 1391 _langagesRegles = true 1392 return true 1393} 1394 1395/* CE QUE LA SYNCHRONISATION DOIT FAIRE, ET DANS CET ORDRE : 1396 1. donner un modèle à chaque fichier du projet, à son adresse ; 1397 2. REPOSER le contenu quand il a changé HORS de l'éditeur â Créa qui écrit, 1398 une version qu'on remonte. On ne repose que si le texte diffère 1399 vraiment : `setValue` efface l'historique d'annulation, et le faire à 1400 chaque rendu le remettrait à zéro comme avant ce lot ; 1401 3. jeter ce qui n'est plus là â un fichier supprimé ou renommé dont le 1402 modèle survit continue de résoudre les imports des autres. */ 1403function synchroniserLesModelesDeCode(m, projet, langueDe) { 1404 if (!m || !m.editor || !projet) return null 1405 var fichiers = projet.files || [] 1406 /* ON REND UNE TABLE ADRESSE â MODÃLE, et pas un tableau au rang du fichier : 1407 `getCodeFile()` retombe sur `files[0]` quand le rang sort du tableau, et 1408 lire les modèles au même rang aurait alors donné le modèle d'un AUTRE 1409 fichier que celui affiché. L'adresse est le seul lien exact. */ 1410 var parAdresse = Object.create(null) 1411 var vivants = Object.create(null) 1412 1413 for (var i = 0; i < fichiers.length; i++) { 1414 var f = fichiers[i] || {} 1415 var chemin = String(f.path || f.name || '') 1416 var adresse = uriDuFichierCode(projet.id, chemin) 1417 if (!adresse) continue 1418 vivants[adresse] = true 1419 var contenu = String(f.content == null ? '' : f.content) 1420 var uri = m.Uri.parse(adresse) 1421 var modele = m.editor.getModel(uri) 1422 if (!modele) { 1423 modele = m.editor.createModel(contenu, langueDe ? langueDe(chemin) : undefined, uri) 1424 } else if (modele.getValue() !== contenu) { 1425 modele.setValue(contenu) 1426 } 1427 parAdresse[adresse] = modele 1428 } 1429 1430 /* LES MODÃLES QUI NE SONT PLUS à PERSONNE. Ceux de CE projet dont le fichier 1431 est parti (supprimé, renommé) ET ceux de tout autre projet â voir 1432 l'arbitrage plus haut. Les deux cas se disent d'une seule façon : n'est 1433 vivant que ce que le projet ouvert nomme à l'instant. 1434 ON NE TOUCHE QUE `file:///` : les modèles anonymes (`inmemory://`) sont 1435 ceux d'autres surfaces de l'application, et ils ne nous appartiennent pas. */ 1436 var tous = m.editor.getModels() || [] 1437 for (var k = 0; k < tous.length; k++) { 1438 var mod = tous[k] 1439 var s = mod && mod.uri ? mod.uri.toString() : '' 1440 if (s.indexOf('file:///') !== 0 || vivants[s]) continue 1441 try { mod.dispose() } catch (e) { /* déjà jeté */ } 1442 } 1443 1444 return parAdresse 1445} 1446 1447if (typeof window !== 'undefined') { 1448 window.uriDuFichierCode = uriDuFichierCode 1449 window.reglerLesLangagesDeMonaco = reglerLesLangagesDeMonaco 1450 window.synchroniserLesModelesDeCode = synchroniserLesModelesDeCode 1451}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.