Come funziona il completamento del codice?


84

Molti editor e IDE hanno il completamento del codice. Alcuni di loro sono molto "intelligenti" altri in realtà non lo sono. Mi interessa il tipo più intelligente. Ad esempio, ho visto IDE che offrono una funzione solo se a) è disponibile nell'ambito corrente b) il suo valore di ritorno è valido. (Ad esempio dopo "5 + foo [tab]" offre solo funzioni che restituiscono qualcosa che può essere aggiunto a un numero intero o nomi di variabili del tipo corretto.) Ho anche visto che mettono in primo piano l'opzione usata più spesso o più lunga della lista.

Mi rendo conto che devi analizzare il codice. Ma di solito mentre si modifica il codice corrente non è valido, ci sono errori di sintassi in esso. Come si analizza qualcosa quando è incompleto e contiene errori?

C'è anche un vincolo di tempo. Il completamento è inutile se ci vogliono pochi secondi per creare un elenco. A volte l'algoritmo di completamento si occupa di migliaia di classi.

Quali sono i buoni algoritmi e strutture dati per questo?


1
Una bella domanda. Potresti voler dare un'occhiata al codice per alcuni degli IDE open source che lo implementano, come Code :: Blocks su codeblocks.org .

1
Ecco l'articolo per creare il completamento del codice in C # Creazione del completamento del codice in C #
Pritam Zope

Risposte:


65

Il motore IntelliSense nel mio prodotto di servizio in linguaggio UnrealScript è complicato, ma qui fornirò la migliore panoramica possibile. Il servizio in linguaggio C # in VS2008 SP1 è il mio obiettivo di prestazioni (per una buona ragione). Non è ancora lì, ma è abbastanza veloce / preciso da poter offrire suggerimenti in modo sicuro dopo aver digitato un singolo carattere, senza aspettare ctrl + spazio o l'utente che digita un .(punto). Più informazioni le persone [che lavorano sui servizi linguistici] ottengono su questo argomento, migliore sarà l'esperienza dell'utente finale se dovessi usare i loro prodotti. Ci sono un certo numero di prodotti con cui ho avuto la sfortunata esperienza di lavorare che non hanno prestato molta attenzione ai dettagli, e di conseguenza stavo combattendo con l'IDE più di quanto stavo codificando.

Nel mio servizio linguistico, è strutturato come segue:

  1. Ottieni l'espressione al cursore. Questo va dall'inizio dell'espressione di accesso del membro alla fine dell'identificatore su cui si trova il cursore. L'espressione di accesso ai membri è generalmente nel formato aa.bb.cc, ma può anche contenere chiamate a metodi come in aa.bb(3+2).cc.
  2. Ottieni il contesto che circonda il cursore. Questo è molto complicato, perché non segue sempre le stesse regole del compilatore (storia lunga), ma qui supponiamo che lo faccia. Generalmente questo significa ottenere le informazioni memorizzate nella cache sul metodo / classe in cui si trova il cursore.
  3. Supponiamo che l'oggetto di contesto implementi IDeclarationProvider, dove puoi chiamare GetDeclarations()per ottenere uno IEnumerable<IDeclaration>di tutti gli elementi visibili nell'ambito. Nel mio caso, questo elenco contiene le variabili locali / parametri (se in un metodo), membri (campi e metodi, solo statici a meno che non si tratti di un metodo di istanza e nessun membro privato di tipi di base), globali (tipi e costanti per il linguaggio I ci sto lavorando) e le parole chiave. In questo elenco ci sarà un elemento con il nome aa. Come primo passo nella valutazione dell'espressione in # 1, selezioniamo l'elemento dall'enumerazione del contesto con il nome aa, dandoci una IDeclarationper il passaggio successivo.
  4. Successivamente, applico l'operatore alla IDeclarationrappresentazione aaper ottenerne un altro IEnumerable<IDeclaration>contenente i "membri" (in un certo senso) di aa. Poiché l' .operatore è diverso ->dall'operatore, chiamo declaration.GetMembers(".")e mi aspetto che l' IDeclarationoggetto applichi correttamente l'operatore elencato.
  5. Questo continua finché non premo cc, dove l'elenco delle dichiarazioni può contenere o meno un oggetto con il nome cc. Come sicuramente saprai, se più elementi iniziano con cc, dovrebbero apparire anche loro. Risolvo questo problema prendendo l'enumerazione finale e passandola attraverso il mio algoritmo documentato per fornire all'utente le informazioni più utili possibili.

Ecco alcune note aggiuntive per il backend IntelliSense:

  • Faccio ampio uso dei meccanismi di valutazione lazy di LINQ nell'implementazione GetMembers. Ogni oggetto nella mia cache è in grado di fornire un funtore che valuta i suoi membri, quindi eseguire azioni complicate con l'albero è quasi banale.
  • Invece di ogni oggetto che mantiene uno List<IDeclaration>dei suoi membri, mantengo a List<Name>, dove Nameè una struttura contenente l'hash di una stringa appositamente formattata che descrive il membro. C'è un'enorme cache che mappa i nomi sugli oggetti. In questo modo, quando rianalizzo un file, posso rimuovere tutti gli elementi dichiarati nel file dalla cache e ripopolarlo con i membri aggiornati. A causa del modo in cui sono configurati i funtori, tutte le espressioni valutano immediatamente i nuovi elementi.

IntelliSense "frontend"

Mentre l'utente digita, il file è sintatticamente errato più spesso di quanto non sia corretto. In quanto tale, non voglio rimuovere a casaccio sezioni della cache quando l'utente digita. Ho un gran numero di regole per casi speciali in atto per gestire gli aggiornamenti incrementali il più rapidamente possibile. La cache incrementale viene mantenuta locale solo in un file aperto e aiuta a garantire che l'utente non si renda conto che la sua digitazione sta facendo sì che la cache di backend contenga informazioni di riga / colonna errate per cose come ogni metodo nel file.

  • Un fattore positivo è che il mio parser è veloce . È in grado di gestire un aggiornamento completo della cache di un file sorgente di 20000 linee in 150 ms durante il funzionamento autonomo su un thread in background a bassa priorità. Ogni volta che questo parser completa con successo un passaggio su un file aperto (sintatticamente), lo stato corrente del file viene spostato nella cache globale.
  • Se il file non è sintatticamente corretto, utilizzo un analizzatore di filtri ANTLR (mi dispiace per il collegamento - la maggior parte delle informazioni è sulla mailing list o raccolte dalla lettura della fonte) per analizzare il file cercando:
    • Dichiarazioni di variabili / campi.
    • La firma per le definizioni di classe / struttura.
    • La firma per le definizioni dei metodi.
  • Nella cache locale, le definizioni di classe / struttura / metodo iniziano alla firma e terminano quando il livello di annidamento delle parentesi graffe torna alla pari. I metodi possono anche terminare se viene raggiunta un'altra dichiarazione di metodo (nessun metodo di annidamento).
  • Nella cache locale, le variabili / i campi sono collegati all'elemento non chiuso immediatamente precedente . Vedere il breve snippet di codice di seguito per un esempio del motivo per cui è importante.
  • Inoltre, mentre l'utente digita, tengo una tabella di rimappatura che contrassegna gli intervalli di caratteri aggiunti / rimossi. Questo è usato per:
    • Assicurandomi di poter identificare il contesto corretto del cursore, poiché un metodo può / si sposta nel file tra le analisi complete.
    • Assicurarsi che Vai a dichiarazione / definizione / riferimento localizzi correttamente gli elementi nei file aperti.

Snippet di codice per la sezione precedente:

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

Ho pensato di aggiungere un elenco delle funzionalità di IntelliSense che ho implementato con questo layout. Le immagini di ciascuno si trovano qui.

  • Completamento automatico
  • Suggerimenti sugli strumenti
  • Suggerimenti sul metodo
  • Visualizzazione di classe
  • Finestra di definizione del codice
  • Call Browser (VS 2010 lo aggiunge finalmente a C #)
  • Trova tutti i riferimenti semanticamente corretto

Questo è un grande grazie. Non ho mai pensato alla distinzione tra maiuscole e minuscole durante l'ordinamento. Mi piace particolarmente il fatto che tu possa gestire le parentesi graffe non corrispondenti.
stribika

16

Non posso dire esattamente quali algoritmi vengono utilizzati da una particolare implementazione, ma posso fare alcune ipotesi plausibili. Un trie è una struttura dati molto utile per questo problema: l'IDE può mantenere un grande trie in memoria di tutti i simboli nel tuo progetto, con alcuni metadati extra in ogni nodo.

Quando digiti un carattere, questo segue un percorso nel trie. Tutti i discendenti di un particolare nodo trie sono possibili completamenti. L'IDE ha quindi solo bisogno di filtrare quelli fuori da quelli che hanno senso nel contesto corrente, ma ha solo bisogno di calcolarne quanti possono essere visualizzati nella finestra pop-up di completamento con tabulazione.

Il completamento con tabulazione più avanzato richiede un trie più complicato. Ad esempio, Visual Assist X ha una funzione per cui devi solo digitare le lettere maiuscole dei simboli CamelCase - ad esempio, se digiti SFN, ti mostra il simbolo SomeFunctionNamenella sua finestra di completamento con tabulazioni.

Il calcolo del trie (o di altre strutture dati) richiede l'analisi di tutto il codice per ottenere un elenco di tutti i simboli nel progetto. Visual Studio lo archivia nel proprio database IntelliSense, un .ncbfile archiviato insieme al progetto, in modo che non debba analizzare tutto ogni volta che si chiude e si riapre il progetto. La prima volta che apri un progetto di grandi dimensioni (ad esempio, uno che hai appena sincronizzato con il controllo del codice sorgente del modulo), VS impiegherà del tempo per analizzare tutto e generare il database.

Non so come gestisce i cambiamenti incrementali. Come hai detto, quando scrivi codice, la sintassi non è valida il 90% delle volte, e ripassare tutto ogni volta che sei inattivo comporterebbe un'enorme tassa sulla tua CPU con un vantaggio minimo, specialmente se stai modificando un file di intestazione incluso da un gran numero di file sorgente.

Ho il sospetto che (a) ripara solo ogni volta che costruisci effettivamente il tuo progetto (o forse quando lo chiudi / lo apri), o (b) esegue una sorta di analisi locale in cui analizza solo il codice dove hai appena modificato in modo limitato, solo per ottenere i nomi dei simboli pertinenti. Dal momento che il C ++ ha una grammatica così straordinariamente complicata, potrebbe comportarsi in modo strano negli angoli bui se stai usando una metaprogrammazione di modelli pesanti e simili.


Il trie è davvero una buona idea. Per quanto riguarda le modifiche incrementali, potrebbe essere possibile prima provare a rieseguire il parsing del file quando non funziona ignorare la riga corrente e quando non funziona ignorare il blocco {...} che lo racchiude. Se tutto il resto fallisce, usa l'ultimo database.
stribika

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.