turbo-editors/turbo-corepublic Fork 0
v1.0.1
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated. 28d5985 · on v1.0.1 · k33g · 14h ago
add-a-language.md · 123 lines · 6.0 KBmarkdown
Blame HistoryOpen raw

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

  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
# 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](../tutorials/build-an-editor.md).

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 :

```go
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 :

```go
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](../reference/syntax.md).

### 3. L'enregistrer

```go
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 :

```go
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 :

```go
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](../reference/syntax.md)
- Pourquoi la liste des classes est fermée : [ce qui appartient ici](../explanation/what-belongs-here.md)