In questo articolo parleremo di come sviluppare un’estensione per Visual Studio Code. Nello specifico, l’estensione presa come esempio serve per programmare in RPG.

L’articolo è strutturato in tre parti, nella prima verrà spiegato come iniziare a sviluppare un’estensione partendo da zero, nella seconda alcune funzionalità base che si possono aggiungere, mentre nella terza si parla di un diverso tipo di estensione che si basa sul language server protocol.

Per Iniziare 

  • Scaricare l’ultima versione di node.js 
  • Aggiornare npm con lo script: npm install -g npm
  • Scaricare git
  • Installare yeoman dal cmd tramite lo script: npm install -g yo
  • Installare il generatore di estensione: npm install -g yo generator-code

Una volta installati tutti i programmi necessari utilizzare lo script: yo code.

Seguire i passaggi guidati per creare la prima estensione, scegliendo tra le varie opzioni in base alla estensione che si vuole creare (Linguaggio typescript o javascript, npm o yarn etc.)

Dopo aver seguito la creazione guidata sul prompt dei comandi uscirà il messaggio:

“Your extension “nome dell’estensione scelto” has been created!

To start editing with Visual Studio Code, use the following commands:”,

Bisognerà quindi inserire: cd “nome dell’estensione”  e  code .

Successivamente vscode dovrebbe aprirsi automaticamente con la nuova estensione già caricata.

Come muoversi all’interno di Visual Studio Code

  • Se non sai cosa siano le estensioni di visual Studio Code clicca qui.
  • Appena avviata l’applicazione di Visual Studio Code aprire il terminal (ctrl+ò) e scrivere npm run watch Questo serve a far compilare l’estensione.
  • Per eseguire l’estensione e testarla usare f5.
  • Per aprire la Command Palette e testare i comandi: ctrl+Shift+P e nome del comando
  • Ogni volta che si modifica il sorgente dell’estensione, se quest’ultima è eseguita si può usare ctrl+r(può andare in conflitto con altre estensioni) per fare un reload. 
  • Un tutorial che ho trovato utile per iniziare:https://www.youtube.com/watch?v=a5DX5pQ9p5M, anche se la maggior parte delle cose sono riportate in questo articolo.

Come cercare informazioni

Informazioni:

La maggior parte delle informazioni necessarie alla creazione di un’estensione sono nel sito ufficiale di micrsoft

Per quanto riguarda domande fatte da altri utenti oltre a stackOverflow si possono consultare i post e ticket fatti su gitHub.

Esempi:

Su GitHub vi è una lista di esempi fornita da microsoft : https://github.com/microsoft/vscode-extension-samples

Inoltre se si vede una feature su un’altra estensione da implementare sulla propria, consultando il sito di quest’estensione è possibile che vi sia la repository con il source code caricato su gihub.

Come è costituito il folder di un’estensione

I principali file all’interno del folder su cui bisogna lavorare sono il package.json e quelli presenti in src (backend in typescript)

package.json:

Nel package.json si trova la struttura dell’estensione.

Le più importanti tra queste sono  activation events e contributes.

In activation events si specificano i possibili eventi che “scatenano” un determinato comando.

In contributes vi sono tutti gli oggetti dell’estensione, come comandi, menu, view etc. 

Src e extension.ts:

Il file predefinito come main è extension.ts, ma se ne può scegliere un altro specificandolo nel package.json.

Inizialmente in extension.ts sono presenti le funzioni 

“ export function activate(context: vscode.ExtensionContext) {}” , in cui si specificano tutte le funzioni che si vogliono eseguire quando la funzioni viene attivata e viene utilizzata,

 e “export function deactivate() {}”, nel quale si inseriscono le funzioni da attivare alla chiusura della estensione.

La logica delle applicazioni può essere scritta in Javascript o Typescript. Oltre a questi linguaggi però vanno utilizzate delle api per le estensioni di vscode. 

Oltre alle API è possibile che servano altre funzionalità scaricabili da npm nel folder node_modules dell’estensione 

 tramite il comando npm i ‘nome-della-funzione’

Successivamente per importarlo nel file .ts scrivere import + as nomenelfile from “nomefunzione”;

I Comandi

Le principali funzionalità delle estensioni si attivano tramite comandi. Questi ultimi successivamente potranno essere richiamati dal command palette (ctrl+Shift+p / cmd+Shift+p) o da un menu.

I comandi vanno inseriti nella funzione activate{} (vedi sopra) , in quanto si usano quando la funzione è attiva. Per creare un comando la sintassi è:

context.subscriptions.push(    vscode.commands.registerCommand("Command-name" => {}),

Una volta creato il comando le file typescript bisogna riportarlo nel package.json per visualizzare e richiamarlo, quindi all’interno di “Contributes”{} andrà inserito:

"commands": [
      {
        "command": "test5.SendRPG",
        "category": "Jariko",
        "title": "Execute RPG "
      },
]
  • La clausola “command” rappresenta l’id del comando, deve essere la stessa dichiarata nel file typescript.
  • La clausola  “category” identifica il nome del gruppo da visualizzare prima del comando, può essere utilizzata per più comandi riguardanti lo stesso ambito.
  • La clausola “title” identifica il nome che rappresenta il comando quando questo viene visualizzato nella command palette o nel menu.

I Menu

Per far richiamare più facilmente un comando all’utente si può inserire questo comando in un menu. L’utente può richiamare i menu schiacciando tasto destro sul codice.

Per creare l’oggetto menu bisogna inserire all’interno del package.json Contributes{}:

"menus": {
      "editor/context": [
        {
          "when": "editorLangId==rpgle",
          "command": "test5.SendRPG",
          "group": "navigation"
        },
  • La clausola “when” identifica quando un comando viene mostrato nel menu.
    Un esempio può essere “editorLangId==rpgle”, in cui il comando viene mostrato solo se si è in un file con estensione rpgle.
  • La clausola  “command” identifica il comando che si vuole richiamare. Utilizzare il formato “Nome_File.Nome_Comando”.
  • La clausola “group” identifica il gruppo di menu in cui si vuole inserire il nostro comando. I gruppi sono predefiniti.

Pubblicare l’Estensione

Una volta terminata l’estensione questa potrà essere caricata sul marketplace di VSC.

Per prima cosa bisogna preparare l’estensione per essere caricata. I passaggi fondamentali da controllare prima di caricarla sono :

  • -Gli errori durante la compilazione, se sono presenti non si può caricare l’estensione.
  • -Il publisher ID specificato nel package.json.
  • -Il ReadME presente nel folder dell’estensione. Quest’ultimo va modificato sia per poter caricare l’estensione, sia perché è la spiegazione dell’estensione che l’utente vedrà prima di scaricarla.

Dopo aver controllato questi passaggi bisognerà trasformare il folder dell’estensione in un unico file di tipo vsix. Per farlo procedere scaricando tramite npm e cdm npm install -g vsce. Successivamente per comprimere l’estensione usare cd nel pannello di controllo per aprire il folder dell’estensione, e lanciare il comando vsce package. Se si hanno seguito tutti i passaggi bisognerebbe trovare il file vsix nella cartella specificata con “cd”.

Una volta compressa in un solo file l’estensione, per pubblicala come prima cosa creare un account Azure Devops come organizzazione.

Successivamente accedere da https://marketplace.visualstudio.com/manage/publishers/francescomazzoleni con l’account appena creato.

Da qui selezionare create publisher e completare i form inerenti alla pubblicazione. Il publisher ID deve essere lo stesso specificato nel package.json dell’estensione.

Infine dopo aver un nuovo publisher clickare su nuova estensione e caricare l’estensione in formato vsix.

L’estensione potrà essere caricata pubblica, disponibile a tutti gli utenti di VSCode, o privata, condividendone l’accesso attraverso dei token. Successivamente inoltre la si potrà aggiornare, caricando con lo stesso procedimento la stessa estensione con un nuovo valore della versione.

Language Server Protocol (LSP)

Il Language Server è un server che contiene la logica per l’utlizzo di un linguaggio di programmazione. Per facilitarne l’utilizzo e imporre uno standard per la comunicazione con il client è stato creato il Language Server Protocol.

Quest’ultimo funziona attraverso l’invio di payload JSON tra il client e il server, richiamando direttamente determinati eventi lato server quando l’utente modifica o apre un documento.

Per quanto riguarda le VSC Extensions il protocollo si può utilizzare per creare una Language Server Extension, ovvero un’estensione per scrivere del codice in un determinato linguaggio, implementando feature come l’autocompletamento o il controllo sintattico.

Nello specifico nell’estensione su cui ho lavorato ho modificato la classe RPGTextDocumentHandler, implementando gli eventi OnCompletion, OnCompletionResolve per l’autocompletamento, e OnTextDocumentDidOpen, OnTextDocumentDidChange per il controllo sintattico.

Autocompletamento:

Il primo evento viene richiamato quando si preme ctrl mentre si scrive per visualizzare i suggerimenti per l’autocompletamento. I suggerimenti da inviare al client li seleziono in base alla posizione in cui si trova l’utente nel testo quando richiama l’evento, visto che in RPG si possono scrivere determinati comandi solo in alcune colonne. Una volta stabilita la colonna richiamo un’altra classe contente delle liste di oggetti per l’autocompletamento che successivamente invio al client per mostrarle all’utente.

Codice:

public CompletionList onCompletion(CompletionParams completionParams) throws HandlingRequestError {
           	MyCompletionItemProvider myitemprovider = new MyCompletionItemProvider();
    	CompletionList completionList = new CompletionList();
    	List<CompletionItem> items = null;
    	int pos = completionParams.getPosition().getCharacter();
    	switch (pos) {
    	case 0:
    	case 5:
    	   	items = myitemprovider.getSpecifications().stream().map(myCompletionItem -> {
    	                 	CompletionItem item = new CompletionItem();
    	                 	item.setLabel(myCompletionItem.getLabel());
    	                 	item.setData(myCompletionItem.getId());
    	                 	item.setDetail(myCompletionItem.getDetail());
    	                 	return item;
    	          	}).collect(Collectors.toList());
    	   	break;
    	default:
    	   	if (pos >= 25 && pos <= 34) {
    	          	items = myitemprovider.getOperationCodes().stream().map(myCompletionItem -> {
    	                 	CompletionItem item = new CompletionItem();
    	                 	item.setLabel(myCompletionItem.getLabel());
    	                 	item.setData(myCompletionItem.getId());
    	                 	item.setDetail(myCompletionItem.getDetail());
    	                 	return item;
    	          	}).collect(Collectors.toList());
    	   	}
    	}
    	
    	
    	completionList.setItems(items);
    	return completionList;
	}

Il secondo metodo viene richiamato quando l’utente clicca su una freccia a destra del suggerimento di autocompletamento per verificarne la documentazione. In questo caso ottengo l’id dell’oggetto come parametro dell’evento, e faccio una ricerca per id per trovare la documentazione corrispondente.

Codice:

public CompletionItem onCompletionResolve(CompletionItem item) throws HandlingRequestError {
           	MyCompletionItemProvider myitemprovider = new MyCompletionItemProvider();
           	MyCompletionItem myitem=myitemprovider.getById(item.getData().toString());
           	
           	item.setDocumentation(myitem.getDocumentation());
           	item.setDetail(myitem.getDetail());
           	return item;
   	}

Controllo Sintattico:

I due eventi vengono richiamati rispettivamente quando il documento viene aperto e quando viene modificato il testo. Una volta chiamato l’evento entrambi richiamano l’evento ValidateDocument, passando come parametro l’intero testo su cui sta lavorando l’utente.

All’intero di ValidateDocuments divido il testo in parole memorizzandone la posizione. Successivamente controllo le colonne che possono contenere i comandi RPG, controllando che l’utente abbia inserito dei comandi tra quelli che ho memorizzato per l’autocompletamento(Uso le stesse liste per comodità, non è necessario). In caso contrario invio gli errori al server specificandone posizione, severity e un eventuale messagio.

Codice:

for (Word word : WORDS) {
                	switch (word.getCol()) {
                	case 5:
                      	if (!myitemprovider.getSpecifications().stream().filter(a -> a.getLabel().equals(word.getText()) ).findFirst().isPresent()) {
                             	Diagnostic diagnostic = new Diagnostic();
                             	Range range= new Range();
                             	range.setStart(new Position(word.getLine(), word.getCol()));
                             	range.setEnd(new Position(word.getLine(), word.getCol()+word.getText().length()));
                             	diagnostic.setRange(range);
                             	diagnostic.setSeverity(1);
                             	diagnostic.setMessage("a");
                             	diagnostics.add(diagnostic);
                      	}
                      	break;
                	case 25:
                      	if (!myitemprovider.getOperationCodes().stream().filter(a -> a.getLabel().equals(word.getText()) ).findFirst().isPresent()) {
                             	Diagnostic diagnostic = new Diagnostic();
                             	Range range= new Range();
                             	range.setStart(new Position(word.getLine(), word.getCol()));
                             	range.setEnd(new Position(word.getLine(), word.getCol()+word.getText().length()));
                             	diagnostic.setRange(range);
                             	diagnostic.setSeverity(1);
                             	diagnostic.setMessage("a");
                             	diagnostics.add(diagnostic);
                      	}
         	         	break;
                	}  	
         	if (!diagnostics.isEmpty()) {
         	PublishDiagnosticsParams publishDiagnosticsParams1 = new PublishDiagnosticsParams();
    	publishDiagnosticsParams1.setUri(uri);
    	publishDiagnosticsParams1.setDiagnostics(diagnostics);
    	context.publishDiagnosticParams(publishDiagnosticsParams1);
         	}
         	}
	}