Як працює завершення коду?


84

Багато редакторів та середовищ розробки ID мають заповнений код. Деякі з них дуже "розумні", інші насправді не є. Мене цікавить більш розумний тип. Наприклад, я бачив середовища розробки, які пропонують функцію лише в тому випадку, якщо вона а) доступна в поточній області дії б) її повернене значення є дійсним. (Наприклад, після "5 + foo [вкладка]" він пропонує лише функції, які повертають те, що можна додати до цілого числа або імен змінних правильного типу.) Я також бачив, що вони ставлять найбільш часто використовуваний або найдовший варіант вперед списку.

Я розумію, що вам потрібно проаналізувати код. Але зазвичай, коли редагування поточного коду є недійсним, у ньому є синтаксичні помилки. Як аналізувати щось, коли воно є неповним і містить помилки?

Також існує обмеження в часі. Завершення даремно, якщо для складання списку потрібні секунди. Іноді алгоритм завершення має справу з тисячами класів.

Які хороші алгоритми та структури даних для цього?


1
Гарне запитання. Можливо, ви захочете поглянути на код для деяких середовищ IDE з відкритим кодом, які реалізують це, наприклад Code :: Blocks на codeblocks.org .

1
Ось стаття про створення Завершення коду в C # Створення заповнення коду в C #
Pritam Zope

Відповіді:


64

Механізм 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>своїх членів, я зберігаю a 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 #)
  • Семантично правильно Знайти всі посилання

Це велике спасибі. Я ніколи не думав про упередження з урахуванням регістру при сортуванні. Мені особливо подобається, що ви можете мати справу з невідповідними брекетами.
стрибіка

15

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

Коли ви вводите персонажа, він проходить шлях у трие. Усі нащадки певного вузла три є можливими завершеннями. Потім IDE просто потрібно відфільтрувати їх за тими, що мають сенс у поточному контексті, але йому потрібно лише обчислити стільки, скільки можна відобразити у спливаючому вікні завершення вкладки.

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

Обчислення трие (або інших структур даних) вимагає аналізу всього коду, щоб отримати список усіх символів у вашому проекті. Visual Studio зберігає це у своїй базі даних IntelliSense, .ncbфайлі, який зберігається поряд із вашим проектом, так що йому не доведеться переробляти все щоразу, коли ви закриваєте та відкриваєте проект. Перший раз, коли ви відкриваєте великий проект (скажімо, той, який ви щойно синхронізували з джерелом форми), VS знайде час, щоб проаналізувати все і сформувати базу даних.

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

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


Трие - справді гарна ідея. Що стосується поступових змін, можливо, можна спершу спробувати повторно проаналізувати файл, коли це не працює, ігноруючи поточний рядок, а коли це не працює, ігноруйте вкладений блок {...}. Якщо все інше не вдається, використовуйте останню базу даних.
stribika

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.