VSCode拡張機能を自作して、半角と全角スペースをわかりやすくハイライト表示させる



### 完成イメージ

* 半角ホバーメッセージ
![スクリーンショット 2023-07-23 23.13.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3518967/3feb0b79-9801-3d1a-1a0e-6f7f396e8dea.png)
* 全角ホバーメッセージ
![スクリーンショット 2023-07-23 23.13.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3518967/a29de7b6-04bb-0c43-1901-5a416af9b382.png)

### ソース
extension.tsに以下を記載。

```ts
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
	console.log('decorator sample is activated');

	let timeout: NodeJS.Timer | undefined = undefined;

	const FullWidthSpaceDecorationType = vscode.window.createTextEditorDecorationType({
		borderWidth: '2px',
		borderStyle: 'solid',
		overviewRulerColor: '#ff0000',
		overviewRulerLane: vscode.OverviewRulerLane.Full,
		light: {
			// this color will be used in light color themes
			borderColor: 'darkblue',
		},
		dark: {
			// this color will be used in dark color themes
			borderColor: 'lightblue',
		},
	});

	const HalfWidthSpaceDecorationType = vscode.window.createTextEditorDecorationType({
		borderWidth: '1px',
		borderStyle: 'solid',

		overviewRulerLane: vscode.OverviewRulerLane.Full,
		light: {
			// this color will be used in light color themes
			overviewRulerColor: 'tan',
			borderColor: 'tan',
		},
		dark: {
			// this color will be used in dark color themes
			overviewRulerColor: 'lemonchiffon',
			borderColor: 'lemonchiffon',
		},
	});

	let activeEditor = vscode.window.activeTextEditor;

	function updateDecorations() {
		if (!activeEditor) {
			return;
		}
// 連続する半角、全角の正規表現を定義
		const hwSpaceRegEx = /[ ]+/g;
		const fwSpaceRegEx = /[\u3000]+/g;
// テキストを取得
		const text = activeEditor.document.getText();

		const halfWidthSpaces: vscode.DecorationOptions[] = [];
		const fullWidthSpaces: vscode.DecorationOptions[] = [];
		let match;

// 半角文字をデコレーション
		while ((match = hwSpaceRegEx.exec(text))) {
			const startPos = activeEditor.document.positionAt(match.index);
			const endPos = activeEditor.document.positionAt(match.index + match[0].length);
			const decoration = {
				range: new vscode.Range(startPos, endPos),
				hoverMessage: '半角文字： **' + match[0].length + '**',
			};
			halfWidthSpaces.push(decoration);
		}
		activeEditor.setDecorations(HalfWidthSpaceDecorationType, halfWidthSpaces);

// 全角文字をデコレーション

		while ((match = fwSpaceRegEx.exec(text))) {
			const startPos = activeEditor.document.positionAt(match.index);
			const endPos = activeEditor.document.positionAt(match.index + match[0].length);
			const decoration = {
				range: new vscode.Range(startPos, endPos),
				hoverMessage: '全角文字： **' + match[0].length + '**',
			};
			fullWidthSpaces.push(decoration);
		}
		activeEditor.setDecorations(FullWidthSpaceDecorationType, fullWidthSpaces);
	}

	function triggerUpdateDecorations(throttle = false) {
		if (timeout) {
			clearTimeout(timeout);
			timeout = undefined;
		}
		if (throttle) {
			timeout = setTimeout(updateDecorations, 500);
		} else {
			updateDecorations();
		}
	}

	if (activeEditor) {
		triggerUpdateDecorations();
	}

	vscode.window.onDidChangeActiveTextEditor(
		(editor) => {
			activeEditor = editor;
			if (editor) {
				triggerUpdateDecorations();
			}
		},
		null,
		context.subscriptions
	);

	vscode.workspace.onDidChangeTextDocument(
		(event) => {
			if (activeEditor && event.document === activeEditor.document) {
				triggerUpdateDecorations(true);
			}
		},
		null,
		context.subscriptions
	);
}
```