Я работаю над установкой завершения (intellisense) для С# в emacs.
Идея состоит в том, что если пользователь набирает фрагмент, затем запрашивает завершение с помощью конкретной комбинации клавиш, средство завершения будет использовать отображение .NET для определения возможных завершений.
Выполнение этого требует, чтобы тип выполняемой вещи был известен. Если это строка, существует известный набор возможных методов и свойств; если это Int32, он имеет отдельный набор и т.д.
Используя семантический, пакет lexer/parser кода, доступный в emacs, я могу найти объявления переменных и их типы. Учитывая это, просто использовать рефлексию для получения методов и свойств в типе, а затем представить список вариантов для пользователя. (Хорошо, не так просто сделать в emacs, но используя возможность запускать процесс powershell внутри emacs, это становится намного проще. пользовательская сборка .NET, чтобы сделать отражение, загрузить его в powershell, а затем elisp, работающий в emacs, может отправлять команды на powershell и читать ответы через comint. В результате emacs может быстро получить результаты отражения.)
Проблема возникает, когда код использует var
в объявлении завершенной вещи. Это означает, что тип явно не указан, и завершение не будет работать.
Как я могу достоверно определить используемый фактический тип, когда переменная объявлена с ключевым словом var
? Чтобы быть ясным, мне не нужно определять его во время выполнения. Я хочу определить его в "Время разработки".
Пока у меня есть эти идеи:
- скомпилировать и вызвать:
- извлечь выражение о декларации, например `var foo = "строковое значение";`
- конкатенировать оператор `foo.GetType();`
- динамически компилировать полученный С# фрагмент в новую сборку
- загрузите сборку в новый AppDomain, запустите фрейм-сегмент и получите возвращаемый тип.
- выгрузить и удалить сборку
Я знаю, как все это делать. Но это звучит ужасно тяжело, для каждого запроса на завершение в редакторе.
Я полагаю, что мне не нужен новый новый AppDomain каждый раз. Я мог бы повторно использовать один AppDomain для нескольких временных сборок и амортизировать затраты на его установку и разрывать его по нескольким запросам на завершение. Это более тонкая идея базовой идеи.
- скомпилировать и проверить IL
Просто скомпилируйте декларацию в модуль и затем проверите IL, чтобы определить фактический тип, который был выведен компилятором. Как это возможно? Что я буду использовать для изучения ИЛ?
Есть ли лучшие идеи? Комментарии? предложения?
РЕДАКТИРОВАТЬ - подумайте об этом дальше, компиляция и вызов не приемлемы, потому что вызов может иметь побочные эффекты. Поэтому первый вариант должен быть исключен.
Кроме того, я думаю, что не могу предположить наличие .NET 4.0.
ОБНОВЛЕНИЕ. Правильный ответ, не упомянутый выше, но осторожно отмеченный Эриком Липпертом, заключается в том, чтобы внедрить систему вывода полного типа верности. Это единственный способ надежно определить тип var во время разработки. Но это также непросто сделать. Поскольку у меня нет иллюзий, что я хочу попытаться построить такую вещь, я взял ярлык варианта 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.?
Механизм добавляет соответствующие предложения использования к сгенерированному исходному коду, чтобы он правильно компилировался, а затем проверка 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.?
Синтаксис Nor LINQ.
Мне нужно подумать о том, насколько ценны эти вещи, прежде чем я буду рассматривать их через то, что определенно является "ограниченным дизайном" (вежливое слово для взлома) для завершения.
Подход к решению проблемы с зависимостями от аргументов метода или методов экземпляров должен заключаться в замене в фрагменте кода, который генерируется, скомпилирован, а затем анализируется IL, ссылки на эти вещи с "синтетическими" локальными варами такой же тип.
Еще одно обновление - завершение работы над vars, которое зависит от членов экземпляра, теперь работает.
То, что я сделал, было опросом типа (через семантику), а затем генерировать синтетические элементы для всех существующих членов. Для буфера С#:
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 тип локального var 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);
}
}
}
Все члены экземпляра и статического типа доступны в коде скелета. Он успешно компилируется. В этот момент определение типа локального var выполняется с помощью Reflection.
Что делает это возможным:
- возможность запуска powershell в emacs
- Компилятор С# очень быстрый. На моей машине для компиляции сборки в памяти требуется около 0,5 с. Не достаточно быстро для анализа между нажатиями клавиш, но достаточно быстро, чтобы поддерживать составление списков завершения по требованию.
Я еще не изучал LINQ.
Это будет гораздо более серьезной проблемой, потому что семантический lexer/parser emacs имеет для С#, не "делает" LINQ.