Outils pour utilisateurs

Outils du site


prog:vsc:module_highlight

Prérequis

La documentation est Extension API de Visual Studio Code. Archive du 11/12/2020 le 01/02/2021

Node.js

Il faut installer Node.js qui possède une application pour créer un squelette de module.

Installer les programmes yo et generator-code par :

npm install -g yo generator-code

Création du module

Générer le module de base pour la coloration syntaxique :

yo code

Résultat de la console :

? ==========================================================================
We're constantly looking for ways to make yo better! 
May we anonymously report usage statistics to improve the tool over time? 
More info: https://github.com/yeoman/insight & http://yeoman.io
========================================================================== Yes
     _-----_     ╭──────────────────────────╮
    |       |    │   Welcome to the Visual  │
    |--(o)--|    │   Studio Code Extension  │
   `---------´   │        generator!        │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

? What type of extension do you want to create? New Language Support
Enter the URL (http, https) or the file path of the tmLanguage grammar or press ENTER to start with a new grammar.
? URL or file to import, or none for new:
? What's the name of your extension? CELLman Lang
? What's the identifier of your extension? cellman-lang
? What's the description of your extension? Syntax highlighting for CELLman lang
Enter the id of the language. The id is an identifier and is single, lower-case name such as 'php', 'javascript'
? Language id: cellman
Enter the name of the language. The name will be shown in the VS Code editor mode selector.
? Language name: CELLman
Enter the file extensions of the language. Use commas to separate multiple entries (e.g. .ruby, .rb)
? File extensions: .cmc, .TRAN
Enter the root scope name of the grammar (e.g. source.ruby)
? Scope names: source.cmc
? Initialize a git repository? Yes
   create cellman-lang\syntaxes\cellman.tmLanguage.json
   create cellman-lang\.vscode\launch.json
   create cellman-lang\package.json
   create cellman-lang\README.md
   create cellman-lang\CHANGELOG.md
   create cellman-lang\vsc-extension-quickstart.md
   create cellman-lang\language-configuration.json
   create cellman-lang\.vscodeignore
   create cellman-lang\.gitignore
   create cellman-lang\.gitattributes

Your extension cellman-lang has been created!

To start editing with Visual Studio Code, use the following commands:

     cd cellman-lang
     code .

Open vsc-extension-quickstart.md inside the new extension for further instructions
on how to modify, test and publish your extension.

For more information, also visit http://code.visualstudio.com and follow us @code.

Syntaxe des fichiers

package.json

Dans ce fichier, il faut uniquement modifier la partie contributes (documentation Extension points).

languages

{
    "contributes": {
        "languages": [{
            "id": "cellman_cmc",
            "aliases": ["CELLman", "cellman"],
            "extensions": [".cmc", ".TRAN"],
            "configuration": "./language-configuration.json",
            "filenames": [],
            "firstLine": "..."
        }],
        "grammars": ...
    }
}
  • languages, on définit un tableau de langages identifié par un id.
  • aliases : le premier alias servira de label.
  • extensions : filtreront les fichiers où s'appliquera la coloration syntaxique. Chaque extension unique ne doit être que dans un seul langage. Si une extension est présente dans deux langages, l'un des deux sera simplement ignoré.
  • filenames : c'est un filtre qui prend en compte le nom du fichier et pas uniquement son extension. C'est utile pour les fichiers sans extension.
  • firstLine : on applique une expression régulière sur le contenu de la première ligne du fichier.

grammars

{
    "contributes": {
        "languages": ...,
        "grammars": [{
            "language": "cellman_cmc",
            "scopeName": "source.common",
            "path": "./syntaxes/cellman.tmLanguage.common.json"
        },
        {
            "scopeName": "source.TRAN",
            "path": "./syntaxes/cellman.tmLanguage.TRAN.json",
            "injectTo": [ "source.common" ]
        }]
    }
}

Il y a deux façons : via l'id du langage ou via une extension du langage.

On peut utiliser le champ language et lui attribuer le champ id précédemment définit via languages.language. Les fichiers concernés seront ceux qui respecteront extensions, filenames ou firstLine. Le champ scopeName devra correspondre au champ scopeName dans le fichier définit par l'attribut path.

Il est aussi possible d'étendre un langage existant. Pour cela, il faut connaître le nom de la coloration syntaxique attribué au texte. Cette information peut s'afficher en saisissant la commande Developer: Inspect Editor Tokens and Scopes via CTRL+SHIFT+P. On note alors ce nom dans le tableau injectTo. La couleur pourra alors être surchargée par les règles définies dans le fichier path.

xxx.tmLanguage.json

Voir Language Grammars pour la syntaxe.

Arborescence de base

{
    "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
    "name": "CELLman",
    "patterns": [
        {
            "include": "#comments"
        }
    ],
    "repository": {
        "comments": ...
    },
    "scopeName": "source.common"
}

avec 3 champs à personnaliser :

  • patterns : ce tableau ne contient que les noms qui font référence aux noms contenu dans le champ repository. En pratique il peut contenir d'autres données mais l'utilisation exclusive du include est une bonne pratique du générateur automatique.
  • repository : contient la liste des patterns dont le nom est défini précédemment.
  • scopeName : doit être le même que celui définit dans le fichier package.json à la rubrique grammars.

pattern

Chaque repository contient un dictionnaire de pattern.

  • match

Il est possible de ne définir que le pattern avec son type de contenu. Ici, on repère 4 mots clés (if, while, for, return).

Le nom doit impérativement respecter la syntaxe de TextMate (§12.4 Naming Conventions). Ici, keyword.control signifie « mainly related to flow control ». Ensuite, on rajoute un dernier . avec l'abréviation du langage.

        "keywords": {
            "patterns": [{
                "name": "keyword.control.cell",
                "match": "\\b(if|while|for|return)\\b"
            }]
        },
  • begin et end

Dans l'autre exemple ci-dessous, le symbole " et " désigne le début (begin) et la fin (end) d'un texte. Là où l'utilisation de match se limite à une seule ligne, begin et end peut être sur une ou plusieurs lignes. Ce texte aura comme coloration syntaxique string.quoted.double.

Ensuite, à l'intérieur de ce texte délimité par begin et end, on cherche tous les patterns \\. (tous les caractères d'échappement) et on lui applique la coloration syntaxique constant.character.escape.

Il y a aussi un double intérêt. Si le caractère d'échappement \" apparait, le guillement sera considéré comme un pattern et ne pourra pas être considéré par la contrainte end. Textmate lisant de gauche à droite et le pattern commençant un caratère avant, patterns sera prioritaire au end. Par contre, si patterns et end commencent au même caratère, c'est end qui sera prioritaire.

        "strings": {
            "name": "string.quoted.double.cell",
            "begin": "\"",
            "end": "\"",
            "patterns": [
                {
                    "name": "constant.character.escape.cell",
                    "match": "\\\\."
                }
            ]
        }

Il est aussi possible de colorer en fonction des groupes de l'expression régulière via captures :

        "strings": {
            "name": "string.quoted.double.cell",
            "match": "\\\"(.*)\\\"",
            "captures": {
                "1": {
                    "name": "string.inside.cell"
                }
            }
        }

Il est aussi possible d'utiliser beginCaptures et endCaptures sur les textes correspondant aux expressions régulières de begin et end.

En plus de name, il existe contentName. Si on applique name à un pattern, il s'appliquera au texte désigné par begin, end et le texte au milieu. Par contre, contentName ne va définir le nom que pour le texte entre les textes désignés par begin et end. Si les deux sont appliqués, c'est le nom définit par contentName qui sera prioritaire.

Il est aussi possible d'utiliser begin et end et de réutiliser leurs contenus dans pattern via les Positive and Negative Lookahead.

Le pattern de récursion ne fonctionnera plus.

"identifiant": {
    "begin": "(?=^\\s*(if)\\s)",
    "end": "(?<=^\\s*end_if\\s*$)",
    "name": "meta.if.definition.cellman.TRAN",
    "pattern": [...]
},

Bonnes pratiques

Principe

L'idéal est d'avoir la sémantique du langage et de le convertir en expression régulière dans un format compatible.

Il est donc préférable d'identifier chaque type de ligne et de construire les expressions régulières correspondantes.

Exemple

  • package.json
package.json
{
    "name": "cell-lang",
    "displayName": "Cell Lang",
    "description": "Syntax highlight for cell lang",
    "version": "0.0.1",
    "engines": {
        "vscode": "^1.52.0"
    },
    "categories": [
        "Programming Languages"
    ],
    "contributes": {
        "languages": [
            {
                "id": "lang",
                "aliases": [
                    "lang",
                    "lang"
                ],
                "extensions": [
                    "lang"
                ],
                "configuration": "./language-configuration.json"
            }
        ],
        "grammars": [
            {
                "language": "lang",
                "scopeName": "source.lang",
                "path": "./syntaxes/lang.tmLanguage.json"
            }
        ]
    }
}
  • lang.tmLanguage.json

Le schéma à respecter est tmlanguage.json.

Il est important de comprendre le fichier .json en exemple, il contient des explications sur comment rédiger les règles en respectant la sémantique et aussi comment gérer les types if/while/… qui sont des blocs d'instructions qui peut être imbriqués.

lang.tmLanguage.json
{
    "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
    "name": "cell",
    "// Le fichier contient soit des fonctions, soit du code source.": "",
    "patterns": [
        {
            "include": "#function"
        },
        {
            "include": "#code"
        },
        {
            "match": ".*",
            "name": "invalid.source.lang"
        }
    ],
    "// Dans le champ repository, on stocke et nomme toutes les regex.": "",
    "repository": {
        "// Regex caractérisant un float.": {},
        "// Ici, l'ordre n'a pas d'important.": {},
        "// Un type peut faire référence (include) à un autre type qui sera défini plus tard.": {},
        "float": {
            "name": "constant.numeric.float.lang",
            "match": "\\b[0-9]+\\.[0-9]*(f|d)\\b"
        },
        "hex": {
            "name": "constant.numeric.hex.lang",
            "match": "\\b0(x|X)[0-9a-fA-F]+\\b"
        },
        "integer": {
            "name": "constant.numeric.integer.lang",
            "match": "\\b[0-9]+\\b"
        },
        "number": {
            "// Regex caractérisant un int.": "",
            "// Il est important de mettre le cas le plus spécialisé en premier.": "",
            "// Si on avait mis integer en premier, on n'aurait jamais réussi à faire matcher float": "",
            "// car les parties entière et décimale seraient détectées comme int": "",
            "patterns": [
                {
                    "include": "#float"
                },
                {
                    "include": "#hex"
                },
                {
                    "include": "#integer"
                }
            ]
        },
        "expression": {
            "// Pour faire simple, on dit qu'un expression, c'est soit :": "",
            "//   - deux nombres avec un opérateur arithmétique au milieu,": "",
            "//   - un nombre.": "",
            "patterns": [
                {
                    "match": "(.*)\\b\\s*(\\+|\\/|\\*|-)\\b\\s*(.*)\\b",
                    "name": "string.regexp.expr.lang",
                    "captures": {
                        "1": {
                            "patterns": [
                                {
                                    "include": "#number"
                                }
                            ]
                        },
                        "2": {
                            "name": "keyword.operator.expr.lang"
                        },
                        "3": {
                            "patterns": [
                                {
                                    "include": "#number"
                                }
                            ]
                        }
                    }
                },
                {
                    "include": "#number"
                }
            ]
        },
        "primary_type": {
            "// On suppose que les types sont des nombres uniquement.": "",
            "// Si le type est inconnu, on le déclare invalide.": "",
            "// Attention à ne pas déclarer invalide comme fallthrough à chaque pattern.": "",
            "// Sinon, les patterns qui include le type s'arrêteront toujours au fallthrough": "",
            "// sans lire les étapes suivantes.": "",
            "patterns": [
                {
                    "match": "(int|short|char|double|float)",
                    "name": "storage.type.lang"
                },
                {
                    "match": ".*",
                    "name": "invalid.primary_type.lang"
                }
            ]
        },
        "init_variable": {
            "// Une initialisation de variable se faire :": "",
            "// int variable = expression;": "",
            "patterns": [
                {
                    "name": "meta.expr.init_variable.lang",
                    "match": "\\s*([^\\s]+)\\b\\s*(.+)\\b\\s*=\\s*(.+);",
                    "captures": {
                        "1": {
                            "patterns": [
                                {
                                    "include": "#primary_type"
                                }
                            ]
                        },
                        "2": {
                            "name": "variable.name.lang"
                        },
                        "3": {
                            "patterns": [
                                {
                                    "include": "#expression"
                                }
                            ]
                        }
                    }
                }
            ]
        },
        "if": {
            "// Un if a la syntaxe suivante :": "",
            "// if expression then": "",
            "//   du code.": "",
            "// fi": "",
            "// La déclaration de la partie code est faite dans l'identifiant code ci-après": "",
            "begin": "^(\\s*)(if)(.*)(then)\\s*",
            "beginCaptures": {
                "2": {
                    "name": "keyword.control.lang"
                },
                "3": {
                    "patterns": [
                        {
                            "include": "#expression"
                        }
                    ]
                },
                "4": {
                    "name": "keyword.control.lang"
                }
            },
            "// Ici, on utilise le groupe \\1 qui fait référence au groupe 1 de begin": "",
            "// Comme il n'est pas possible de détecter les fi correspondant aux if": "",
            "// dans le cas de if imbriqués, on utilise l'indentation pour la correspondance.": "",
            "end": "^(\\1)(fi)\\s*",
            "endCaptures": {
                "2": {
                    "name": "keyword.control.lang"
                }
            },
            "name": "meta.body.if.definition.lang",
            "patterns": [
                {
                    "include": "#code"
                }
            ]
        },
        "code": {
            "// Le code représente des instructions entières.": "",
            "// Ici, on considère que des nombres et expression peuvent": "",
            "// être des instructions mais cela aurait pu être interdit.": "",
            "patterns": [
                {
                    "include": "#if"
                },
                {
                    "include": "#init_variable"
                },
                {
                    "include": "#expression"
                },
                {
                    "include": "#number"
                }
            ]
        },
        "function": {
            "begin": "^\\s*(function)\\s+([^\\s]*)\\s*$",
            "beginCaptures": {
                "1": {
                    "name": "storage.type.function.lang"
                },
                "2": {
                    "name": "entity.name.method.lang"
                }
            },
            "end": "\\s*(end_function)\\s*",
            "endCaptures": {
                "1": {
                    "name": "keyword.other.lang"
                }
            },
            "name": "meta.body.function.definition.lang",
            "patterns": [
                {
                    "include": "#code"
                }
            ]
        }
    },
    "scopeName": "source.lang"
}
test.lang
text not supported
 
156
156.6f
0x156aE
 
123+456
 
float eiuaf = 123;
 
eiua eiua = 123;
 
function xxx
  double eiuad = 123.3d;
  if 156 = 156 then
    if 156 = 156 then
      int eiua = 123.3d;
    fi
    int eiua = 123.3d;
  fi
end_function
 
156
Version colorée :

Utilisation d'un langage existant

Une partie du code peut intégrer un langage existant.

Il faut définir qu'on va s'injecter dans le scopeName dans package.json.

{
  "language": "cellman_stave",
  "scopeName": "source.cellman.stave",
  "path": "./syntaxes/cellman.tmLanguage.stave.json"
},
{
  "scopeName": "shell.injection",
  "path": "./syntaxes/shellscriptInjection.tmGrammar.json",
  "injectTo": [
    "source.cellman.stave"
  ],
  "embeddedLanguages": {
    "meta.embedded.shellscript": "shellscript"
  }
}
shellscriptInjection.tmGrammar.json
{
  "scopeName": "shell.injection",
  "injectionSelector": "R:meta.embedded.shellscript",
  "patterns": [
    {
      "include": "source.shell"
    }
  ]
}

Utilisation du langage importé. Il faut impérativement utiliser begin, end et contentName qui sont multiligne.

Pour être utiliser en single line, on peut tricher…

{
  "begin": ".",
  "end": "$",
  "contentName": "meta.embedded.shellscript"
}

Coloration personnalisée

Afin d'avoir une coloration syntaxique sans devoir personnaliser les couleurs, il est impératif que les noms respectent les règles de la syntaxe grammaticale.

A défaut, il faudra définir les couleurs manuellement dans les préférences de l'utilisateur (fichier settings.json) en plus de l'installation de l'extension.

Gros inconvénient, cela devra être fait pour chaque thème existant.

    "editor.tokenColorCustomizations": {
        "[Default Dark+]": {
            "textMateRules": [
                {
                    "scope": [
                        "source.cellman.TRAN entity.name.extended.language",
                        "Same color than entity.name.function"
                    ],
                    "settings": {
                        "foreground": "#DCDCAA",
                    }
                }
            ]
        },
    }

Générer le module

En local

Il faut ajouter le champ publisher dans package.json et lancer la commande :

vsce package

Et pour installer le module dans Visual Studio Code :

code --install-extension myextension.vsix
prog/vsc/module_highlight.txt · Dernière modification : 2021/02/26 13:22 de root