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