giftee engineer blog

CSS Variables 形式のデザイントークンを補完する VS Code Extension を作成し生産性を上げる

2023-10-10

CSS Variables 形式のデザイントークンを VS Code 上で補完することで開発の生産性を上げました。

thumbnail

こんにちは、エンジニアの egurinko です。過去の記事でデザインシステム Abukuma を作り始めたこと紹介しましたが、今回は CSS Variables 形式で利用可能としているデザイントークンを VS Code 上で補完する Extension を作成したので、その紹介をします。

なぜ VS Code Extension が必要なのか?

Abukuma ではデザイントークンを Figma Variables として管理しつつ、それを CSS Variables に変換し npm package として publish しています。そして、CSS Variables として publish されたデザイントークンを以下のように CSS ライブラリの開発に利用しています。

.ab-Button {
    color: var(--ab-semantic-color-text-primary);
}

しかし、適切な CSS Variables を利用するためには、元の CSS Variables の定義を確認し、そこから変数名をコピペするという手間がありとても大変でした。

そこで、VS Code 上で CSS Variables の補完ができる Extension を作成することにしました。これにより、定義元を確認しコピペする手間を減らしたり、補完時に CSS Variables の補足を出すことで誤った CSS Variables を使うことも減らしたりすることで、生産性を向上させることが狙いです。なお、発想や実装は Shopify の Polaris にある VS Code Extension を大いに参考にしています。

実装

実装には大きく分けて 2 つのステップがありました。

  1. デザイントークンを TypeScript で扱えるようにする
  2. VS Code Extension を TypeScript で実装する

VS Code Extension の処理は比較的シンプルで、入力された値に従ってデザイントークンを絞り込み、補完候補として表示するだけです。しかし、それを実現するにはそもそもデザイントークンを TypeScript で扱える必要があります。以下で順を追って説明します。

1. デザイントークンを TypeScript で扱えるようにする

前述したように、Abukuma では Figma Variables で管理しているデザイントークンを CSS Variales に変換していました。

この変換は以下のように Figma Variables を JSON で export したものを Style Dictionary に流し込むことで行っていました。

thumbnail

そこで、Style Dictionary に JavaScript と型定義の設定を追加し、デザイントークンを TypeScript で扱えるようにしました。

{
    "source": ["tokens/**/*.json"],
    "platforms": {
        "css": {
            "transformGroup": "css",
            "buildPath": "dist/",
            "files": [
                {
                    "destination": "css/index.css",
                    "format": "css/variables",
                    "options": {
                        "outputReferences": false
                    }
                }
            ]
        },
        "cjs": {
            "transformGroup": "js",
            "buildPath": "dist/",
            "files": [
                {
                    "destination": "cjs/index.js",
                    "format": "javascript/module"
                },
            ],
        },
        "types": {
            "transformGroup": "js",
            "buildPath": "dist/",
            "files": [
                {
                    "destination": "types/index.d.js",
                    "format": "typescript/module-declarations"
                }
            ]
        }
    }
}

これで VS Code Extension の開発準備が完了です。

2. VS Code Extension を TypeScript で実装する

VS Code の Extension はコマンドを作成したり、テーマを変更したりと様々なことができます。今回は、補完をすることが目的であるため、Language Extension を作成していくことになりました。

また、Language Extension の作成には、VS Code の API を直接叩く方法と、Language Server を利用する方法がありますが、今回は Language Server で実装することとしました。

なぜ Language Server を利用するのか?

Language Server を用いる実装をする場合、Client と Server の 2 つを実装することになります。Client がいわゆる VS Code の拡張機能部分で、Server 側で言語解析処理を行います。Client/Server 間は Language Server Protocol (LSP) という言語処理サーバとエディタの通信用のプロトコルを利用します。

lsp

こうすることで、Server の処理は他のエディタでも基本的に使い回すことができます。ギフティでは VS Code 以外のエディタの使い手も多くいるため、エディタの拡張性を考えて Language Server を利用した実装をしました。

Client の実装

まずは、開発している Extension が起動するタイミングを package.json に記述します。activationEvents で起動タイミングを設定できるので、今回は CSS/SCSS ファイルを開かれているタイミングとしています。また起動時に、./out/client.js を見にいくようにしています。

{
  "main": "./out/client.js",
  "activationEvents": [
    "onLanguage:css",
    "onLanguage:scss"
  ]
}

compile される前の client.ts の処理は、generator で generate したコードとほとんど変わりません。一点変更されているのは、ここでも CSS/SCSS のみを処理対象としているところです。

  const clientOptions: LanguageClientOptions = {
    documentSelector: [
      {scheme: 'file', language: 'css'},
      {scheme: 'file', language: 'scss'},
    ],
    synchronize: {
      fileEvents: workspace.createFileSystemWatcher('**/.clientrc'),
    },
  };

Server の実装

Server では、まずいつ補完処理をトリガーするのかを設定します。CSS Variables の利用時に補完候補の表示を行いたいため、-- をトリガーとして設定します。

// server.ts
import {
  ProposedFeatures,
  createConnection,
} from 'vscode-languageserver/node';
import type { InitializeResult } from 'vscode-languageserver/node';

const connection = createConnection(ProposedFeatures.all);

connection.onInitialize(() => {
  const result: InitializeResult = {
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
      completionProvider: {
        triggerCharacters: ['--'], // '--' が入力されたら補完候補を表示する
      },
    },
  };

  return result;
});

また、デザイントークンの絞り込みを行う前に、npm package のデザイントークンを補完候補として表示できるようにデータを整形します。この際に、トークングループごとの補完候補データと、全てのトークングループを合わせた補完候補データの 2 つを用意しておきます。

この補完候補をテキスト入力が終了した connection.onCompletion の callback で return してあげると、それが補完候補として表示されます。

import { tokens } from 'design-tokens of giftee';

// もともとこんな感じ
/*
{
    color: {
        brand: {
            default: {
                name: "ab-color-brand-default",
                value: "#FFFFFF",
                type: "color",
                comment: "comment"
            },
            dark: {},
            light: {},
        },
        text: {},
    },
    border: {
        radius: {
            xs: {
                name: "ab-border-radius-xs",
                value: "4px",
                type: "number",
                comment: "comment"
            },
            sm: {},
        },
        width: {},
    },
    spacindg: {}
}
*/

// 補完候補に変換。トークングループごとにまとめておく
/*
const tokenGroupCompletionItems = {
    color: [
        {
            label: "--ab-color-brand-default",
            kind: CompletionItemKind.Color,
            insertText: `var(--ab-color-brand-default)`,
            documentation: "comment",
            filterText: "--ab-color-brand-default",
        }
    ],
    border: [],
    spacing: [],
}
const allCompletionItems = [{}, {}];
*/

import { TextDocuments } from 'vscode-languageserver/node';
import type {
  TextDocumentPositionParams,
  CompletionItem,
} from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';

const documents = new TextDocuments(TextDocument);

connection.onCompletion(
  (textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    return allCompletionItems;
  },
);

上記では、最後に絞り込みなどをせずに全トークンを補完候補して返しています。

さらにここから、実際に入力されたテキストからデザイントークンを絞り込み表示していく処理を書いていきます。まずは、テキスト入力が完了したタイミングで、入力されたテキストを受け取る処理を書きます。

connection.onCompletion(
  (textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    const doc = documents.get(textDocumentPosition.textDocument.uri);

    // 入力中のテキスト
    const currentText = doc.getText({
      start: { line: textDocumentPosition.position.line, character: 0 },
      end: { line: textDocumentPosition.position.line, character: 1000 },
    });

    return allCompletionItems;
  },
);

currentText は入力している行のテキストを 0~1000字まで取得するので、ここまでで、background-color: -- のような入力が currentText に入っているようなイメージになります。ここから、入力されたテキストに応じてデザイントークンをフィルタリングします。これは、あらかじめ入力されるテキストに含まれる CSS property とデザイントークンのグルーピングを対応させておくところから始まります。

const tokenGroupPatterns = {
  border: /border/,
  color: /color|background|border/, // テキスト入力の color, background, border はデザイントークンの color group に対応
  spacing: /margin|padding|gap/, // テキスト入力の margin, padding, gap はデザイントークンの spacing group に対応
  icon: /font/,
};

connection.onCompletion(
  (textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    const doc = documents.get(textDocumentPosition.textDocument.uri);
    let matchedCompletionItems: CompletionItem[] = [];

    // 入力中のテキスト
    const currentText = doc.getText({
      start: { line: textDocumentPosition.position.line, character: 0 },
      end: { line: textDocumentPosition.position.line, character: 1000 },
    });

    for (const [tokenGroupName, pattern] of Object.entries(
      tokenGroupPatterns,
    )) {
      if (!pattern.test(currentText)) {
        continue;
      }

      // あらかじめトークングループごとに整理した補完候補を取り出して返す
      const currentCompletionItems = tokenGroupCompletionItems[tokenGroupName];

      return matchedCompletionItems.concat(
        currentCompletionItems,
      );
    }

    return allCompletionItems;
  },
);

あらかじめ CSS property とデザイントークングループを対応させたものと、入力中のテキストに含まれる CSS property を照合し、対応するデザイントークングループを割り出します。padding: -- と入力された場合は、デザイントークンの spacing グループが該当します。

あとは、事前にトークングループごとに整理した補完候補を取り出し、それを return します。こうすることで、入力された内容から絞り込まれたデザイントークンを補完候補として表示することができました。

さらに VS Code Extension でできそうなこと

ここまででひとまず CSS Variables を補完候補として表示することができました。今後は、hover 時の内容を充実させたり、他のエディタへの対応、開発している CSS ライブラリのクラスを補完できるような VS Code Extension の作成も視野に入りそうです。

まとめ

今回は CSS Variables 形式のデザイントークンを補完する VS Code Extension を作成し生産性を上げるという内容を紹介しました。

ギフティではデザインシステムを一緒に考えていけるようなエンジニア/デザイナーも絶賛募集中です。気になる方は、是非一度カジュアル面談でもなんでも良いので一度お話ししましょう!

ギフティ採用ページを見てみる