Как работи допълването на код?

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

Разбирам, че трябва да анализирате кода. Но обикновено, докато редактирането на текущия код е невалидно, в него има синтактични грешки. Как анализирате нещо, когато е непълно и съдържа грешки?

Има и ограничение във времето. Завършването е безполезно, ако изготвянето на списък отнема секунди. Понякога алгоритъмът за завършване се занимава с хиляди класове.

Кои са добрите алгоритми и структури от данни за това?


person stribika    schedule 02.08.2009    source източник
comment
Добър въпрос. Може да искате да разгледате кода за някои от IDE с отворен код, които прилагат това, като например Code::Blocks на codeblocks.org.   -  person    schedule 03.08.2009
comment
Ето статията за създаване на довършване на код в C# Създаване на довършване на код в C#   -  person Pritam Zope    schedule 01.02.2016


Отговори (3)


Механизмът 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. Всеки обект в моя кеш е в състояние да предостави функтор, който оценява на своите членове, така че извършването на сложни действия с дървото е почти тривиално.
  • Вместо всеки обект да пази List<IDeclaration> от своите членове, аз запазвам List<Name>, където Name е структура, съдържаща хеша на специално форматиран низ, описващ члена. Има огромен кеш, който картографира имена към обекти. По този начин, когато анализирам отново файл, мога да премахна всички елементи, декларирани във файла, от кеша и да го попълня отново с актуализираните членове. Поради начина, по който функторите са конфигурирани, всички изрази незабавно се оценяват на новите елементи.

IntelliSense „frontend“

Докато потребителят въвежда, файлът е синтактично неправилен по-често, отколкото е правилен. Като такъв, не искам случайно да премахвам секции от кеша, когато потребителят въвежда. Имам голям брой правила за специални случаи, за да обработвам инкременталните актуализации възможно най-бързо. Инкременталният кеш се съхранява само локално за отворен файл и помага да се гарантира, че потребителят не осъзнава, че неговото въвеждане кара задния кеш да съдържа неправилна информация за ред/колона за неща като всеки метод във файла.

  • Един изкупуващ фактор е, че анализаторът ми е бърз. Той може да се справи с пълна актуализация на кеша на изходен файл от 20 000 реда за 150 ms, докато работи самостоятелно във фонова нишка с нисък приоритет. Всеки път, когато този анализатор завърши успешно преминаване на отворен файл (синтактично), текущото състояние на файла се премества в глобалния кеш.
  • If the file is not syntactically correct, I use an ANTLR filter parser (sorry about the link - most info is on the mailing list or gathered from reading the source) to reparse the file looking for:
    • Variable/field declarations.
    • Сигнатурата за дефиниции на клас/структура.
    • Сигнатурата за дефинициите на метода.
  • В локалния кеш дефинициите на клас/структура/метод започват от сигнатурата и завършват, когато нивото на вложени скоби се върне обратно към равно. Методите могат също да завършат, ако се достигне друга декларация на метод (без методи за влагане).
  • В локалния кеш променливите/полетата са свързани с непосредствено предходния unclosed елемент. Вижте краткия кодов фрагмент по-долу за пример защо това е важно.
  • Also, as the user types, I keep a remap table marking the added/removed character ranges. This is used for:
    • Making sure I can identify the correct context of the cursor, since a method can/does move in the file between full parses.
    • Уверете се, че Go To Declaration/Definition/Reference локализира елементите правилно в отворените файлове.

Кодов фрагмент за предишния раздел:

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#)
  • Семантично правилно намиране на всички препратки
person Sam Harwell    schedule 02.08.2009
comment
Това е страхотно благодаря. Никога не съм се замислял за чувствителното отклонение при сортиране. Особено ми харесва, че можете да се справите с неподходящи брекети. - person stribika; 03.08.2009

Не мога да кажа какви точно алгоритми се използват от някоя конкретна реализация, но мога да направя някои обосновани предположения. trie е много полезна структура от данни за този проблем: IDE може да поддържа голям trie в паметта на всички символи във вашия проект, с някои допълнителни метаданни във всеки възел.

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

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

Изчисляването на trie (или други структури от данни) изисква анализиране на целия ви код, за да получите списък с всички символи във вашия проект. Visual Studio съхранява това в своята база данни IntelliSense, файл .ncb, съхраняван заедно с вашия проект, така че да не се налага да анализира всичко всеки път, когато затворите и отворите отново проекта си. Първият път, когато отворите голям проект (да речем, такъв, който току-що сте синхронизирали с източник контрол на формата), VS ще отдели време, за да анализира всичко и да генерира базата данни.

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

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

person Adam Rosenfield    schedule 02.08.2009
comment
Опитът е наистина добра идея. Що се отнася до постепенните промени, може да е възможно първо да се опитате да анализирате отново файла, когато това не работи, игнорирайте текущия ред и когато това не работи, игнорирайте обхващащия блок { ... }. Ако всичко друго се провали, използвайте последната база данни. - person stribika; 03.08.2009

Следната връзка ще ви помогне допълнително..

Маркиране на синтаксис: Бързо цветно текстово поле за подчертаване на синтаксис

person matrix    schedule 01.04.2012