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 la modification du code actuel n'est pas valide, il contient 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?
Le moteur IntelliSense de mon produit de service de langage UnrealScript est compliqué, mais je vais 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 assez 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 n'ont pas prêté une telle attention aux détails, et par conséquent je me battais avec le IDE plus que moi codage.
Dans mon service linguistique, il est présenté comme suit:
aa.bb.cc
, Mais peut également contenir des appels de méthode comme dans aa.bb(3+2).cc
.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 IDeclaration
pour l'étape suivante.IDeclaration
représentant aa
pour 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 je m'attends à ce que l'objet IDeclaration
applique correctement l'opérateur répertorié.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 mon algorithme documenté pour fournir à l'utilisateur les informations les plus utiles possibles.Voici quelques remarques supplémentaires pour le backend IntelliSense:
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.List<IDeclaration>
De ses membres, je garde un List<Name>
, Où Name
est 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.
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. Les photos de chacun se trouvent ici.
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. A trie est une structure de données très utile pour ce problème: le IDE peut maintenir 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. Le IDE 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 ce qui peut être affiché dans la fenêtre pop-up de complétion de tabulation .
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 SomeFunctionName
dans son fenêtre de saisie semi-automatique.
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 .ncb
fichier 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, la 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 qu'il (a) ne répète que chaque fois que vous construisez réellement votre projet (ou peut-être lorsque vous le fermez/l'ouvrez), ou (b) il effectue une sorte d'analyse locale où il analyse uniquement le code là où vous venez de édité de façon 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 bizarrement dans les coins sombres si vous utilisez une métaprogrammation de modèles lourds, etc.
Le lien suivant vous aidera davantage.
Mise en évidence de la syntaxe: Zone de texte couleur rapide pour la mise en évidence de la syntaxe