Sto lavorando a una struttura di completamento (intellisense) per C # in emacs.
L'idea è che, se un utente digita un frammento, quindi richiede il completamento tramite una particolare combinazione di tasti, la funzione di completamento utilizzerà la riflessione .NET per determinare i possibili completamenti.
Per fare questo è necessario conoscere il tipo di cosa da completare. Se è una stringa, esiste un insieme noto di possibili metodi e proprietà; se è un Int32, ha un set separato e così via.
Usando semantic, un pacchetto lexer / parser di codice disponibile in emacs, posso individuare le dichiarazioni di variabili e i loro tipi. Detto questo, è semplice utilizzare la reflection per ottenere i metodi e le proprietà sul tipo, quindi presentare l'elenco di opzioni all'utente. (Ok, non è abbastanza semplice da fare all'interno di emacs, ma usando la possibilità di eseguire un processo powershell all'interno di emacs , diventa molto più semplice. Scrivo un assembly .NET personalizzato per fare la riflessione, lo carico in PowerShell e poi elisp viene eseguito all'interno emacs può inviare comandi a powershell e leggere le risposte, tramite comint. Di conseguenza emacs può ottenere rapidamente i risultati della riflessione.)
Il problema arriva quando il codice utilizza var
nella dichiarazione della cosa da completare. Ciò significa che il tipo non è specificato esplicitamente e il completamento non funzionerà.
Come posso determinare in modo affidabile il tipo effettivo utilizzato, quando la variabile viene dichiarata con l'estensione var
parola chiave? Giusto per essere chiari, non ho bisogno di determinarlo in fase di esecuzione. Voglio determinarlo in "Design time".
Finora ho queste idee:
- compilare e invocare:
- estrae la dichiarazione di dichiarazione, ad esempio `var foo =" a string value ";`
- concatenare un'istruzione `foo.GetType ();`
- compilare dinamicamente il frammento C # risultante in un nuovo assembly
- caricare l'assembly in un nuovo AppDomain, eseguire il framgment e ottenere il tipo restituito.
- scaricare e scartare l'assieme
So come fare tutto questo. Ma suona terribilmente pesante, per ogni richiesta di completamento nell'editor.
Suppongo di non aver bisogno ogni volta di un nuovo AppDomain. Potrei riutilizzare un singolo AppDomain per più assemblaggi temporanei e ammortizzare il costo di installazione e demolizione, tra più richieste di completamento. È più un ritocco dell'idea di base.
- compilare e ispezionare IL
Compilare semplicemente la dichiarazione in un modulo, quindi ispezionare l'IL per determinare il tipo effettivo dedotto dal compilatore. Come sarebbe possibile? Cosa userei per esaminare l'IL?
Qualche idea migliore là fuori? Commenti? suggerimenti?
MODIFICA - pensando a questo ulteriormente, la compilazione e l'invocazione non è accettabile, perché l'invocazione potrebbe avere effetti collaterali. Quindi la prima opzione deve essere esclusa.
Inoltre, penso di non poter presumere la presenza di .NET 4.0.
AGGIORNAMENTO - La risposta corretta, non menzionata sopra, ma delicatamente sottolineata da Eric Lippert, è quella di implementare un sistema di inferenza di tipo full fidelity. È l'unico modo per determinare in modo affidabile il tipo di una variabile in fase di progettazione. Ma non è nemmeno facile da fare. Poiché non mi illudo di voler tentare di costruire una cosa del genere, ho scelto la scorciatoia dell'opzione 2: estrarre il codice di dichiarazione pertinente e compilarlo, quindi ispezionare l'IL risultante.
Questo funziona effettivamente, per un discreto sottoinsieme degli scenari di completamento.
Ad esempio, si supponga nei seguenti frammenti di codice, il? è la posizione in cui l'utente chiede il completamento. Funziona:
var x = "hello there";
x.?
Il completamento si rende conto che x è una stringa e fornisce le opzioni appropriate. Lo fa generando e quindi compilando il seguente codice sorgente:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... e poi ispezionare l'IL con una semplice riflessione.
Funziona anche:
var x = new XmlDocument();
x.?
Il motore aggiunge le clausole using appropriate al codice sorgente generato, in modo che venga compilato correttamente, quindi l'ispezione IL è la stessa.
Funziona anche questo:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Significa solo che l'ispezione IL deve trovare il tipo della terza variabile locale, invece della prima.
E questo:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... che è solo un livello più profondo dell'esempio precedente.
Ma ciò che non funziona è il completamento di qualsiasi variabile locale la cui inizializzazione dipende in qualsiasi momento da un membro dell'istanza o dall'argomento del metodo locale. Piace:
var foo = this.InstanceMethod();
foo.?
Né la sintassi LINQ.
Dovrò pensare a quanto siano preziose queste cose prima di considerare di affrontarle tramite quello che è sicuramente un "progetto limitato" (parola gentile per hack) per il completamento.
Un approccio per affrontare il problema con le dipendenze dagli argomenti del metodo o dai metodi di istanza potrebbe essere quello di sostituire, nel frammento di codice che viene generato, compilato e quindi analizzato IL, i riferimenti a queste cose con variabili locali "sintetiche" dello stesso tipo.
Un altro aggiornamento : il completamento delle variabili che dipendono dai membri dell'istanza ora funziona.
Quello che ho fatto è stato interrogare il tipo (tramite semantica) e quindi generare membri sostitutivi sintetici per tutti i membri esistenti. Per un buffer C # come questo:
public class CsharpCompletion
{
private static int PrivateStaticField1 = 17;
string InstanceMethod1(int index)
{
...lots of code here...
return result;
}
public void Run(int count)
{
var foo = "this is a string";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var mmm = count + z + CsharpCompletion.PrivateStaticField1;
var nnn = this.InstanceMethod1(mmm);
var fff = nnn.?
...more code here...
... il codice generato che viene compilato, in modo che io possa apprendere dall'output IL il tipo di var locale nnn, assomiglia a questo:
namespace Nsbwhi0rdami {
class CsharpCompletion {
private static int PrivateStaticField1 = default(int);
string InstanceMethod1(int index) { return default(string); }
void M0zpstti30f4 (int count) {
var foo = "this is a string";
var fred = new System.Collections.Generic.List<String> { foo, foo.Length.ToString() };
var z = fred.Count;
var mmm = count + z + CsharpCompletion.PrivateStaticField1;
var nnn = this.InstanceMethod1(mmm);
}
}
}
Tutti i membri dell'istanza e del tipo statico sono disponibili nel codice scheletro. Si compila con successo. A quel punto, determinare il tipo di var locale è semplice tramite Reflection.
Ciò che lo rende possibile è:
- la possibilità di eseguire PowerShell in emacs
- il compilatore C # è davvero veloce. Sulla mia macchina, ci vogliono circa 0,5 secondi per compilare un assembly in memoria. Non abbastanza veloce per l'analisi tra le sequenze di tasti, ma abbastanza veloce da supportare la generazione su richiesta di elenchi di completamento.
Non ho ancora esaminato LINQ.
Questo sarà un problema molto più grande perché il lexer / parser semantico che emacs ha per C #, non "fa" LINQ.