Я работаю над средством завершения (intellisense) для C # в emacs.
Идея заключается в том, что если пользователь набирает фрагмент, а затем запрашивает завершение с помощью определенной комбинации клавиш, средство завершения будет использовать отражение .NET для определения возможных завершений.
Для этого необходимо знать тип завершаемой вещи. Если это строка, существует известный набор возможных методов и свойств; если это Int32, у него отдельный набор и так далее.
Используя семантику, пакет лексера / анализатора кода, доступный в emacs, я могу найти объявления переменных и их типы. Учитывая это, легко использовать отражение, чтобы получить методы и свойства типа, а затем представить список параметров пользователю. (Хорошо, это не совсем просто сделать в emacs, но, используя возможность запускать процесс powershell внутри emacs , это становится намного проще. Я пишу настраиваемую сборку .NET для отражения, загружаю ее в powershell, а затем elisp работает в emacs может отправлять команды в powershell и читать ответы через comint. В результате emacs может быстро получить результаты отражения.)
Проблема возникает, когда код использует var
в объявлении то, что выполняется. Это означает, что тип явно не указан, и завершение не будет работать.
Как я могу надежно определить фактический используемый тип, если переменная объявлена с var
ключевым словом? Чтобы быть ясным, мне не нужно определять его во время выполнения. Я хочу определить это во «Время разработки».
Пока у меня есть эти идеи:
- скомпилировать и вызвать:
- извлеките оператор объявления, например, `var foo =" строковое значение ";`
- объединить оператор `foo.GetType ();`
- динамически скомпилировать полученный фрагмент C # в новую сборку
- загрузите сборку в новый AppDomain, запустите фрагмент и получите возвращаемый тип.
- выгрузить и выбросить сборку
Я умею все это делать. Но это звучит ужасно тяжеловесно для каждого запроса на завершение в редакторе.
Полагаю, мне не нужен каждый раз новый домен приложения. Я мог бы повторно использовать один AppDomain для нескольких временных сборок и амортизировать затраты на его настройку и разрушение по нескольким запросам на завершение. Это больше изменение основной идеи.
- компилировать и проверять IL
Просто скомпилируйте объявление в модуль, а затем проверьте IL, чтобы определить фактический тип, который был выведен компилятором. Как это было возможно? Что бы я использовал для проверки IL?
Есть идеи получше? Комментарии? предложения?
РЕДАКТИРОВАТЬ - размышляя об этом дальше, компиляция и вызов недопустимы, потому что вызов может иметь побочные эффекты. Так что первый вариант нужно исключить.
Кроме того, я думаю, что не могу предположить наличие .NET 4.0.
ОБНОВЛЕНИЕ . Правильный ответ, не упомянутый выше, но мягко указанный Эриком Липпертом, - реализовать систему вывода типов с полной точностью. Это единственный способ надежно определить тип переменной во время разработки. Но сделать это тоже непросто. Поскольку у меня нет иллюзий, что я хочу попытаться создать такую вещь, я воспользовался сокращением варианта 2 - извлеките соответствующий код объявления и скомпилируйте его, а затем изучите полученный IL.
Это действительно работает для некоторой части сценариев завершения.
Например, предположим, что в следующих фрагментах кода? - позиция, на которой пользователь запрашивает завершение. Это работает:
var x = "hello there";
x.?
Завершение понимает, что x является строкой, и предоставляет соответствующие параметры. Для этого он генерирует и компилирует следующий исходный код:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... а затем проверка IL с помощью простого отражения.
Это тоже работает:
var x = new XmlDocument();
x.?
Движок добавляет соответствующие предложения using к сгенерированному исходному коду, чтобы он правильно компилировался, а затем проверка IL остается такой же.
Это тоже работает:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Это просто означает, что проверка IL должна найти тип третьей локальной переменной вместо первой.
И это:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... что всего на один уровень глубже, чем в предыдущем примере.
Но то, что не работает, - это завершение любой локальной переменной, инициализация которой в любой момент зависит от члена экземпляра или аргумента локального метода. Подобно:
var foo = this.InstanceMethod();
foo.?
Ни синтаксиса LINQ.
Мне нужно будет подумать о том, насколько ценны эти вещи, прежде чем я решу решить их с помощью того, что определенно является «ограниченным дизайном» (вежливое слово для взлома) для завершения.
Подход к решению проблемы, связанной с зависимостями от аргументов метода или методов экземпляра, заключался бы в замене во фрагменте кода, который генерируется, компилируется и затем анализируется IL, ссылки на эти вещи с «синтетическими» локальными переменными того же типа.
Еще одно обновление - завершение для варов, которые зависят от членов экземпляра, теперь работает.
Что я сделал, так это опросил тип (через семантику), а затем сгенерировал синтетические замещающие члены для всех существующих членов. Для такого буфера C #:
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 тип локальной переменной nnn, выглядит так:
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);
}
}
}
Все члены экземпляра и статического типа доступны в скелетном коде. Он успешно компилируется. В этот момент определить тип локальной переменной просто через Reflection.
Что делает это возможным:
- возможность запускать powershell в emacs
- компилятор C # действительно быстр. На моей машине компиляция сборки в памяти занимает около 0,5 с. Не достаточно быстро для анализа между нажатиями клавиш, но достаточно быстро, чтобы поддерживать создание списков завершения по запросу.
Я еще не изучал LINQ.
Это будет гораздо более серьезной проблемой, потому что семантический лексер / синтаксический анализатор, который emacs имеет для C #, не "выполняет" LINQ.