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".
quelle
Antworten:
Ich kann für Sie beschreiben, wie wir das in der "echten" C # IDE effizient machen.
Das erste, was wir tun, ist einen Pass auszuführen, der nur das "Top Level" -Ding im Quellcode analysiert. Wir überspringen alle Methodenkörper. Auf diese Weise können wir schnell eine Datenbank mit Informationen darüber aufbauen, welche Namespaces, Typen und Methoden (und Konstruktoren usw.) im Quellcode des Programms enthalten sind. Das Analysieren jeder einzelnen Codezeile in jedem Methodenkörper würde viel zu lange dauern, wenn Sie versuchen, dies zwischen Tastenanschlägen auszuführen.
Wenn die IDE den Typ eines bestimmten Ausdrucks in einem Methodenkörper ermitteln muss, geben Sie "foo" ein. und wir müssen herausfinden, was die Mitglieder von foo sind - wir tun dasselbe; Wir überspringen so viel Arbeit wie möglich.
Wir beginnen mit einem Durchlauf, der nur die lokalen Variablendeklarationen innerhalb dieser Methode analysiert . Wenn wir diesen Durchgang ausführen, machen wir eine Zuordnung von einem Paar aus "Bereich" und "Name" zu einem "Typbestimmer". Der "Typbestimmer" ist ein Objekt, das den Begriff "Ich kann den Typ dieses Lokals ermitteln, wenn ich muss" darstellt. Das Ausarbeiten des Typs eines Einheimischen kann teuer sein, daher möchten wir diese Arbeit verschieben, wenn dies erforderlich ist.
Wir haben jetzt eine träge Datenbank, die uns den Typ jedes Lokals mitteilen kann. Also, zurück zu diesem "Foo". - Wir finden heraus, in welcher Anweisung sich der relevante Ausdruck befindet, und führen dann den semantischen Analysator für genau diese Anweisung aus. Angenommen, Sie haben den Methodenkörper:
und jetzt müssen wir herausfinden, dass foo vom Typ char ist. Wir erstellen eine Datenbank mit allen Metadaten, Erweiterungsmethoden, Quellcodetypen usw. Wir erstellen eine Datenbank mit Typbestimmungsfaktoren für x, y und z. Wir analysieren die Aussage, die den interessanten Ausdruck enthält. Wir beginnen damit, es syntaktisch zu transformieren
Um die Art von foo herauszufinden, müssen wir zuerst die Art von y kennen. An dieser Stelle fragen wir den Typbestimmer: "Was ist der Typ von y?" Anschließend wird ein Ausdrucksauswerter gestartet, der x.ToCharArray () analysiert und fragt: "Was ist der Typ von x?" Wir haben einen Typbestimmer für das, der sagt "Ich muss" String "im aktuellen Kontext nachschlagen". Der aktuelle Typ enthält keinen Typ String, daher sehen wir uns den Namespace an. Es ist auch nicht da, also schauen wir in den using-Anweisungen nach und stellen fest, dass es ein "using-System" gibt und dass das System einen Typ-String hat. OK, das ist also die Art von x.
Anschließend fragen wir die Metadaten von System.String nach dem Typ von ToCharArray ab und es wird angegeben, dass es sich um ein System.Char [] handelt. Super. Wir haben also einen Typ für y.
Jetzt fragen wir: "Hat System.Char [] eine Methode, wo?" Also schauen wir uns die using-Direktiven an; Wir haben bereits eine Datenbank vorberechnet, die alle Metadaten für Erweiterungsmethoden enthält, die möglicherweise verwendet werden könnten.
Jetzt sagen wir: "OK, es gibt achtzehn Dutzend Erweiterungsmethoden mit dem Namen Where in scope. Hat eine von ihnen einen ersten formalen Parameter, dessen Typ mit System.Char [] kompatibel ist?" Also starten wir eine Runde Konvertierbarkeitstests. Die Where-Erweiterungsmethoden sind jedoch generisch , was bedeutet, dass wir Typinferenz durchführen müssen.
Ich habe eine spezielle Typ-Infererencing-Engine geschrieben, die unvollständige Inferenzen vom ersten Argument bis zu einer Erweiterungsmethode verarbeiten kann. Wir führen den Typ inferrer aus und stellen fest, dass es eine Where-Methode gibt, die eine nimmt
IEnumerable<T>
, und dass wir eine Folgerung von System.Char [] nach machen könnenIEnumerable<System.Char>
, also ist T System.Char.Die Signatur dieser Methode ist
Where<T>(this IEnumerable<T> items, Func<T, bool> predicate)
und wir wissen, dass T System.Char ist. Wir wissen auch, dass das erste Argument in den Klammern der Erweiterungsmethode ein Lambda ist. Wir starten also einen Inferrer vom Typ Lambda-Ausdruck, der besagt, dass "der formale Parameter foo als System.Char angenommen wird". Verwenden Sie diese Tatsache, wenn Sie den Rest des Lambda analysieren.Wir haben jetzt alle Informationen, die wir brauchen, um den Körper des Lambda zu analysieren, der "foo" ist. Wir suchen nach der Art des Foo und stellen fest, dass es sich laut Lambda-Binder um System.Char handelt, und wir sind fertig. Wir zeigen Typinformationen für System.Char an.
Und wir machen alles außer der "Top Level" -Analyse zwischen den Tastenanschlägen . Das ist das wirklich Knifflige. Eigentlich ist es nicht schwer, die gesamte Analyse zu schreiben. Es macht es schnell genug, dass Sie es mit einer Schreibgeschwindigkeit tun können, die das wirklich schwierige Stück ist.
Viel Glück!
quelle
Ich kann Ihnen ungefähr sagen, wie die Delphi-IDE mit dem Delphi-Compiler zusammenarbeitet, um Intellisense auszuführen (Code Insight nennt Delphi das). Es ist nicht zu 100% auf C # anwendbar, aber es ist ein interessanter Ansatz, der Beachtung verdient.
Die meisten semantischen Analysen in Delphi werden im Parser selbst durchgeführt. Ausdrücke werden beim Parsen eingegeben, außer in Situationen, in denen dies nicht einfach ist. In diesem Fall wird die Vorausschau-Analyse verwendet, um herauszufinden, was beabsichtigt ist, und diese Entscheidung wird dann in der Analyse verwendet.
Bei der Analyse handelt es sich größtenteils um eine rekursive LL (2) -Rückführung, mit Ausnahme von Ausdrücken, die unter Verwendung der Operatorrangfolge analysiert werden. Eines der besonderen Merkmale von Delphi ist, dass es sich um eine Single-Pass-Sprache handelt. Daher müssen Konstrukte deklariert werden, bevor sie verwendet werden. Daher ist kein Top-Level-Pass erforderlich, um diese Informationen herauszubringen.
Diese Kombination von Funktionen bedeutet, dass der Parser ungefähr alle Informationen hat, die für Code Insight an jedem Punkt benötigt werden, an dem sie benötigt werden. So funktioniert es: Die IDE informiert den Lexer des Compilers über die Position des Cursors (den Punkt, an dem Codeeinsicht gewünscht wird), und der Lexer wandelt dies in ein spezielles Token um (es wird als Kibitz-Token bezeichnet). Immer wenn der Parser auf dieses Token trifft (das sich irgendwo befinden könnte), weiß er, dass dies das Signal ist, alle Informationen, die er hat, an den Editor zurückzusenden. Dies geschieht mit einem longjmp, da es in C geschrieben ist. Es benachrichtigt den endgültigen Aufrufer über die Art des syntaktischen Konstrukts (dh den grammatikalischen Kontext), in dem der Kibitz-Punkt gefunden wurde, sowie über alle für diesen Punkt erforderlichen symbolischen Tabellen. So zum Beispiel Wenn sich der Kontext in einem Ausdruck befindet, der ein Argument für eine Methode ist, können wir die Methodenüberladungen überprüfen, die Argumenttypen überprüfen und die gültigen Symbole nur nach denen filtern, die in diesen Argumenttyp aufgelöst werden können (dies reduziert in a viel irrelevante Kruft im Dropdown). Wenn es sich in einem verschachtelten Bereichskontext befindet (z. B. nach einem "."), Hat der Parser einen Verweis auf den Bereich zurückgegeben, und die IDE kann alle in diesem Bereich gefundenen Symbole auflisten.
Andere Dinge werden auch getan; Beispielsweise werden Methodenkörper übersprungen, wenn das Kibitz-Token nicht in ihrer Reichweite liegt. Dies erfolgt optimistisch und wird zurückgesetzt, wenn das Token übersprungen wird. Das Äquivalent zu Erweiterungsmethoden - Klassenhelfer in Delphi - verfügt über eine Art versionierten Cache, sodass die Suche relativ schnell erfolgt. Aber Delphis generische Typinferenz ist viel schwächer als die von C #.
Nun zur spezifischen Frage: Das Ableiten der mit deklarierten Variablentypen
var
entspricht der Art und Weise, wie Pascal den Konstantentyp ableitet. Es kommt vom Typ des Initialisierungsausdrucks. Diese Typen werden von unten nach oben aufgebaut. Wennx
es vom TypInteger
ist undy
vom Typ istDouble
, dannx + y
ist es vom TypDouble
, weil dies die Regeln der Sprache sind; usw. Sie befolgen diese Regeln, bis Sie auf der rechten Seite einen Typ für den vollständigen Ausdruck haben, und diesen Typ verwenden Sie für das Symbol auf der linken Seite.quelle
Wenn Sie keinen eigenen Parser schreiben müssen, um den abstrakten Syntaxbaum zu erstellen, können Sie die Parser von SharpDevelop oder MonoDevelop verwenden , die beide Open Source sind.
quelle
Intellisense-Systeme stellen den Code normalerweise mithilfe eines abstrakten Syntaxbaums dar, mit dem sie den Rückgabetyp der Funktion, die der Variablen 'var' zugewiesen ist, mehr oder weniger auf dieselbe Weise wie der Compiler auflösen können. Wenn Sie VS Intellisense verwenden, stellen Sie möglicherweise fest, dass der Typ var erst angezeigt wird, wenn Sie einen gültigen (auflösbaren) Zuweisungsausdruck eingegeben haben. Wenn der Ausdruck immer noch nicht eindeutig ist (z. B. kann er die generischen Argumente für den Ausdruck nicht vollständig ableiten), wird der var-Typ nicht aufgelöst. Dies kann ein ziemlich komplexer Prozess sein, da Sie möglicherweise ziemlich tief in einen Baum hineingehen müssen, um den Typ aufzulösen. Zum Beispiel:
Der Rückgabetyp ist
IEnumerable<Bar>
, aber um dies zu lösen, ist Folgendes erforderlich:IEnumerable
.OfType<T>
die für IEnumerable gilt.IEnumerable<Foo>
und es gibt eine Erweiterungsmethode,Select
die hierfür gilt.foo => foo.Bar
hat den Parameter foo vom Typ Foo. Dies wird durch die Verwendung von Select abgeleitet, die a benötigt,Func<TIn,TOut>
und da TIn bekannt ist (Foo), kann der Typ von foo abgeleitet werden.IEnumerable<TOut>
und TOut aus dem Ergebnis des Lambda-Ausdrucks abgeleitet werden können, daher muss der resultierende Elementtyp seinIEnumerable<Bar>
.quelle
Da Sie auf Emacs abzielen, ist es möglicherweise am besten, mit der CEDET-Suite zu beginnen. Alle Details, die Eric Lippert bereits im Code Analyzer im CEDET / Semantic Tool für C ++ behandelt hat. Es gibt auch einen C # -Parser (der wahrscheinlich ein wenig TLC benötigt), sodass die einzigen fehlenden Teile mit der Optimierung der erforderlichen Teile für C # zusammenhängen.
Die grundlegenden Verhaltensweisen werden in Kernalgorithmen definiert, die von überladbaren Funktionen abhängen, die pro Sprache definiert werden. Der Erfolg der Completion Engine hängt davon ab, wie viel Tuning durchgeführt wurde. Mit C ++ als Leitfaden sollte es nicht schlecht sein, Unterstützung ähnlich wie C ++ zu erhalten.
Daniels Antwort schlägt vor, MonoDevelop zum Parsen und Analysieren zu verwenden. Dies könnte ein alternativer Mechanismus anstelle des vorhandenen C # -Parsers sein oder zum Erweitern des vorhandenen Parsers verwendet werden.
quelle
var
. Semantic identifiziert es korrekt als var, bietet jedoch keine Typinferenz. Meine Frage war speziell, wie ich das angehen soll . Ich habe auch versucht, mich an die vorhandene CEDET-Fertigstellung anzuschließen, konnte aber nicht herausfinden, wie. Die Dokumentation für CEDET ist ... ah ... nicht vollständig.Es ist ein schwieriges Problem, es gut zu machen. Grundsätzlich müssen Sie die Sprachspezifikation / den Compiler durch die meisten Lexing / Parsing / Typechecking modellieren und ein internes Modell des Quellcodes erstellen, das Sie dann abfragen können. Eric beschreibt es ausführlich für C #. Sie können jederzeit den Quellcode des F # -Compilers (Teil des F # CTP) herunterladen und sich
service.fsi
die Schnittstelle ansehen , die aus dem F # -Compiler verfügbar gemacht wird, den der F # -Sprachendienst für die Bereitstellung von Intellisense, Tooltips für abgeleitete Typen usw. verwendet ein Gefühl für eine mögliche 'Schnittstelle', wenn Sie den Compiler bereits als API zum Aufrufen zur Verfügung hatten.Die andere Möglichkeit besteht darin, die Compiler unverändert wiederzuverwenden, wie Sie es beschreiben, und dann Reflection zu verwenden oder den generierten Code zu betrachten. Dies ist unter dem Gesichtspunkt problematisch, dass Sie "vollständige Programme" benötigen, um eine Kompilierungsausgabe von einem Compiler zu erhalten, während Sie beim Bearbeiten des Quellcodes im Editor häufig nur "Teilprogramme" haben, die noch nicht analysiert wurden habe alle Methoden noch implementiert, etc.
Kurz gesagt, ich denke, die "Low Budget" -Version ist sehr schwer gut zu machen, und die "echte" Version ist sehr, sehr schwer gut zu machen. (Wo "hart" hier sowohl "Aufwand" als auch "technische Schwierigkeit" misst.)
quelle
NRefactory erledigt dies für Sie.
quelle
Für die Lösung "1" haben Sie eine neue Funktion in .NET 4, um dies schnell und einfach zu erledigen. Wenn Sie also Ihr Programm in .NET 4 konvertieren lassen können, ist dies die beste Wahl.
quelle