Как работает автозавершение кода?

84

Многие редакторы и IDE имеют автозавершение кода. Некоторые из них очень «умны», другие нет. Меня интересует более умный тип. Например, я видел IDE, которые предлагают функцию только в том случае, если она а) доступна в текущей области б) ее возвращаемое значение является допустимым. (Например, после "5 + foo [tab]" он предлагает только функции, которые возвращают то, что может быть добавлено к целым числам или именам переменных правильного типа.) Я также видел, что они помещают впереди наиболее часто используемый или самый длинный вариант. списка.

Я понимаю, что вам нужно разобрать код. Но обычно при редактировании недействительного текущего кода в нем есть синтаксические ошибки. Как вы разбираете что-то, если оно неполное и содержит ошибки?

Также есть временные ограничения. Завершение бесполезно, если создание списка занимает секунды. Иногда алгоритм завершения имеет дело с тысячами классов.

Какие для этого подходят хорошие алгоритмы и структуры данных?

Стрибика
источник
1
Хороший вопрос. Возможно, вы захотите взглянуть на код некоторых из IDE с открытым исходным кодом, которые реализуют это, например Code :: Blocks на codeblocks.org .
1
Вот статья о создании автозавершения кода в C # Создание автозавершения кода в C #
Pritam Zope

Ответы:

65

Механизм IntelliSense в моем сервисном продукте языка UnrealScript сложен, но я дам здесь как можно лучший обзор. Служба языка C # в VS2008 SP1 - моя цель производительности (не зря). Этого еще нет, но он достаточно быстрый / точный, чтобы я мог безопасно предлагать предложения после ввода одного символа, не дожидаясь Ctrl + пробел или пользователя, вводящего. (точки). Чем больше информации люди [работающие над языковыми услугами] получат по этому поводу, тем лучше будет опыт конечных пользователей, если я когда-нибудь буду использовать их продукты. Есть ряд продуктов, с которыми у меня был неудачный опыт работы, которые не уделяли такого пристального внимания деталям, и в результате я боролся с IDE больше, чем кодировал.

В моей языковой службе это выглядит следующим образом:

  1. Получите выражение под курсором. Это идет от начала выражения доступа к члену до конца идентификатора, над которым находится курсор. Выражение доступа к члену обычно находится в форме aa.bb.cc, но также может содержать вызовы методов, как в aa.bb(3+2).cc.
  2. Получите контекст, окружающий курсор. Это очень сложно, потому что он не всегда следует тем же правилам, что и компилятор (длинная история), но здесь предположим, что это так. Обычно это означает получение кэшированной информации о методе / классе, внутри которого находится курсор.
  3. Скажем, реализует объект контекста IDeclarationProvider, где вы можете вызвать, GetDeclarations()чтобы получить IEnumerable<IDeclaration>из всех элементов, видимых в области. В моем случае этот список содержит локальные переменные / параметры (если в методе), члены (поля и методы, только статические, если только в методе экземпляра, и нет закрытых членов базовых типов), глобальные переменные (типы и константы для языка I работаю над) и ключевыми словами. В этом списке будет пункт с названием aa. В качестве первого шага в оценке выражения в №1 мы выбираем элемент из контекстного перечисления с именем aa, которое дает нам IDeclarationследующий шаг.
  4. Затем я применяю оператор к IDeclarationпредставлению, aaчтобы получить другой, IEnumerable<IDeclaration>содержащий «члены» (в некотором смысле) из aa. Поскольку .оператор отличается от ->оператора, я вызываю declaration.GetMembers(".")и ожидаю, что IDeclarationобъект правильно применит указанный оператор.
  5. Это продолжается, пока я не нажму cc, где список объявлений может содержать или не содержать объект с именем cc. Как я уверен, вы знаете, если несколько элементов начинаются с cc, они также должны появиться. Я решил эту проблему, взяв окончательное перечисление и пропустив его через свой документированный алгоритм, чтобы предоставить пользователю максимально полезную информацию.

Вот несколько дополнительных примечаний для серверной части IntelliSense:

  • Я широко использую механизмы отложенной оценки LINQ при реализации GetMembers. Каждый объект в моем кэше может предоставить функтор, оценивающий его члены, поэтому выполнение сложных действий с деревом почти тривиально.
  • Вместо того, чтобы каждый объект хранит a List<IDeclaration>из своих членов, я сохраняю List<Name>, где Name- структура, содержащая хеш специально отформатированной строки, описывающей член. Есть огромный кеш, который сопоставляет имена объектам. Таким образом, когда я повторно анализирую файл, я могу удалить все элементы, объявленные в файле, из кеша и повторно заполнить его обновленными членами. Из-за способа настройки функторов все выражения немедленно вычисляют новые элементы.

"Интерфейс" IntelliSense

По мере того как пользователь набирает текст, файл синтаксически неверен чаще, чем правильный. Таким образом, я не хочу случайно удалять разделы кеша, когда пользователь вводит текст. У меня есть большое количество правил для особых случаев, чтобы обрабатывать инкрементные обновления как можно быстрее. Инкрементный кеш хранится только локально для открытого файла и помогает убедиться, что пользователь не понимает, что его ввод заставляет внутренний кеш хранить неверную информацию о строках / столбцах для таких вещей, как каждый метод в файле.

  • Одним из плюсов моих достоинств является то, что мой парсер работает быстро . Он может обрабатывать полное обновление кэша исходного файла на 20000 строк за 150 мс, работая автономно в фоновом потоке с низким приоритетом. Каждый раз, когда этот синтаксический анализатор успешно завершает проход открытого файла (синтаксически), текущее состояние файла перемещается в глобальный кеш.
  • Если файл синтаксически неверен, я использую анализатор фильтра ANTLR (извините за ссылку - большая часть информации находится в списке рассылки или собрана при чтении источника), чтобы повторно проанализировать файл в поисках:
    • Объявления переменных / полей.
    • Подпись для определений класса / структуры.
    • Подпись для определений методов.
  • В локальном кэше определения классов / структур / методов начинаются с подписи и заканчиваются, когда уровень вложенности фигурных скобок возвращается к четному. Методы также могут завершиться, если достигнуто объявление другого метода (нет методов вложения).
  • В локальном кэше переменные / поля связаны с непосредственно предшествующим незамкнутым элементом. См. Краткий фрагмент кода ниже, чтобы понять, почему это важно.
  • Кроме того, по мере ввода пользователя я веду таблицу переназначения, в которой отмечаются добавленные / удаленные диапазоны символов. Это используется для:
    • Убедитесь, что я могу определить правильный контекст курсора, так как метод может / действительно перемещается в файле между полными анализами.
    • Убедитесь, что пункт «Перейти к объявлению / определению / ссылке» правильно определяет местонахождение элементов в открытых файлах.

Фрагмент кода для предыдущего раздела:

class A
{
    int x; // linked to A

    void foo() // linked to A
    {
        int local; // linked to foo()

    // foo() ends here because bar() is starting
    void bar() // linked to A
    {
        int local2; // linked to bar()
    }

    int y; // linked again to A

Я решил добавить список функций IntelliSense, которые я реализовал с помощью этого макета. Фотографии каждого находятся здесь.

  • Автозаполнение
  • Советы по инструментам
  • Советы по методам
  • Просмотр класса
  • Окно определения кода
  • Браузер вызовов (VS 2010, наконец, добавляет это в C #)
  • Семантически правильный Найти все ссылки
Сэм Харвелл
источник
Большое спасибо. Я никогда не думал о предвзятости при сортировке с учетом регистра. Мне особенно нравится, что вы можете иметь дело с неподходящими брекетами.
stribika 03
16

Я не могу точно сказать, какие алгоритмы используются в какой-либо конкретной реализации, но могу сделать некоторые обоснованные предположения. Trie очень полезная структура данных для этой проблемы: IDE может поддерживать большой в памяти синтаксического дерева всех символов в вашем проекте, с некоторыми дополнительными метаданными на каждом узле.

Когда вы вводите символ, он идет по тропинке в дереве. Все потомки конкретного trie-узла являются возможными дополнениями. Затем IDE просто нужно отфильтровать их по тем, которые имеют смысл в текущем контексте, но ей нужно только вычислить столько, сколько может быть отображено во всплывающем окне завершения табуляции.

Более продвинутое завершение табуляции требует более сложного дерева. Например, Visual Assist X имеет функцию, при которой вам нужно вводить только заглавные буквы символов CamelCase - например, если вы вводите SFN, он показывает вам символ SomeFunctionNameв своем окне завершения табуляции.

Вычисление дерева (или других структур данных) требует синтаксического анализа всего вашего кода, чтобы получить список всех символов в вашем проекте. Visual Studio хранит это в своей базе данных IntelliSense, .ncbфайле, который хранится вместе с вашим проектом, поэтому ему не нужно повторно анализировать все каждый раз, когда вы закрываете и снова открываете свой проект. В первый раз, когда вы откроете большой проект (скажем, тот, который вы только что синхронизировали с системой управления версиями), VS потребуется время, чтобы проанализировать все и сгенерировать базу данных.

Я не знаю, как он обрабатывает постепенные изменения. Как вы сказали, когда вы пишете код, это неверный синтаксис в 90% случаев, и повторный анализ всего, когда вы простаиваете, приведет к огромному налогу на ваш процессор с очень небольшой выгодой, особенно если вы изменяете файл заголовка, включенный большое количество исходных файлов.

Я подозреваю, что он либо (а) только повторно анализирует, когда вы действительно создаете свой проект (или, возможно, когда вы его закрываете / открываете), либо (б) он выполняет своего рода локальный синтаксический анализ, где он анализирует только код там, где вы только что отредактировано ограниченным образом, чтобы получить имена соответствующих символов. Поскольку C ++ имеет такую ​​чрезвычайно сложную грамматику, он может вести себя странно в темных углах, если вы используете тяжелое метапрограммирование шаблонов и тому подобное.

Адам Розенфилд
источник
ТРИ - действительно хорошая идея. Что касается инкрементных изменений, то может быть возможно сначала попытаться повторно проанализировать файл, когда это не сработает, игнорируя текущую строку, а когда это не работает, игнорируйте включающий блок {...}. Если ничего не помогает, используйте последнюю базу данных.
stribika 03