Perché abbiamo costruito shell specifiche per ogni piattaforma attorno a un unico core Rust
Per offrire lo stesso editor su tre sistemi operativi, esiste una strada che permette di scrivere l’interfaccia utente una sola volta. Noi l’abbiamo percorsa, poi siamo tornati indietro e abbiamo sovrapposto a un unico core un insieme di shell sottili, una per ogni piattaforma.

Per offrire lo stesso editor su Linux, macOS e Windows bisogna creare l’interfaccia utente tre volte.
All’inizio volevamo evitare quel costo. Abbiamo disegnato la stessa schermata sui tre sistemi operativi usando un unico toolkit multipiattaforma. Grazie a quella decisione abbiamo superato il primo gate prestazionale e anche la prima milestone per l’input in coreano.
Poi le priorità sono cambiate.
Le prestazioni sono più importanti del design, e il design è più importante dei costi. Tempo e costi sono esclusi dai vincoli.
Riconsiderando tutto secondo questi criteri, anche la risposta è cambiata.
Abbiamo rinunciato a un’unica UI
Un toolkit multipiattaforma offre vantaggi evidenti. Una funzionalità creata una volta può essere usata su più sistemi operativi.
Ma c’erano anche dei costi da sostenere. Utilizzava 40–60 MB di memoria in più e l’avvio era più lento di 150–200 ms. La resa dei caratteri era inferiore a quella del motore di testo nativo del sistema operativo e non era possibile sfruttare pienamente nemmeno gli effetti visivi propri del sistema.
Era una scelta ragionevole quando i costi di sviluppo erano importanti. Ma, una volta esclusi i costi dai vincoli, non c’era più motivo di accettare questi compromessi.
Abbiamo quindi deciso di realizzare in Rust il core che elabora i documenti e di creare separatamente, per ogni sistema operativo, la shell responsabile dell’interfaccia.
Non perché la tecnologia precedente fosse scadente. Quando cambiano le priorità, le stesse opzioni possono portare a una risposta diversa. Abbiamo deciso di sostenere il costo di creare le funzionalità tre volte e di recuperare prestazioni, qualità dei caratteri e un’esperienza coerente con il sistema.
Tuttavia, non abbiamo abbandonato tutto.
Abbiamo mantenuto intatto il nostro insieme di regole per le misurazioni. Tenere il puntatore fuori dalla finestra, individuare con precisione la finestra tramite il suo nome, contare i frame in modo sincrono. Abbiamo trasferito anche i principi di progettazione dell’input coreano. Mostriamo al sistema operativo soltanto la riga in cui si trova il cursore e non applichiamo stili ai caratteri durante la composizione. Se lo stato perde la sincronizzazione, non facciamo supposizioni: lo riallineiamo.
Il codice può essere abbandonato, ma i principi convalidati restano.
Abbiamo limitato il ruolo della shell
Se le shell specifiche delle piattaforme cominciano ad assumersi troppi compiti, i tre prodotti diventano presto diversi. Per questo abbiamo stabilito chiaramente ciò che la shell deve e non deve fare.
La shell crea la finestra e disegna i caratteri. Trasmette al core gli input da tastiera e puntatore e converte l’input coreano in comandi di modifica. Applica anche gli effetti visivi del sistema operativo.
Non interpreta invece il Markdown. A parte le interruzioni di riga, non decide nemmeno il layout, non salva i file e non valuta le licenze.
Non ci siamo limitati a mettere per iscritto questo principio. Se in una shell viene introdotto del codice per interpretare il Markdown, il controllo automatico interrompe la build.
Se si comincia a elaborare la sintassi in una sola shell, i risultati possono diventare leggermente diversi su ogni sistema operativo. Problemi del genere sono anche difficili da individuare. Una tabella può apparire diversa soltanto in una shell, mentre la vera causa potrebbe non trovarsi affatto nel codice che disegna la tabella.
Abbiamo fatto in modo che soltanto il core durasse nel tempo
Il core non dipende da alcuna tecnologia per l’interfaccia utente. Può essere testato autonomamente nel terminale anche senza una finestra.
Il core si occupa della modifica dei documenti e dell’interpretazione del Markdown, dell’annullamento, della ricerca, della struttura, del salvataggio dei file, dell’esportazione e della verifica delle licenze.

Quando l’utente preme un tasto, la shell trasmette l’input al core. Il core modifica il documento e reinterpreta soltanto l’area attorno alla parte cambiata. Dopo aver calcolato esclusivamente il contenuto visibile sullo schermo, restituisce alla shell i risultati che sono cambiati. La shell ridisegna soltanto le righe interessate e completa un frame.
Se durante questo processo si rilegge l’intero documento, non si tratta semplicemente di un problema di prestazioni. Significa aver violato il budget stabilito all’inizio.
Anche la correttezza viene verificata una sola volta, nel core. Devono essere superati tutti i 652 esempi ufficiali di CommonMark e, se il numero di esempi superati diminuisce, la build fallisce. Eseguiamo anche test di fuzzing che continuano a fornire input imprevisti.
Controlliamo inoltre che il file salvato sia identico all’originale byte per byte. Se il rapporto di round trip è inferiore a 1,0, non significa che il sistema è lento: significa che abbiamo perso parte del file.
Anche se la shell cambia, il core rimane invariato. Su Linux e Windows colleghiamo direttamente il core, mentre su macOS utilizziamo codice di collegamento in Swift. Stiamo realizzando la shell per macOS con AppKit e TextKit 2 e quella per Windows con DirectWrite.
L’obiettivo è mantenere identico il documento e rendere l’esperienza d’uso propria di ciascun sistema operativo.
Il documento è uguale, l’esperienza d’uso è diversa
Una volta definiti i confini, è diventato chiaro anche che cosa dovesse essere uniformato.
Ciò che deve essere uniforme è il documento. Indipendentemente dal sistema operativo su cui viene salvato, il contenuto del file deve essere identico a livello di byte. Non devono cambiare nemmeno l’allineamento delle tabelle, le terminazioni di riga e l’interruzione di riga finale.
La correttezza del Markdown è già verificata nel core. Non occorre dimostrarla di nuovo ogni volta che si crea una nuova shell. Sulla nuova piattaforma bisogna verificare l’esperienza d’uso e le prestazioni.
Al contrario, non uniformiamo l’esperienza percepita direttamente dall’utente.
I caratteri vengono disegnati con il motore di testo di ciascun sistema operativo. Anche per l’input coreano usiamo direttamente il metodo del sistema operativo. Per gli effetti di trasparenza utilizziamo le funzionalità di sistema su macOS e Windows, mentre su Linux, dove non esiste la stessa funzionalità, disegniamo direttamente soltanto le parti necessarie.
Non forziamo nemmeno l’uniformità della posizione dei pulsanti della finestra, della struttura dei menu o dei tasti modificatori delle scorciatoie. Mostrare gli stessi pixel su sistemi operativi diversi non è il nostro obiettivo. È più importante che l’esperienza risulti naturale su ciascun sistema operativo.
Abbiamo definito anche le condizioni di rilascio separatamente per ogni sistema operativo. Il rilascio è possibile soltanto dopo aver superato i criteri prestazionali e aver completato test reali con il metodo di input coreano predefinito.
La riga relativa a una piattaforma viene aggiunta anche alla tabella dei benchmark soltanto dopo che è stata effettivamente prodotta una build. Se si inserisce in anticipo una riga non ancora misurata, diventa difficile fidarsi anche delle altre righe della tabella.
Questa scelta ha un costo evidente
Dobbiamo creare la stessa funzionalità tre volte. Dobbiamo implementare tre volte anche l’input coreano e gestire tre ambienti di build.
Sulla macchina che utilizziamo attualmente non possiamo compilare né eseguire il debug per Windows e macOS. Servono hardware separato e ambienti di automazione dedicati. Per questo non indichiamo date per queste due piattaforme.
Abbiamo stabilito anche le condizioni per riesaminare questa decisione. Il presupposto della decisione è che i costi siano esclusi dai vincoli. Se non riusciamo a procurarci l’hardware necessario, rinviamo soltanto il programma della piattaforma interessata. Il rilascio per Linux procede senza cambiamenti.
Una decisione richiede non soltanto una motivazione, ma anche le condizioni in cui deve essere riconsiderata. Così, quando la situazione cambia, non è necessario ricominciare la discussione da capo.
Ciò che deve durare nel tempo è il core. Le shell devono poter essere abbandonate in qualsiasi momento.
Abbiamo affidato alla piattaforma soltanto ciò che può essere abbandonato.