Pracuję nad narzędziem uzupełniania (Intellisense) dla języka C # w emacsie.
Chodzi o to, że jeśli użytkownik wpisze fragment, a następnie poprosi o uzupełnienie za pomocą określonej kombinacji naciśnięć klawiszy, narzędzie uzupełniania użyje odbicia .NET do określenia możliwych uzupełnień.
Aby to zrobić, trzeba znać rodzaj realizowanej rzeczy. Jeśli jest to ciąg znaków, istnieje znany zestaw możliwych metod i właściwości; jeśli jest to Int32, ma oddzielny zestaw i tak dalej.
Używając semantycznego pakietu leksera / parsera kodu dostępnego w emacsie, mogę zlokalizować deklaracje zmiennych i ich typy. Biorąc to pod uwagę, łatwo jest użyć odbicia, aby uzyskać metody i właściwości typu, a następnie przedstawić listę opcji użytkownikowi. (Ok, nie jest to całkiem proste do zrobienia w emacsie, ale używając możliwości uruchomienia procesu PowerShell wewnątrz emacsa , staje się znacznie łatwiejsze. Piszę niestandardowy zespół .NET, aby wykonać odbicie, załadować go do PowerShell, a następnie elisp uruchomić w nim emacs może wysyłać polecenia do programu PowerShell i odczytywać odpowiedzi przez comint. W rezultacie emacs może szybko uzyskać wyniki refleksji).
Problem pojawia się, gdy kod używa var
w deklaracji rzeczy do wykonania. Oznacza to, że typ nie jest jawnie określony, a uzupełnianie nie będzie działać.
W jaki sposób mogę wiarygodnie określić używany typ, gdy zmienna jest zadeklarowana za pomocą var
słowa kluczowego? Żeby było jasne, nie muszę tego określać w czasie wykonywania. Chcę to określić w „czasie projektowania”.
Do tej pory mam te pomysły:
- kompiluj i wywołuj:
- wyodrębnij deklarację, np. `var foo =" wartość ciągu ";`
- konkatenuje instrukcję `foo.GetType ();`
- dynamicznie skompiluj wynikowy C # fragment go do nowego zestawu
- Załaduj zestaw do nowej domeny AppDomain, uruchom ramkę i pobierz zwracany typ.
- rozładować i wyrzucić zespół
Wiem, jak to wszystko zrobić. Ale brzmi to strasznie ciężko, dla każdego żądania uzupełnienia w edytorze.
Przypuszczam, że nie potrzebuję za każdym razem nowej domeny AppDomain. Mógłbym ponownie użyć pojedynczej domeny AppDomain do wielu tymczasowych zestawów i zamortyzować koszty jego konfiguracji i zerwania w wielu żądaniach ukończenia. To bardziej modyfikacja podstawowej idei.
- skompiluj i sprawdź IL
Po prostu skompiluj deklarację do modułu, a następnie sprawdź IL, aby określić rzeczywisty typ, który został wywnioskowany przez kompilator. Jak byłoby to możliwe? Czego użyłbym do zbadania IL?
Są jakieś lepsze pomysły? Komentarze? propozycje?
EDYCJA - myśląc o tym dalej, kompiluj i wywołuj jest nie do przyjęcia, ponieważ wywołanie może mieć skutki uboczne. Dlatego pierwszą opcję należy wykluczyć.
Myślę też, że nie mogę zakładać obecności .NET 4.0.
AKTUALIZACJA - Prawidłowa odpowiedź, niewymieniona powyżej, ale delikatnie wskazana przez Erica Lipperta, polega na wdrożeniu systemu wnioskowania o pełnej wierności. Jest to jedyny sposób na wiarygodne określenie typu zmiennej w czasie projektowania. Ale to też nie jest łatwe. Ponieważ nie mam złudzeń, że chcę spróbować zbudować coś takiego, wybrałem skrót do opcji 2 - wyodrębnij odpowiedni kod deklaracji i skompiluj go, a następnie sprawdź wynikowy IL.
To faktycznie działa w przypadku sporego podzbioru scenariuszy zakończenia.
Na przykład załóżmy, że w poniższych fragmentach kodu? to pozycja, na której użytkownik prosi o wypełnienie. To działa:
var x = "hello there";
x.?
Ukończenie uświadamia sobie, że x jest ciągiem i zapewnia odpowiednie opcje. Robi to, generując, a następnie kompilując następujący kod źródłowy:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... a następnie inspekcja IL z prostą refleksją.
Działa to również:
var x = new XmlDocument();
x.?
Silnik dodaje odpowiednie klauzule using do wygenerowanego kodu źródłowego tak, aby kompilował się poprawnie, a następnie inspekcja IL jest taka sama.
To też działa:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Oznacza to po prostu, że inspekcja IL musi znaleźć typ trzeciej zmiennej lokalnej zamiast pierwszej.
I to:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... czyli o jeden poziom głębiej niż w poprzednim przykładzie.
Ale to, co nie działa, to uzupełnianie dowolnej zmiennej lokalnej, której inicjalizacja zależy w dowolnym momencie od elementu członkowskiego instancji lub argumentu metody lokalnej. Lubić:
var foo = this.InstanceMethod();
foo.?
Ani składnia LINQ.
Muszę pomyśleć o tym, jak cenne są te rzeczy, zanim rozważę rozwiązanie ich za pomocą zdecydowanie „ograniczonego projektu” (grzeczne słowo oznaczające hack).
Podejściem do rozwiązania problemu z zależnościami od argumentów metod lub metod instancji byłoby zastąpienie w fragmencie kodu, który jest generowany, kompilowany, a następnie analizowany IL, odniesienia do tych rzeczy „syntetycznymi” lokalnymi zmiennymi tego samego typu.
Kolejna aktualizacja - teraz działa aktualizacja zmiennych zależnych od członków instancji.
To, co zrobiłem, to przesłuchanie typu (za pomocą semantycznej), a następnie wygenerowanie syntetycznych członków zastępczych dla wszystkich istniejących członków. Dla bufora C # takiego jak ten:
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...
... wygenerowany kod, który jest kompilowany, dzięki czemu mogę dowiedzieć się z wyjścia IL typu lokalnego var nnn, wygląda następująco:
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);
}
}
}
Wszystkie elementy członkowskie instancji i typu statycznego są dostępne w kodzie szkieletowym. Kompiluje się pomyślnie. W tym momencie określenie typu zmiennej lokalnej jest proste dzięki odbiciu.
Umożliwia to:
- możliwość uruchomienia programu PowerShell w emacsie
- kompilator C # jest naprawdę szybki. Na moim komputerze kompilacja zestawu w pamięci zajmuje około 0,5 sekundy. Niewystarczająco szybki do analizy między naciśnięciami klawiszy, ale wystarczająco szybki, aby obsługiwać generowanie list ukończenia na żądanie.
Nie zajrzałem jeszcze do LINQ.
Będzie to znacznie większy problem, ponieważ semantyczny lekser / parser, który emacs ma dla C #, nie „robi” LINQ.