I made a VSCode extension: "markdown-table-rainbow"
Tags: typescript, vscode, extension, markdown
Published: 2023-04-22 05:03:28

Last month when I was writing Markdown, I thought it would be easier to understand if each column in the table had its own color, so I made a VSCode extension that colors the columns in the table.

Of course, I knew there were extensions to format Markdown tables, but I have a problem when:

* If the table has very long items and formatting makes them too wide
* When CKJ characters are used and formatting does not align well

As far as I know, I haven't found an extension with similar functionality.

This extension is published on the marketplace.

* marketplace: [markdown-table-rainbow](https://marketplace.visualstudio.com/items?itemName=yoshi389111.markdown-table-rainbow)

The source code is on GitHub.

{% embed https://github.com/yoshi389111/vscode-md-table-rainbow %}

## How to install

Download it from the marketplace or search for `markdown-table-rainbow` in VSCode Extensions and install it.

## Features

This extension colors by columns in markdown tables.

Also, when the cursor is on a table column, all columns at the same position as the corresponding column are colored.

![demo](https://github.com/yoshi389111/vscode-md-table-rainbow/raw/HEAD/assets/demo.gif)

## Extension Settings

You can use it without any setting. However, you can also change:

```javascript
  // Delay time in milliseconds before updating colors.
  // Smaller values will color faster, but will increase processing cost.
  "markdownTableRainbow.updateDelay": 100,
  // An array of color strings (hex, rgba, rgb) for each column.
  // You can specify any number of colors. used cyclically.
  "markdownTableRainbow.colors": [
    "rgba(79,236,236,0.1)",
    "rgba(255,255,63,0.1)",
    "rgba(127,255,127,0.1)",
    "rgba(255,127,255,0.1)"
  ],
  // Cursor column color strings (hex, rgba, rgb).
  "markdownTableRainbow.cursorColor": "rgba(100,50,180,0.8)",
```

### markdownTableRainbow.updateDelay

Specifies the delay time in milliseconds before updating. When not specified, it operates as 100ms.

Smaller values will color faster, but will increase processing cost.

### markdownTableRainbow.colors

Specifies a list (array) of background colors for each column in the table.
If not specified, the default color will be used.
Cycles through the specified list if the number of columns in the table is greater than the array length.

The colors that can be specified are as follows.

* `#RRGGBB`
* `rgb(R, G, B)`
* `rgba(R, G, B, A)`

### markdownTableRainbow.cursorColor

Specifies the color of the column at the same position as the cursor position.

The colors that can be specified are the same as `markdownTableRainbow.colors`.

## Sites used as reference for creation

* [official VSCode API](https://code.visualstudio.com/api/references/vscode-api)
* [Official sample extension (Decorator)](https://github.com/microsoft/vscode-extension-samples/tree/main/decorator-sample)

## Miscellaneous Notes

### Parsing Markdown

When I first came up with this extension, I thought I would have to use a library to parse the structure of Markdown.
However, when I checked some sources of extension functions for manipulating Markdown tables, I couldn't find any that used such a library.
So I decided to use regular expressions for table-like expressions, i.e. rows containing `|`  at the beginning of a line and multiple `|` on the same line.

Lines starting with `>` are also included to account for tables within quoted blocks.

So, for example, if there is a Markdown table description inside a code block, it will be misidentified as a table as well. This is the intended behavior.

### range to color

This time, the range of columns to be colored is inclusive of the leading `|` and not including the trailing `|`.

At first, I thought it would be better not to include both, but I found an article that says that depending on the type of markdown, colspan-like things can be written like `| here is a two-column join ||`.
Therefore, I thought that it would be difficult to understand if the column color was not displayed at all, so I put a `|` on one side.

I could have included either before or after, but for now I included the previous `|`.

### About resource dispose

In the official sample code mentioned above, we only create an instance of `vscode.TextEditorDecorationType`, which is a definition for decoration, and do not do anything particularly post-processing.

But if you look at the source of `vscode.TextEditorDecorationType` there is a `dispose()` method.
It may not be a problem if I don't call it, but it bothers me so I called `dispose()` at the end.

Just register it with the instance of `vscode.ExtensionContext` that is passed when `activate()` is called, and it will be processed when it is no longer needed.

### rangeBehavior

By default, the decoration range of VSCode is a specification that if you add a character immediately before or after it, the added character will also be included in the decoration range.

Specifically, the `rangeBehavior` specified in `vscode.TextEditorDecorationType` is `OpenOpen` by default.

In this extension, `|` in front of the column is included in the decoration range, but when a character is added before that, it is strange that the added character will be colored in the column behind it, so marked `Closed`.

Since the trailing `|` is not included in the decoration range, it is often better to input immediately after the decoration range (that is, before `|`) in the column color, so immediately after I left it as `Open` (the exception is when entering a new `|`, but in that case, I decided that rewriting after the delay would not be a problem).

Now specifies `ClosedOpen` as result.

### Loading timing of setting items

At first, like the official sample, I looked at the config when `activate()` was called and generated a definition for `vscode.TextEditorDecorationType`.

However, with this, even if the user rewrites `settings.json`, it will continue to work with the old definition unless `activate()` is called again. And since it is not called normally, it will not be reflected unless you restart.

Therefore, `vscode.workspace.onDidChangeConfiguration()` detects the configuration change and recreates the `vscode.TextEditorDecorationType` object again.

By the way, `vscode.TextEditorDecorationType` is registered in `context.subscriptions` in order to `dispose()` at the end of processing.
It may not be such a problem, but it is annoying to keep unnecessary resources all the time, and if you change the configuration many times, it will be registered many times, so when you recreate it, Added processing to remove the previous object from `context.subscriptions`.

I'm not sure if this method is the best.

### Checking config array values

In the case of items defined from the setting screen, even if they are of `string` type, for example, they can be checked with regular expressions (regular expressions can be specified in the `pattern` of the property definition in `package.json`).

However, this time, the background color is defined as an array type, and it is necessary to have it listed directly in `settings.json`, so the format check cannot be applied as it is.

Therefore, I detected the configuration change with `vscode.workspace.onDidChangeConfiguration()` mentioned above and applied a format check when recreating the resource.

At that time, I am currently preparing a long regular expression and checking whether it matches.
Actually, I feel that it would be better to divide it into several short regular expressions or check it with logic instead of doing everything with regular expressions (for example, if the value of `R`/`G`/`B` is 0 to 255 is easier to do with logic than with a regular expression). This is because I thought that the regular expression could be used in common if I checked the format with the definition (`pattern`) of `package.json` and checked both with regular expressions.
In the future, when I increase other patterns, I just copy the same value for both.

Therefore, the JavaScript regular expression is defined like `new RegExp("RE")` instead of `/RE/` (the other one defines it as a string in `package.json`, so use the same format wanted).

### How to get config

I saw a source that acquired the config as follows.

```typescript
const config = vscode.workspace.getConfiguration('myExtension');
const updateDelay: number = config['updateDelay'] || 500;
```

This still works, but in this case the value returned by `config[]` is of the form `any`.

Looking at the API specifications, there is a method called `get()`, which seems to allow you to specify a generic type and specify a default value in the method. Of course, it is necessary to describe it with consistency between the definition and the code.

```typescript
const config = vscode.workspace.getConfiguration('myExtension');
const updateDelay: number = config.get<number>('updateDelay', 500);
```

### Filtering documents of interest

At first, I misunderstood, and if I registered `onLanguage:markdown` in `activationEvents` of `package.json`, I thought that the event would be notified only in Markdown, but apparently it is activated when a Markdown file is opened. Subsequent events seem to be notified regardless of document type.

So before processing the target `editor` we need to check if the document is target like this:

```typescript
  if (editor.document.languageId !== 'markdown') {
    return;
  }
```

I thought this would be fine, but I thought it would be troublesome to have to modify both `package.json` and this logic if I wanted to process files other than `markdown` in the future.

It seems that `context.extension.packageJSON` can get the contents of the extension's `package.json`.
(Although he seems to be able to get other extensions with `vscode.extensions.getExtension()`. The key seems to be specified like `"<publisher name>.<extension name>"`)

So I tried the following. The current situation is over spec.

```typescript
    const onLanguages = (context.extension.packageJSON["activationEvents"] as string[])
        .filter(it => it.startsWith("onLanguage:"))
        .map(it => it.substring("onLanguage:".length));

    // ...

        if (!onLanguages.includes(editor.document.languageId)) {
            return;
        }
```

By the way, are there any languages other than `markdown` that should be supported?

### Minimum version of VSCode

There is a place to describe the minimum version of VSCode that works in `engines` of `package.json`.

```javascript
    "engines": {
        "vscode": "^1.76.0"
    },
```

This extension does not use any new features, so it should work even on very old versions.
However, I don't know how to test with older versions, so I specified the latest version at the time.

Is it usually the one that specifies the latest version?

### Japanese version of this document

* Qiita: [Markdownのテーブルの列に色付けするVSCode拡張機能を作ったよ](https://qiita.com/yoshi389111/items/bd6435ef6830742e4d9d)