Comment ajouter un langage
Ce guide montre comment apprendre à un éditeur bâti sur turbo-core à colorer un langage. Il suppose que vous avez déjà un éditeur — sinon, construisez-en un d'abord.
turbo-core colore lui-même TOML, YAML, Markdown, JavaScript, HTML, XML, les Dockerfiles et le shell. Le langage pour lequel votre éditeur existe, c'est à vous de l'enregistrer : c'est ce qui fait que Turbo Go colore Go et que Turbo Rust colore Rust.
Étapes
1. Décider comment le langage est reconnu
Un fichier est reconnu d'abord par son extension, et seulement à défaut par sa première ligne :
syntax.Definition{
Language: "zig",
Extensions: []string{".zig"}, // avec le point, en minuscules
Filenames: []string{"build.zig"}, // pour les fichiers sans extension utile
Shebangs: []string{"zig"}, // la plupart des langages n'en ont pas
Highlight: highlightZig,
}
Language est le nom sous lequel le langage est connu partout ailleurs : c'est ce que renvoie LanguageOf, et ce qu'un utilisateur écrit dans la clé languages d'un fichier d'extraits. Gardez-le en minuscules, et gardez-le stable.
Un fichier est reconnu par son extension, puis par son nom, puis par sa première ligne. Filenames sert aux fichiers sans extension exploitable — un Dockerfile, un Makefile. La correspondance se fait sur le nom entier ou sur la partie précédant le premier point, sans tenir compte de la casse : lister "Dockerfile" reconnaît donc aussi Dockerfile.dev sans avoir à nommer chaque variante qu'un projet pourrait inventer.
2. Écrire l'analyseur
Un analyseur prend le document entier et renvoie une tranche de segments par ligne. ScanLines découpe les lignes et fait passer d'une ligne à l'autre ce qui traverse un saut de ligne :
func highlightZig(src string) [][]syntax.Span {
return syntax.ScanLines(src, func(line []rune, inComment bool) ([]syntax.Span, bool) {
s := syntax.NewLineScanner(line)
// … colorer la ligne, en mettant inComment à jour au passage …
return s.Spans(), inComment
})
}
Dans la fonction, demandez à s.Peek(0) ce qui se trouve là et prenez-le :
| Pour colorer | Appelez |
|---|---|
| un commentaire jusqu'à la fin de la ligne | s.TakeRest(syntax.ClassComment) |
| une chaîne entre guillemets se fermant sur cette ligne | syntax.TakeQuoted(s, '"', syntax.ClassString) |
| un commentaire de bloc qui s'ouvre ici | syntax.OpenBlockComment(s, "/*", "*/", syntax.ClassComment) |
| la suite d'un commentaire ouvert plus tôt | syntax.FinishBlockComment(s, "*/", syntax.ClassComment) |
| une suite de runes qui correspondent | s.TakeWhile(syntax.ClassNumber, syntax.IsDigit) |
| un nombre fixe de runes | s.Take(2, syntax.ClassOperator) |
| des espaces, sans les colorer | s.SkipSpaces() |
| n'importe quoi d'autre, sans le colorer | s.Advance(1) |
La liste complète est dans la référence syntaxique.
3. L'enregistrer
func Register() {
syntax.Register(syntax.Definition{ /* … */ })
}
Appelez cela depuis le main de votre commande, avant qu'aucun fichier ne soit ouvert. Faites-le explicitement plutôt que depuis une fonction init, pour que « cet éditeur connaît Zig » soit une ligne que quelqu'un peut lire.
4. Le tester
Pilotez l'analyseur par syntax.Highlight, qui est le chemin que prend l'éditeur :
func TestKeywordsAreColoured(t *testing.T) {
Register()
spans := syntax.Highlight("zig", "const x = 1;")
if spans[0][0].Class != syntax.ClassKeyword {
t.Errorf("const est %v, on veut keyword", spans[0][0].Class)
}
}
Trois choses méritent leur propre test, parce que chacune est un vrai bug déjà survenu ici :
- Une entrée par ligne. L'éditeur indexe le résultat par numéro de ligne sans vérifier : un résultat trop court, c'est un dépassement d'indice au milieu d'un redessin.
- Des segments ordonnés et sans recouvrement. Ils sont dessinés dans l'ordre ; deux segments désordonnés se repeignent l'un sur l'autre.
- Une source cassée reste colorée. Le code sous le curseur est invalide la plupart du temps pendant qu'on le tape.
Variantes
Le langage a déjà un analyseur lexical
Si votre langage fournit un lexeur — go/scanner pour Go, par exemple — utilisez-le et convertissez ses décalages en octets avec LineIndex au lieu d'écrire un analyseur ligne à ligne :
func highlightGo(src string) [][]syntax.Span {
lines := syntax.NewLineIndex(src)
out := make([][]syntax.Span, lines.Count())
for _, token := range tokenise(src) {
lines.AppendSpans(out, token.start, token.end, classOf(token))
}
return out
}
AppendSpans découpe en un segment par ligne une plage qui traverse un saut de ligne — ce qu'un segment n'a jamais le droit de faire.
Quelque chose traverse un saut de ligne
Mettez-le dans l'état que ScanLines fait circuler. Utilisez une valeur, pas un drapeau, quand la construction s'imbrique : les commentaires de bloc de Rust s'imbriquent, donc Turbo Rust transporte une profondeur, et un bool fermerait un commentaire imbriqué un niveau trop tôt.
Votre langage a besoin d'une couleur que rien d'autre n'utilise
C'est probablement faux. Les classes sont fixées exprès, pour qu'un seul thème colore tous les langages qu'un éditeur apprendra jamais. Le balisage a réclamé cinq classes supplémentaires (ClassHeading, ClassTag, ClassAttribute, ClassEmphasis, ClassLink) parce qu'un titre n'est réellement pas un mot-clé ; servez-vous d'abord de celles-là avant d'en demander une sixième.
Vous voulez remplacer un langage intégré
Enregistrez votre propre définition avec le même Language. L'enregistrement le plus récent gagne, parce que c'est l'affirmation la plus précise des deux.
Voir aussi
- Tout ce qu'offre la boîte à outils : référence syntaxique
- Pourquoi la liste des classes est fermée : ce qui appartient ici
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
|