Я працюю над завершенням (intellisense) об'єктом для C # в emacs.
Ідея полягає в тому, що якщо користувач вводить фрагмент, а потім просить завершити за допомогою певної комбінації натискань клавіш, програма завершення використовує відображення .NET для визначення можливих завершень.
Для цього потрібно знати тип речі, що завершується, бути відомим. Якщо це рядок, існує відомий набір можливих методів та властивостей; якщо це Int32, він має окремий набір тощо.
Використовуючи семантичний пакет коду лексеру / парсера, доступний в emacs, я можу знайти декларації змінних та їх типи. Враховуючи це, просто використовувати відображення, щоб отримати методи та властивості за типом, а потім представити користувачеві список параметрів. (Добре, не зовсім просто зробити в Emacs, але використовуючи можливість запуску процесу Powershell всередині Emacs , стає набагато простіше. Я пишу для користувача збірки .NET , щоб зробити відображення, завантажте його в PowerShell, а потім Elisp працює в межах emacs може відправляти команди в паттерн і читати відповіді за допомогою comint. В результаті emacs може швидко отримати результати відображення.)
Проблема виникає, коли код використовує var
в декларації про завершену річ. Це означає, що тип не вказаний явно, і завершення не працюватиме.
Як я можу надійно визначити фактично використаний тип, коли змінна оголошується за допомогою var
ключового слова? Щоб було зрозуміло, мені не потрібно визначати це під час виконання. Я хочу визначити це в "Час проектування".
Поки що у мене є такі ідеї:
- компілювати та викликати:
- витягнути заяву декларації, наприклад `var foo =" значення рядка ";`
- об'єднати вислів `foo.GetType ();`
- динамічно компілюйте отриманий фрагмент C # в нову збірку
- завантажте збірку в новий AppDomain, запустіть фрагмент і отримайте тип повернення.
- вивантажте та відкиньте збірку
Я знаю, як все це зробити. Але це звучить надзвичайно важко для кожного запиту на завершення в редакторі.
Я думаю, що мені не потрібен новий свіжий AppDomain кожен раз. Я можу повторно використовувати один AppDomain для декількох тимчасових збірок і амортизувати витрати на його налаштування та зривання за допомогою декількох запитів на завершення. Це більше виправлення основної ідеї.
- складати та перевіряти ІЛ
Просто складіть декларацію в модуль, а потім огляньте ІЛ, щоб визначити фактичний тип, який був зроблений висновком компілятора. Як це було б можливо? Що б я використав для дослідження ІЛ?
Якісь кращі ідеї там? Коментарі? пропозиції?
EDIT - роздумувати над цим далі, компіляція та виклик неприйнятна, оскільки виклик може мати побічні ефекти. Тож перший варіант повинен бути виключений.
Крім того, я думаю, я не можу припустити наявність .NET 4.0.
ОНОВЛЕННЯ - Правильна відповідь, не згадана вище, але її обережно зазначив Ерік Ліпперт, полягає в застосуванні повної системи виводу типу вірності. Це єдиний спосіб надійно визначити тип вару на час проектування. Але це також зробити непросто. Оскільки я не відчуваю жодних ілюзій, що хочу спробувати створити таке, я взяв ярлик варіанту 2 - витягніть відповідний код декларації та скомпілюйте його, а потім огляньте отриманий ІР.
Це фактично працює для справедливого набору сценаріїв завершення.
Наприклад, припустимо, у наступних фрагментах коду:? - це позиція, в якій користувач просить завершити. Це працює:
var x = "hello there";
x.?
Завершення розуміє, що x - це String, і забезпечує відповідні параметри. Це робиться, генеруючи та потім компілюючи такий вихідний код:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... а потім огляд ІЛ з простим відображенням.
Це також працює:
var x = new XmlDocument();
x.?
Двигун додає відповідні за допомогою пункту до сформованого вихідного коду, щоб він компілювався належним чином, і тоді перевірка IL буде такою ж.
Це також працює:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Це просто означає, що інспекція ІЛ повинна знайти тип третьої локальної змінної замість першої.
І це:
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.
Мені доведеться подумати про те, наскільки цінні ці речі, перш ніж розглянути їх вирішення через те, що, безумовно, є "обмеженим дизайном" (ввічливим словом для злому) для завершення.
Підхід до вирішення проблеми із залежностями від аргументів методу чи методів екземплярів полягав би у заміні у фрагменті коду, який генерується, компілюється та потім аналізується ІЛ, посиланнями на ці речі на "синтетичні" локальні параметри одного типу.
Ще одне оновлення - завершення на vars, які залежать від членів екземпляра, зараз працює.
Що я робив - це допит типу (через семантичний), а потім генерування синтетичних резервних членів для всіх існуючих членів. Для такого буфера 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.