Je travaille sur une installation de complétion (intellisense) pour C # dans emacs.
L'idée est que, si un utilisateur tape un fragment, puis demande la complétion via une combinaison de touches particulière, la fonction de complétion utilisera la réflexion .NET pour déterminer les complétions possibles.
Pour ce faire, il faut que le type de chose en cours de réalisation soit connu. S'il s'agit d'une chaîne, il existe un ensemble connu de méthodes et de propriétés possibles; s'il s'agit d'un Int32, il a un ensemble séparé, et ainsi de suite.
En utilisant sémantique, un package lexer / parser de code disponible dans emacs, je peux localiser les déclarations de variables et leurs types. Compte tenu de cela, il est simple d'utiliser la réflexion pour obtenir les méthodes et les propriétés du type, puis de présenter la liste des options à l'utilisateur. (Ok, ce n'est pas tout à fait simple à faire dans emacs, mais en utilisant la possibilité d'exécuter un processus PowerShell dans emacs , cela devient beaucoup plus facile. J'écris un assembly .NET personnalisé pour faire la réflexion, le charge dans le PowerShell, puis elisp s'exécutant dans emacs peut envoyer des commandes à PowerShell et lire les réponses, via comint. En conséquence, emacs peut obtenir rapidement les résultats de la réflexion.)
Le problème survient lorsque le code utilise var
dans la déclaration de la chose en cours de réalisation. Cela signifie que le type n'est pas spécifié explicitement et que la complétion ne fonctionnera pas.
Comment puis-je déterminer de manière fiable le type réel utilisé, lorsque la variable est déclarée avec le var
mot-clé? Pour être clair, je n'ai pas besoin de le déterminer au moment de l'exécution. Je veux le déterminer au "Design time".
Jusqu'à présent, j'ai ces idées:
- compilez et invoquez:
- extraire l'instruction de déclaration, par exemple `var foo =" une valeur de chaîne ";`
- concaténer une instruction `foo.GetType ();`
- compilez dynamiquement le fragment C # résultant dans un nouvel assembly
- chargez l'assembly dans un nouvel AppDomain, exécutez le framgment et obtenez le type de retour.
- décharger et jeter l'ensemble
Je sais comment faire tout ça. Mais cela semble terriblement lourd, pour chaque demande d'achèvement dans l'éditeur.
Je suppose que je n'ai pas besoin d'un nouvel AppDomain à chaque fois. Je pourrais réutiliser un seul AppDomain pour plusieurs assemblages temporaires et amortir le coût de sa configuration et de sa suppression, sur plusieurs demandes d'achèvement. C'est plus une modification de l'idée de base.
- compiler et inspecter IL
Compilez simplement la déclaration dans un module, puis inspectez l'IL pour déterminer le type réel qui a été déduit par le compilateur. Comment cela serait-il possible? Qu'est-ce que j'utiliserais pour examiner l'IL?
Y a-t-il de meilleures idées là-bas? Commentaires? suggestions?
EDIT - en y réfléchissant davantage, la compilation et l'invocation n'est pas acceptable, car l'invocation peut avoir des effets secondaires. La première option doit donc être écartée.
De plus, je pense que je ne peux pas supposer la présence de .NET 4.0.
MISE À JOUR - La bonne réponse, non mentionnée ci-dessus, mais gentiment soulignée par Eric Lippert, est de mettre en œuvre un système d'inférence de type de fidélité complète. C'est le seul moyen de déterminer de manière fiable le type d'une variable au moment de la conception. Mais ce n'est pas non plus facile à faire. Parce que je ne me fais aucune illusion sur le fait que je veux essayer de construire une telle chose, j'ai pris le raccourci de l'option 2 - extraire le code de déclaration pertinent, le compiler, puis inspecter l'IL résultant.
Cela fonctionne réellement, pour un sous-ensemble équitable des scénarios d'achèvement.
Par exemple, supposons que dans les fragments de code suivants, le? est la position à laquelle l'utilisateur demande la complétion. Cela marche:
var x = "hello there";
x.?
La complétion se rend compte que x est une chaîne et fournit les options appropriées. Pour ce faire, il génère puis compile le code source suivant:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... puis en inspectant l'IL avec une simple réflexion.
Cela fonctionne également:
var x = new XmlDocument();
x.?
Le moteur ajoute les clauses using appropriées au code source généré, de sorte qu'il se compile correctement, puis l'inspection IL est la même.
Cela fonctionne aussi:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Cela signifie simplement que l'inspection IL doit trouver le type de la troisième variable locale, au lieu de la première.
Et ça:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... qui est juste un niveau plus profond que l'exemple précédent.
Mais ce qui ne fonctionne pas , c'est la complétion sur toute variable locale dont l'initialisation dépend à tout moment d'un membre d'instance ou d'un argument de méthode locale. Comme:
var foo = this.InstanceMethod();
foo.?
Ni la syntaxe LINQ.
Je vais devoir réfléchir à la valeur de ces choses avant d'envisager de les aborder via ce qui est définitivement une "conception limitée" (mot poli pour hack) pour l'achèvement.
Une approche pour résoudre le problème des dépendances sur les arguments de méthode ou les méthodes d'instance serait de remplacer, dans le fragment de code qui est généré, compilé puis analysé par IL, les références à ces choses par des variables locales "synthétiques" du même type.
Une autre mise à jour - la complétion sur les variables qui dépendent des membres de l'instance, fonctionne maintenant.
Ce que j'ai fait, c'est interroger le type (via sémantique), puis générer des membres suppléants synthétiques pour tous les membres existants. Pour un tampon C # comme celui-ci:
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...
... le code généré qui est compilé, afin que je puisse apprendre à partir de la sortie IL le type de la var locale nnn, ressemble à ceci:
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);
}
}
}
Tous les membres de type instance et statique sont disponibles dans le code squelette. Il se compile avec succès. À ce stade, la détermination du type de var local est simple via Reflection.
Ce qui rend cela possible est:
- la possibilité d'exécuter PowerShell dans emacs
- le compilateur C # est vraiment rapide. Sur ma machine, il faut environ 0,5 seconde pour compiler un assemblage en mémoire. Pas assez rapide pour l'analyse entre les frappes, mais assez rapide pour prendre en charge la génération à la demande de listes d'achèvement.
Je n'ai pas encore examiné LINQ.
Ce sera un problème beaucoup plus important car le lexeur / analyseur sémantique emacs a pour C #, ne "fait" pas LINQ.