Per evitare che il documento risulti vuoto dopo un git checkout, guardiamo i byte invece dell’orologio
Ti è mai capitato di cambiare branch e vedere che il documento aperto era diventato una schermata vuota? Mentre scattavamo delle schermate da pubblicare sul blog, abbiamo visto quella scena nella nostra app e, già che c’eravamo, abbiamo corretto anche un bug più silenzioso.

mote si fida dei file così come sono. Il contenuto sullo schermo è uguale ai byte sul disco e, al momento del salvataggio, scrive quegli stessi byte senza modificarli.
Ma cosa succede se un file viene modificato al di fuori dell’app? Se mote non se ne accorge, al salvataggio successivo potrebbe sovrascrivere ciò che ha scritto qualcun altro.
Abbiamo scoperto questo problema mentre scattavamo delle schermate. Mentre passavamo da un branch all’altro per preparare la schermata dei «byte che cambiano», a volte il documento rimaneva bloccato su una schermata vuota.
Due problemi nascosti dietro la schermata vuota
git checkout non modifica un file in un solo passaggio. Elimina il file esistente, ne crea uno nuovo e poi ne scrive il contenuto.
Il file watcher segnala questo processo attraverso diversi eventi. Ma mote leggeva il file non appena arrivava il primo evento. A volte finiva quindi per leggere un file vuoto, perché il contenuto non era ancora stato scritto.
Durante la rilettura del file veniva ricreato anche il watcher. Nel frattempo, gli eventi ancora in attesa nel watcher precedente scomparivano e la schermata rimaneva vuota.
C’era anche un problema più pericoloso.
Dopo un salvataggio, mote ignorava per 1,5 secondi gli eventi in arrivo, considerandoli «modifiche effettuate da me». Ma in quell’intervallo un formatter o un hook di salvataggio poteva modificare nuovamente il file.
In effetti, modificando il file 0,3 secondi dopo il salvataggio, mote non mostrava alcun cambiamento. Se l’utente salvava di nuovo, il contenuto modificato dall’esterno spariva senza lasciare traccia.
Un problema era visibile, l’altro no. La causa era la stessa.
Le modifiche del file venivano valutate in base al tempo, non al contenuto.
Abbiamo confrontato il contenuto invece del tempo
Ora, quando arriva un evento del file, non lo leggiamo subito. Aspettiamo 200ms e, ogni volta che arriva un nuovo evento, facciamo ripartire l’attesa.
Dopo che il processo di eliminazione, creazione e scrittura del file è terminato, leggiamo una sola volta soltanto lo stato finale. I timer obsoleti vengono invalidati tramite un numero di generazione. Non abbiamo nemmeno creato timer che continuano a essere eseguiti quando non succede nulla.
Il contenuto letto viene confrontato con gli ultimi byte su disco visti dal buffer.
Se sono uguali, significa che il salvataggio è stato effettuato dall’app oppure che è stato riscritto lo stesso contenuto. Se sono diversi, si tratta di una vera modifica esterna. Non importa che siano trascorsi 0,1 secondi o 1 secondo dal salvataggio.
Quando rileggiamo lo stesso file, manteniamo anche lo stesso watcher. In questo modo gli eventi non scompaiono durante la sostituzione del watcher.
Chiediamo solo durante la modifica
Dopo aver rilevato una modifica esterna, controlliamo lo stato dell’utente.
Se sono presenti modifiche non salvate, mostriamo un banner. L’utente può scegliere se ricaricare il file modificato o continuare a lavorare sul contenuto attuale. Entrambe le opzioni possono comportare la perdita di contenuti, quindi l’app non decide al posto suo.
Se invece il documento non è in fase di modifica, carichiamo subito il nuovo contenuto. Non mostriamo il banner e manteniamo inalterate la posizione del cursore e quella di scorrimento. Quando si passa da un branch all’altro mentre si legge un documento, è più naturale conservare il punto in cui si era arrivati che mostrare una notifica.

Anche quando il file scompare, non prendiamo subito una decisione. Poiché un altro editor potrebbe essere nel mezzo della rinomina del file, controlliamo di nuovo dopo 300ms.
Se anche allora il file non esiste, avvisiamo l’utente. Il contenuto su cui stava lavorando rimane nel buffer e, salvandolo di nuovo, il file viene ricreato.
Verifichiamo sei casi nell’ambiente reale
È difficile verificare questo problema usando soltanto gli unit test. Questo perché si verifica quando il file watcher, il disco reale e il gestore delle finestre operano insieme.
Per questo abbiamo creato dei controlli da eseguire nell’ambiente reale.
Verifichiamo che il buffer continui a seguire il file anche dopo aver cambiato branch dieci volte e che una modifica apportata al file 300ms dopo il salvataggio venga comunque recepita. Controlliamo inoltre che non compaia alcuna notifica quando è l’app stessa a salvare e che, se avviene una modifica esterna durante l’editing, venga mostrato il banner senza perdere il contenuto scritto.
In tutto sono sei. Li eseguiamo a ogni rilascio.

tools/e2e/external.sh su un display reale. Tutte e sei le righe riportano ok e l’ultima riga è PASS=6 FAIL=0.Trattare il file come fonte di verità non significa soltanto scrivere esattamente il contenuto salvato.
Significa anche non lasciarsi sfuggire le modifiche apportate al file a nostra insaputa.