Ich arbeite an einer Vervollständigungsfunktion (Intellisense) für C # in Emacs.
Die Idee ist, dass, wenn ein Benutzer ein Fragment eingibt und dann über eine bestimmte Tastenkombination nach Abschluss fragt, die Abschlussfunktion .NET Reflection verwendet, um die möglichen Abschlüsse zu ermitteln.
Um dies zu tun, muss der Typ der zu erledigenden Sache bekannt sein. Wenn es sich um eine Zeichenfolge handelt, sind eine Reihe möglicher Methoden und Eigenschaften bekannt. Wenn es sich um ein Int32 handelt, hat es einen separaten Satz und so weiter.
Mit semantic, einem in emacs verfügbaren Code-Lexer / Parser-Paket, kann ich die Variablendeklarationen und ihre Typen finden. Angesichts dessen ist es einfach, Reflection zu verwenden, um die Methoden und Eigenschaften des Typs abzurufen und dem Benutzer dann die Liste der Optionen zu präsentieren. (Ok, nicht ganz einfach zu tun , innerhalb von Emacs, wobei jedoch die Möglichkeit , einen Powershell - Prozess innerhalb von Emacs zu laufen , wird es viel einfacher. Ich ein benutzerdefinierte .NET schreibe Montag Reflexion zu tun, es in die Powershell laden, und dann elisp im laufenden emacs können über comint Befehle an Powershell senden und Antworten lesen. Dadurch können emacs die Ergebnisse der Reflexion schnell abrufen.)
Das Problem tritt auf, wenn der Code var
in der Deklaration der abgeschlossenen Sache verwendet wird. Das bedeutet, dass der Typ nicht explizit angegeben ist und die Vervollständigung nicht funktioniert.
Wie kann ich den tatsächlich verwendeten Typ zuverlässig bestimmen, wenn die Variable mit dem var
Schlüsselwort deklariert wird? Um ganz klar zu sein, muss ich es zur Laufzeit nicht ermitteln. Ich möchte es zur "Entwurfszeit" bestimmen.
Bisher habe ich folgende Ideen:
- kompilieren und aufrufen:
- Extrahieren Sie die Deklarationsanweisung, z. B. "var foo =" a string value ";"
- verketten Sie eine Anweisung `foo.GetType ();`
- Kompilieren Sie das resultierende C # -Fragment dynamisch in eine neue Assembly
- Laden Sie die Assembly in eine neue AppDomain, führen Sie das Framgement aus und rufen Sie den Rückgabetyp ab.
- Entladen und entsorgen Sie die Baugruppe
Ich weiß, wie man das alles macht. Aber es klingt für jede Abschlussanforderung im Editor furchtbar schwer.
Ich brauche wohl nicht jedes Mal eine neue AppDomain. Ich könnte eine einzelne AppDomain für mehrere temporäre Assemblys wiederverwenden und die Kosten für das Einrichten und Herunterfahren über mehrere Abschlussanforderungen amortisieren. Das ist eher eine Optimierung der Grundidee.
- IL kompilieren und inspizieren
Kompilieren Sie einfach die Deklaration in ein Modul und überprüfen Sie dann die IL, um den tatsächlichen Typ zu bestimmen, der vom Compiler abgeleitet wurde. Wie wäre das möglich? Womit würde ich die IL untersuchen?
Irgendwelche besseren Ideen da draußen? Bemerkungen? Vorschläge?
BEARBEITEN - Wenn Sie weiter darüber nachdenken, ist das Kompilieren und Aufrufen nicht akzeptabel, da der Aufruf Nebenwirkungen haben kann. Die erste Option muss also ausgeschlossen werden.
Außerdem kann ich das Vorhandensein von .NET 4.0 nicht annehmen.
UPDATE - Die richtige Antwort, die oben nicht erwähnt wurde, auf die Eric Lippert jedoch sanft hingewiesen hat, besteht darin, ein Inferenzsystem vom Typ Full Fidelity zu implementieren. Dies ist die einzige Möglichkeit, den Typ einer Variable zur Entwurfszeit zuverlässig zu bestimmen. Aber es ist auch nicht einfach zu tun. Da ich keine Illusionen habe, dass ich versuchen möchte, so etwas zu erstellen, habe ich die Verknüpfung von Option 2 gewählt: Extrahieren Sie den entsprechenden Deklarationscode, kompilieren Sie ihn und überprüfen Sie die resultierende IL.
Dies funktioniert tatsächlich für eine angemessene Teilmenge der Abschlussszenarien.
Angenommen, in den folgenden Codefragmenten ist das? ist die Position, an der der Benutzer nach Abschluss fragt. Das funktioniert:
var x = "hello there";
x.?
Die Vervollständigung erkennt, dass x ein String ist, und bietet die entsprechenden Optionen. Dazu wird der folgende Quellcode generiert und anschließend kompiliert:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... und dann die IL mit einfacher Reflexion inspizieren.
Das funktioniert auch:
var x = new XmlDocument();
x.?
Die Engine fügt dem generierten Quellcode die entsprechenden using-Klauseln hinzu, damit er ordnungsgemäß kompiliert wird. Anschließend ist die IL-Überprüfung dieselbe.
Das funktioniert auch:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Es bedeutet nur, dass die IL-Inspektion den Typ der dritten lokalen Variablen anstelle der ersten finden muss.
Und das:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... das ist nur eine Ebene tiefer als das vorherige Beispiel.
Was jedoch nicht funktioniert, ist die Vervollständigung einer lokalen Variablen, deren Initialisierung zu einem beliebigen Zeitpunkt von einem Instanzmitglied oder einem lokalen Methodenargument abhängt. Mögen:
var foo = this.InstanceMethod();
foo.?
Noch LINQ-Syntax.
Ich muss darüber nachdenken, wie wertvoll diese Dinge sind, bevor ich darüber nachdenke, sie über ein definitiv "begrenztes Design" (höfliches Wort für Hack) zur Vervollständigung anzusprechen.
Ein Ansatz zur Behebung des Problems mit Abhängigkeiten von Methodenargumenten oder Instanzmethoden besteht darin, in dem Codefragment, das generiert, kompiliert und anschließend IL analysiert wird, die Verweise auf diese Dinge durch "synthetische" lokale Variablen des gleichen Typs zu ersetzen.
Ein weiteres Update - die Fertigstellung von Variablen, die von Instanzmitgliedern abhängen, funktioniert jetzt.
Ich habe den Typ (über die Semantik) abgefragt und dann synthetische Ersatzmitglieder für alle vorhandenen Mitglieder generiert. Für einen C # -Puffer wie diesen:
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...
... der generierte Code, der kompiliert wird, damit ich aus der Ausgabe IL den Typ der lokalen Variable nnn lernen kann, sieht folgendermaßen aus:
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);
}
}
}
Alle Instanz- und statischen Typelemente sind im Skeleton-Code verfügbar. Es wird erfolgreich kompiliert. Zu diesem Zeitpunkt ist die Bestimmung des Typs der lokalen Variable über Reflection unkompliziert.
Was dies möglich macht, ist:
- die Fähigkeit, Powershell in Emacs auszuführen
- Der C # -Compiler ist sehr schnell. Auf meinem Computer dauert das Kompilieren einer In-Memory-Assembly etwa 0,5 Sekunden. Nicht schnell genug für die Analyse zwischen Tastenanschlägen, aber schnell genug, um die On-Demand-Erstellung von Abschlusslisten zu unterstützen.
Ich habe mich noch nicht mit LINQ befasst.
Das wird ein viel größeres Problem sein, da die semantischen Lexer / Parser-Emacs für C # LINQ nicht "tun".