KomodoEdit: il lucertolone incontra il gecko

Tutti conoscono Gecko, il render engine su cui si basano i prodotti Mozilla come Firefox e Thunderbird.
Gecko puo’ essere usato per creare nuove applicazioni multipiattaforma soprattutto se abbinato ad un altro componente realizzato da Mozilla, XulRunner.

Questi componenti sono alla base dell’editor free KomodoEdit di ActiveState e della versione open source OpenKomodo che ha come logo un simpatico lucertolone.
Il fatto che OpenKomodo si basi su queste tecnologie lo rende particolarmente adatto ad essere “esteso” sfruttando il meccanismo delle estensioni che ha reso celebre Firefox.

Di seguito vengono mostrati alcuni servizi specifici di OpenKomodo messi a disposizione del programmatore XUL quindi accessibili da Javascript ed un esempio pratico di estensione.

Accesso alle API Scintilla

Da un editor programmabile ci si aspetta che abbia delle API flessibili per la gestione del testo e con OpenKomodo non si resta delusi.
OpenKomodo per le operazioni di editing ed highlighting del codice utilizza Scintilla, un componente multipiattaforma ed open source per creare editor.

Scintilla e’ stato incapsulato come componente XPCOM rendendone banale l’utilizzo tramite Javascript.
Il codice mostrato di seguito ottiene il testo selezionato, notare come sia possibile distinguere se la selezione e’ di un blocco rettangolare oppure multiriga.

var view = ko.views.manager.currentView; // accede alla finestra che si sta editando
var scimoz = view.scintilla.scimoz;      // oggetto scintilla sci-ntilla moz-illa
var lineStart = scimoz.lineFromPosition(scimoz.selectionStart);
var lineEnd = scimoz.lineFromPosition(scimoz.selectionEnd);

var selectedText = "";
if (scimoz.selectionMode == scimoz.SC_SEL_RECTANGLE) {
    for (var i = lineStart; i <= lineEnd; i++) {
        var selectionStart = scimoz.getLineSelStartPosition(i);
        var selectionEnd = scimoz.getLineSelEndPosition(i);
        selectedText += "[" + scimoz.getTextRange(selectionStart, selectionEnd) + "]\n";
    }
} else {
    var selectionStart = scimoz.getLineSelStartPosition(lineStart);
    var selectionEnd = scimoz.getLineSelEndPosition(lineEnd);
    selectedText = scimoz.getTextRange(selectionStart, selectionEnd);
}
alert(selectedText);

Per chi ha usato Scintilla in C e’ tutto molto familiare, ad esempio SCI_GETSELECTIONMODE corrisponde in OpenKomodo a selectionMode e la constante
SC_SEL_RECTANGLE ha esattamente lo stesso significato.
Lo stesso vale per SCI_LINEFROMPOSITION e gli altri.
OpenKomodo non wrappa tutto il widget Scintilla, che contiene tantissime API, pero’ difficilmente si sente la mancanza di qualche funzionalita’.

La finestra dell’editor

Come si e’ visto l’accesso a scimoz e’ stato fatto tramite l’oggetto currentView che altro non e’ che la finestra corrente.
OpenKomodo ha diversi tipi di view ma il piu’ comune e’ ovviamente “editor” sul quale esiste anche l’oggetto scintilla.

if (view.getAttribute("type") == "startpage") {
    alert("Siamo nella pagina di riepilogo di Komodo");
} else if (view.getAttribute("type") == "editor") {
    alert("Finestra editor");
}

Le API delle view sono abbastanza ricche ad esempio e’ possibile sapere se il file e’ stato modificato dall’ultimo salvataggio con ko.views.manager.currentView.isDirty.
L’esplorazione del resto delle proprieta’ e delle funzioni e’ lasciata alla curiosita’ del lettore.

Nuove interfacce XPCOM, accesso a file locali e remoti

ActiveState e’ famosa per le sue implementazioni del linguaggio Python e OpenKomodo al suo interno ne fa largo uso anzi buona parte degli oggetti XPCOM sono scritti in questo linguaggio.

Python puo’ essere utilizzato al posto di Javascript per estendere OpenKomodo. La cosa piu’ interessante e’ che alcuni oggetti Python sono stati wrappati in XPCOM in modo da essere utilizzabili da Javascript.

Tra questi c’e’ l’interfaccia koIOsPath che di fatto e’ un wrapperone di os.path

var osPathSvc = Components.classes["@activestate.com/koOsPath;1"]
                        .getService(Components.interfaces.koIOsPath);

alert(osPathSvc.normpath("/usr/local/bin/../etc/../")); // stampa /usr/local
alert(osPathSvc.expanduser("~/trash")); // stampa /home/dave/trash

Oggetti come questo colmano le carenze di interfacce quali ad esempio nsIFile, carenza che diventa evidente quando si cercano API per l’accesso remoto ai file.

Komodo ha una buona libreria per accedere file remoti tramite SCP/FTP/SFTP; per quanto possibile i metodi di nsIFile trovano un loro
“corrispondente” in koIFileEx.
Purtroppo inspiegabilmente koIFileEx non estende nsIFile per questo si parla di “corrispondenza”.

Di seguito e’ riportato un esempio di rename di un file remoto

var file = Components
                .classes["@activestate.com/koFileEx;1"]
                .createInstance(Components.interfaces.koIFileEx);
// scp e' un protocollo riconosciuto da Komodo
file.path = "scp://feisty-server/home/notroot/st.txt";
             
if (file.isRemoteFile) {
    var rcSvc = Components
                .classes["@activestate.com/koRemoteConnectionService;1"]
                .getService(Components.interfaces.koIRemoteConnectionService);
    var conn = rcSvc.getConnectionUsingUri(file.URI);
    var parent = conn.getParentPath(file.path);
    conn.rename(file.path, parent + "/" + "new-name.txt");
}

Namespace OpenKomodo

I programmatori di OpenKomodo hanno fatto tesoro dell’amara esperienza avuta con Firefox dove il codice riutilizzabile di fatto non esisteva o almeno non era ingegnerizzato ed hanno deciso di organizzare il proprio in namespace.
Tralasciando le considerazioni sintattico-semantiche Javascript sui namespace si puo’ dire che l’operazione e’ riuscita, accedere al codice in questa maniera e’ assai meno error-prone ma anche piu’ manutenibile.
Abbiamo gia’ visto l’uso del namespace con ko.views.manager.currentView dove ko e’ il root namespace di Komodo.
Esistono diversi namespace ma la documentazione lascia molto a desiderare e spesso bisogna cercare direttamente nei sorgenti.

Di seguito vengono mostrati i namespace piu’ usati, una lista piu’ completa e aggiornata e’ disponibile qui.

Alcuni namespace di OpenKomodo
Nome Descrizione
ko.filepicker Contiene funzioni che wrappano nsIFilePicker
ko.logging Funzioni di log
ko.printing Accesso alle stampanti
ko.uriparse Funzioni di gestione di URL e path

Suggerimenti per fare debug e test

Chi ha scritto estensioni XUL sa quanto sia avvilente il ciclo di
debug e test. Vediamo cosa si puo’ fare per migliorare le cose.
Diciamo subito che il debug va sempre fatto con log e alert aspettando l’arrivo di Snapdragon.
OpenKomodo mette a disposizione una comoda API di log a livelli di criticita’, l’interfaccia koILoggingService e’
gia’ wrappata nel namespace di utilita’ ko.logging per cui basta richiamare i metodi mostrati di seguito.

var loggerName = "ko.main";
var message = "Huston abbiamo un problema";
ko.logging.getLogger(loggerName).debug(message)
ko.logging.getLogger(loggerName).info(message)
ko.logging.getLogger(loggerName).warn(message)
ko.logging.getLogger(loggerName).error(message)
ko.logging.getLogger(loggerName).critical(message)

Per i test si possono fare due cose.

La prima consiste nel testare le routine che lo permettono, ad esempio quelle che non richiedono interazione con UI, tramite le macro.
OpenKomodo permette di scrivere macro in Javascript (o Python) che accedono a tutti gli oggetti XUL definiti nell’editor.
Si crea una nuova macro e si copia il codice dell’esempio di Scintilla a questo punto e’ possibile provare e
riprovare senza dover chiudere e riaprire l’applicativo: questo dovrebbe evitare diversi esaurimenti nervosi.

La seconda possibilita’ e’ tipica delle applicazioni Gecko based e viene in aiuto quando si vogliono testare le modifiche alle UI.
E’ sufficiente disabilitare la cache degli oggetti XUL come descritto su Mozillazine agendo sul flag nglayout.debug.disable_xul_cache.
Questo ci permettera’ di fare modifiche alle UI sulle quali lavoriamo senza dover chiudere OpenKomodo.

Il suddetto flag puo’ essere impostato tramite l’editor di configurazione about:config;
su Firefox e’ sufficiente digitarne il nome sulla barra indirizzi, su Thunderbird invece si raggiunge tramite la dialog delle preferenze.
OpenKomodo non ha ne una barra indirizzi ne una opzione per arrivare a tale editor pero’ fortunatamente ha le macro che possono aprire finestre XUL.
Richiamando la macro riportatata di seguito appariranno le properties alle quali bastera’ aggiungere nglayout.debug.disable_xul_cache

ko.views.manager.loadViewFromURI('about:config','browser');

Estendere OpenKomodo

Se e’ vero che OpenKomodo ha molte delle features che ci si aspetta da un moderno IDE e’ anche vero
che alcune di esse sono un po’ incomplete.

Ad esempio l’icona sulla statusbar ci segnala la presenza di errori o warning; verde tutto bene, clessidra parsing in corso, rosso qualcosa non va.
Quando il sorgente che stiamo editando contiene degli errori o dei warning vorremmo poterne vedere la lista completa, peccato
pero’ che questa lista che fa da error console in OpenKomodo non esiste!

Se il sorgente contiene degli errori l’unica possibilita’ che si ha e’ quella di cliccare sull’icona citata, ogni clic mostra il messaggio di errore e posiziona il cursore sulla linea corrispondere.
Se ci sono dieci errori, per vedere l’ultimo bisognera’ cliccare dieci volte! Semplicemente inutilizzabile.

Per risolvere questo problema bisogna scrivere una estensione XUL che e’ quello che andremo a vedere di seguito.

Il look&feel del risultato e’ visibile in Figura 1.

Error console.

Figura 1. La finestra con la lista degli errori

Estensione XUL: Klint

Per creare la error console dobbiamo

  • fare l’overlay di alcuni elementi XUL
  • scrivere il codice che accede all’oggetto Komodo contenente la lista dei messaggi

Il nome dell’estensione e’ Klint contrazione delle parole Komodo e lint, come vedremo infatti la lista dei messaggi e’ mantenuta da un oggetto linter di OpenKomodo.

La parte di creazione XPI non viene affrontata per dare maggiore spazio agli elementi specifici di OpenKomodo.

Overlay

Il file contenente gli elementi di cui si fa l’overlay e’ komodo.xul per cui il nostro chrome.manifest conterra’

overlay	chrome://komodo/content/komodo.xul	chrome://klint/content/klintOverlay.xul

OpenKomodo visualizza i tab in un’area suddivisa in due, la prima contiene i pulsanti con i titoli come “Command Output” e la seconda con i widget specifici di quel tab (campi di testo, pulsanti, etichette, etc).
Il funzionamento di Klint prevede l’overload di entrambe le aree.

Per il tab (il titolo della error console) avremo

<tabs id="output_tabs">
    <tab id="klint_tab" label="Lint Messages" />
</tabs>

Dove output_tabs e’ il contenitore dei tab e klint_tab e’ la console.

Il contenitore dei widget e’ output_tabpanels, al suo interno si trovano il pulsante di refresh ed il tree usato per la lista dei messaggi.

<tabpanels id="output_tabpanels">
    <tabpanel id="klint_tabpanel" orient="vertical">
        <vbox flex="1">
            <hbox align="center">
                <label id="klint-count" value=""/>
                <button id="klint-refresh" label="Refresh"
                        oncommand="ko.lint.doRequest();"/>
            </hbox>
        <tree id="klint-tree" seltype="single">
            <treecols>
                <treecol id="klint-linenum"
                         label="Line number" />
                <splitter class="tree-splitter" />
                <treecol id="klint-messageType"
                         label="Type" />
                <splitter class="tree-splitter" />
                <treecol id="klint-message"
                         label="Message text" />
            </treecols>
            <treechildren id="klint-treechildren" />
        </tree>
        </vbox>
    </tabpanel>
</tabpanels>

Lato interfaccia grafica questo e’ tutto, un tab ed un tabpanel.

Osservare i cambiamenti

Il parser di OpenKomodo e’ (passatemi il termine) realtime, ovvero si ha sempre il feedback visivo sul numero di errori.

Quando il parser trova un errore viene effettuata una notifica agli observer registrati.
Il topic al quale agganciarsi e’ current_view_check_status

Components.classes["@mozilla.org/observer-service;1"]
        .getService(Components.interfaces.nsIObserverService);
        .addObserver(this, "current_view_check_status", false);

Passando da una finestra di editing all’altra vogliamo che la error console rifletta lo stato della vista corrente, questo richiede la
sottoscrizione ad un altro observer: current_view_changed

Components.classes["@mozilla.org/observer-service;1"]
        .getService(Components.interfaces.nsIObserverService);
        .addObserver(this, "current_view_changed", false);

Adesso ogni qualvolta l’utente sposta il focus oppure digitando fa cambiare il numero di errori il nostro klint verra’ avvisato.

Accedere al linter

Il tree presente nel codice XUL va riempito in base allo stato del sorgente in edit,
ci sono errori/warning oppure va tutto bene.
Sara’ il codice eseguito negli observer a riempire il tree, recuperando le informazioni dal linter.

OpenKomodo aggiunge dinamicamente la proprieta’ lintBuffer alle viste con errori,
questo oggetto contiene tutte le informazioni che ci servono (linea, colonna, tipo errore) ma il suo utilizzo non e’ esattamente intuitivo.
Un frammento di codice puo’ aiutare a capire come usarlo.

var results = view.lintBuffer.lintResults;
var resultsObj = new Object();
var numResultsObj = new Object();
results.getResults(resultsObj, numResultsObj);

var arrayErrori = resultsObj;
count = numResultsObj.value;
numErrs = results.getNumErrors();
numWarns = results.getNumWarnings();

for (var row = 0; row < count; row++) {
    var numeroLinea = arrayErrori.value[row].lineStart;
    var messaggioErrore = arrayErrori.value[row].description;
    var severity = arrayErrori.value[row].severity;
}

Va notato che results.getResults ritorna in un oggetto l’array ed in un altro la sua dimensione.

Valori ammessi per severity
Valore Descrizione
Components.interfaces.koILintResult.SEV_INFO Informazione
Components.interfaces.koILintResult.SEV_ERROR Errore
Components.interfaces.koILintResult.SEV_WARNING Warning

Una volta scritto il codice che recupera i messaggi non resta che passare l’array all’implementazione dell’interfaccia nsITreeView che riempe il widget tree.

Posizionare il cursore

L’ultima funzionalita’ mancante consiste nel posizionare il cursore sulla linea (e colonna) dell’errore facendo doppio clic su una riga dell’albero.
La posizione dell’errore si ottiene dal linter, lo spostamento del cursore si ottiene tramite Scintilla.

E’ importante sottolineare che Scintilla esprime la posizione di un carattere in byte a partire dall’inizio file quindi e’ necessario convertire
linea e colonna nel corrispondente valore, ad esempio la colonna 6 della linea 5 corrisponde al byte 1970.
Fortunatamente le API di Scintilla fanno il lavoro per noi.

moveCursorToMessage : function(view, result) {
    var line = result.lineStart - 1; // per scintilla parte da zero
    var column = result.columnStart - 1;
    var pos = view.scimoz.positionAtColumn(line, column);

    view.scimoz.ensureVisibleEnforcePolicy(line);
    view.scimoz.gotoPos(pos);
    view.scimoz.selectionStart = pos;
    view.scimoz.selectionEnd = pos;
},

Conclusioni

OpenKomodo e’ un buon editor che puo’ trovare consenso anche tra gli utenti piu’ esigenti,
le funzionalita’ viste sopra sono soltanto un piccolo sottoinsieme di quello che si puo’ fare.

Chiaramente non e’ perfetto e la versione open e’ scarsa di funzionalita’ di debug ma questo dovrebbe cambiare quando nascera’ Snapdragon, l’editor Mozilla per applicazioni Mozilla.

Estendere l’editor di ActiveState e’ facile come estendere Firefox e tramite la tecnologia Gecko siamo riusciti a colmare una lacuna senza dover sperare che lo facesse una nuova versione.
Una estensione XUL ci ha permesso di aggiungere una funzionalita’ mancante sfruttando quanto gia’ esistente, il linter.
Anche l’interfaccia grafica tramite il meccanismo degli overlay e’ stata arricchita agevolmente.

Il linter e’ uno degli oggetti peggio utilizzati in Komodo ma con una API XPCOM abbastanza completa da rendere
possibili personalizzazioni come Klint.

Chi volesse puo’ scaricare l’XPI da qui.

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.