Comment fonctionne la complétion de code?

84

De nombreux éditeurs et IDE ont une complétion de code. Certains d'entre eux sont très "intelligents", d'autres ne le sont pas vraiment. Je suis intéressé par le type le plus intelligent. Par exemple, j'ai vu des IDE qui n'offrent une fonction que si elle est a) disponible dans la portée actuelle b) sa valeur de retour est valide. (Par exemple, après "5 + foo [tab]", il ne propose que des fonctions qui renvoient quelque chose qui peut être ajouté à un nom entier ou variable du type correct.) J'ai également vu qu'ils placent en avant l'option la plus souvent utilisée ou la plus longue de la liste.

Je réalise que vous devez analyser le code. Mais généralement, lors de l'édition du code actuel n'est pas valide, il y a des erreurs de syntaxe. Comment analysez-vous quelque chose lorsqu'il est incomplet et contient des erreurs?

Il y a aussi une contrainte de temps. La complétion est inutile si cela prend quelques secondes pour arriver à une liste. Parfois, l'algorithme d'achèvement traite des milliers de classes.

Quels sont les bons algorithmes et structures de données pour cela?

stribika
la source
1
Une bonne question. Vous voudrez peut-être jeter un œil au code de certains des IDE open source qui implémentent cela, tels que Code :: Blocks sur codeblocks.org .
1
Voici l'article pour créer l'achèvement du code en C # Création de l'achèvement du code en C #
Pritam Zope

Réponses:

64

Le moteur IntelliSense de mon produit de service de langage UnrealScript est compliqué, mais je vais vous donner le meilleur aperçu que je peux ici. Le service de langage C # dans VS2008 SP1 est mon objectif de performances (pour une bonne raison). Ce n'est pas encore là, mais il est suffisamment rapide / précis pour que je puisse proposer en toute sécurité des suggestions après la saisie d'un seul caractère, sans attendre que ctrl + espace ou que l'utilisateur tape un .(point). Plus les gens [travaillant sur les services linguistiques] obtiennent d'informations sur ce sujet, meilleure sera l'expérience de l'utilisateur final si j'utilise leurs produits. Il y a un certain nombre de produits avec lesquels j'ai eu la malheureuse expérience de travailler qui ne prêtaient pas autant d'attention aux détails, et par conséquent, je me battais plus avec l'IDE que je ne codais.

Dans mon service linguistique, il est présenté comme suit:

  1. Récupère l'expression au niveau du curseur. Cela marche du début de l' expression d'accès aux membres jusqu'à la fin de l'identificateur sur lequel se trouve le curseur. L'expression d'accès aux membres est généralement au format aa.bb.cc, mais peut également contenir des appels de méthode comme dans aa.bb(3+2).cc.
  2. Obtenez le contexte entourant le curseur. C'est très délicat, car il ne suit pas toujours les mêmes règles que le compilateur (longue histoire), mais pour ici supposons que c'est le cas. Généralement, cela signifie obtenir les informations mises en cache sur la méthode / classe dans laquelle se trouve le curseur.
  3. Dites les implémentations d'objet de contexte IDeclarationProvider, où vous pouvez appeler GetDeclarations()pour obtenir un IEnumerable<IDeclaration>de tous les éléments visibles dans la portée. Dans mon cas, cette liste contient les locals / paramètres (si dans une méthode), les membres (champs et méthodes, statiques uniquement sauf dans une méthode d'instance, et aucun membre privé des types de base), globals (types et constantes pour la langue I je travaille sur) et des mots-clés. Dans cette liste sera un élément avec le nom aa. Comme première étape dans l'évaluation de l'expression dans # 1, nous sélectionnons l'élément de l'énumération de contexte avec le nom aa, nous donnant un IDeclarationpour l'étape suivante.
  4. Ensuite, j'applique l'opérateur au IDeclarationreprésentant aapour en obtenir un autre IEnumerable<IDeclaration>contenant les "membres" (dans un certain sens) de aa. Puisque l' .opérateur est différent de l' ->opérateur, j'appelle declaration.GetMembers(".")et j'attends que l' IDeclarationobjet applique correctement l'opérateur répertorié.
  5. Cela continue jusqu'à ce que je frappe cc, où la liste de déclaration peut ou non contenir un objet avec le nom cc. Comme je suis sûr que vous le savez, si plusieurs éléments commencent par cc, ils devraient également apparaître. Je résous ce problème en prenant l'énumération finale et en la passant par mon algorithme documenté pour fournir à l'utilisateur les informations les plus utiles possible.

Voici quelques remarques supplémentaires pour le backend IntelliSense:

  • J'utilise largement les mécanismes d'évaluation paresseux de LINQ dans la mise en œuvre GetMembers. Chaque objet de mon cache est capable de fournir un foncteur qui évalue à ses membres, donc effectuer des actions compliquées avec l'arborescence est presque trivial.
  • Au lieu que chaque objet garde un List<IDeclaration>de ses membres, je garde a List<Name>, où Nameest une structure contenant le hachage d'une chaîne spécialement formatée décrivant le membre. Il y a un énorme cache qui mappe les noms aux objets. De cette façon, lorsque je ré-analyse un fichier, je peux supprimer tous les éléments déclarés dans le fichier du cache et le remplir à nouveau avec les membres mis à jour. En raison de la façon dont les foncteurs sont configurés, toutes les expressions sont immédiatement évaluées par rapport aux nouveaux éléments.

"Interface" IntelliSense

Au fur et à mesure que l'utilisateur tape, le fichier est syntaxiquement incorrect plus souvent qu'il n'est correct. En tant que tel, je ne veux pas supprimer au hasard des sections du cache lorsque l'utilisateur tape. J'ai mis en place un grand nombre de règles de cas spéciaux pour gérer les mises à jour incrémentielles le plus rapidement possible. Le cache incrémentiel est uniquement conservé localement dans un fichier ouvert et permet de s'assurer que l'utilisateur ne se rend pas compte que sa saisie amène le cache principal à contenir des informations de ligne / colonne incorrectes pour des éléments comme chaque méthode du fichier.

  • Un facteur de rachat est que mon analyseur est rapide . Il peut gérer une mise à jour complète du cache d'un fichier source de 20000 lignes en 150 ms tout en fonctionnant de manière autonome sur un thread d'arrière-plan de faible priorité. Chaque fois que cet analyseur effectue une passe sur un fichier ouvert avec succès (syntaxiquement), l'état actuel du fichier est déplacé dans le cache global.
  • Si le fichier n'est pas syntaxiquement correct, j'utilise un analyseur de filtre ANTLR (désolé pour le lien - la plupart des informations sont sur la liste de diffusion ou recueillies à partir de la lecture de la source) pour analyser le fichier à la recherche:
    • Déclarations de variables / champs.
    • La signature pour les définitions de classe / structure.
    • La signature des définitions de méthode.
  • Dans le cache local, les définitions de classe / structure / méthode commencent à la signature et se terminent lorsque le niveau d'imbrication d'accolades revient à pair. Les méthodes peuvent également se terminer si une autre déclaration de méthode est atteinte (pas de méthodes d'imbrication).
  • Dans le cache local, les variables / champs sont liés à l' élément non fermé immédiatement précédent . Voir le bref extrait de code ci-dessous pour un exemple de pourquoi c'est important.
  • De plus, au fur et à mesure que l'utilisateur tape, je garde une table de remappage marquant les plages de caractères ajoutées / supprimées. Ceci est utilisé pour:
    • S'assurer que je peux identifier le contexte correct du curseur, car une méthode peut / se déplace dans le fichier entre des analyses complètes.
    • S'assurer que Aller à la déclaration / définition / référence localise correctement les éléments dans les fichiers ouverts.

Extrait de code de la section précédente:

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

J'ai pensé que j'ajouterais une liste des fonctionnalités IntelliSense que j'ai implémentées avec cette mise en page. Des photos de chacun se trouvent ici.

  • Saisie automatique
  • Conseils d'outils
  • Conseils de méthode
  • Vue de classe
  • Fenêtre de définition de code
  • Navigateur d'appels (VS 2010 ajoute enfin ceci à C #)
  • Sémantiquement correct Rechercher toutes les références
Sam Harwell
la source
C'est super merci. Je n'ai jamais pensé au biais sensible à la casse lors du tri. J'aime particulièrement le fait que vous puissiez gérer des accolades dépareillées.
stribika
15

Je ne peux pas dire exactement quels algorithmes sont utilisés par une implémentation particulière, mais je peux faire des suppositions éclairées. Un trie est une structure de données très utile pour ce problème: l'EDI peut conserver un grand trie en mémoire de tous les symboles de votre projet, avec quelques métadonnées supplémentaires à chaque nœud.

Lorsque vous tapez un caractère, il emprunte un chemin dans le trie. Tous les descendants d'un nœud de trie particulier sont des complétions possibles. L'EDI a alors juste besoin de filtrer ceux qui ont du sens dans le contexte actuel, mais il a seulement besoin d'en calculer autant que possible dans la fenêtre contextuelle de complétion des onglets.

La complétion par tabulation plus avancée nécessite un tri plus compliqué. Par exemple, Visual Assist X a une fonction dans laquelle vous n'avez qu'à taper les lettres majuscules des symboles CamelCase - par exemple, si vous tapez SFN, il vous montre le symbole SomeFunctionNamedans sa fenêtre de complétion par tabulation.

Le calcul du trie (ou d'autres structures de données) nécessite l'analyse de tout votre code pour obtenir une liste de tous les symboles de votre projet. Visual Studio le stocke dans sa base de données IntelliSense, un .ncbfichier stocké à côté de votre projet, afin qu'il n'ait pas à tout analyser à chaque fois que vous fermez et rouvrez votre projet. La première fois que vous ouvrez un grand projet (par exemple, celui que vous venez de synchroniser avec le contrôle de code source), VS prendra le temps de tout analyser et de générer la base de données.

Je ne sais pas comment il gère les changements incrémentiels. Comme vous l'avez dit, lorsque vous écrivez du code, sa syntaxe est invalide 90% du temps, et tout réanalyser chaque fois que vous êtes inactif imposerait une énorme taxe sur votre processeur pour très peu d'avantages, surtout si vous modifiez un fichier d'en-tête inclus par un grand nombre de fichiers source.

Je soupçonne que soit (a) il ne répète que chaque fois que vous construisez votre projet (ou peut-être lorsque vous le fermez / ouvrez), ou (b) il effectue une sorte d'analyse locale où il analyse uniquement le code là où vous venez de édité de manière limitée, juste pour obtenir les noms des symboles pertinents. Étant donné que C ++ a une grammaire extrêmement compliquée, il peut se comporter de manière étrange dans les coins sombres si vous utilisez une métaprogrammation de modèles lourds, etc.

Adam Rosenfield
la source
Le trie est une très bonne idée. En ce qui concerne les modifications incrémentielles, il peut être possible d'essayer d'abord de réanalyser le fichier lorsque cela ne fonctionne pas, ignorez la ligne actuelle et lorsque cela ne fonctionne pas, ignorez le bloc {...} englobant. Si tout le reste échoue, utilisez la dernière base de données.
stribika