BACK
Featured image of post 今天來搞一個屬於自己的 VScode Extension 吧!

今天來搞一個屬於自己的 VScode Extension 吧!

VSCode 是個基於 Electron 與 Typescript,由微軟開發的一款輕量跨平台的 IDE (Integrated Development Environment),具有豐富且龐大的生態系。最近幾年特別火紅,不僅僅由於它本身內建功能越來越多,也因為 IDE 軟體開源、功能容易擴充,備有完善且成熟的擴充套件 API、文件以及相關教學,讓廣大的開發者更舒服的發揮想法,一同參與官方或非官方的擴充套件開發,以增強這款編輯器軟體,讓整個編輯器生態越來越好。

參考網站
參考網站
參考網站


前言

VScode 使用者一定會安裝各種 Extension 來建立一個適合自己的開發環境,而這些強大的 Extension 大部分都由世界各地的愛用者開發並發布(publish) ,一切都是為了要讓 VSCode 更加實用上手甚至美觀。

這篇就是要分享如何建立並發佈一個 Extension,英文程度不錯的建議直接閱讀官方文件,會有更加詳細的解說


開發環境配置

在開始前,請照下方指示正確設置自己的環境。

確認已安裝 nodejs 與 npm

1
node -v && npm -v

如未安裝兩者,請先至 Nodejs 官方網站 下載。npm 會隨著 nodejs 下載一併被安裝,有時候您可能使用過舊的 npm,這時您可以使用以下指令更新它到最新版。

1
npm install -g npm@latest

如果正確安裝了以上兩者,會跳出兩行版本號資訊。

export:
1
2
v16.15.0
8.15.1

安裝 yoman

Yoman 是一款流行已久的code generator,可以允許我們使用yoman設置專案的樣板, 並讓我們使用 yo 指令快速產生樣板專案。

VSCode 官方已經發布了 VSCode Yoman 專案,並且定期更新,因此我們可以直接使用 yo 指令產生 VSCode Extension,無需手動開發 yoman 樣板。

請使用以下指令安裝yoman與VSCode Extension Generator:

1
npm install -g yo generator-code

安裝 yoman 後,再次使用版本號指令確認有無正確安裝:

1
yo --version

使用 yo 快速產生第一個 Extension 專案

首先,於 terminal 進入一個要放置專案的資料夾後,使用指令:

1
yo code

generator 會跳出提示,讓我們選擇要產生的 extension 種類:

這些 Extension 選項的描述與說明如下:

Extension 選項描述
New Extension(Typescript)產生使用typescript開發的extension專案
New Extension(Javascript)產生使用javascript開發的extension專案
New Color Theme配置VSCode介面顏色的擴充套件專案(詳見: Color Theme)
New Language Support程式語言(Programming Languages)擴充套件
New Code Snippets程式碼片段擴充套件
New Keymap快捷鍵擴充套件,keymap讓使用者得以在vscode中使用vim、sublime等等不同編輯器的快捷鍵開發。
New Extension Pack打包多個已發佈的extension,讓使用者一鍵快速安裝。
New Language Pack (Localization)配置VSCode編輯器多國語氣的擴充套件。

我們依序輸入如下:

  • What type of extension do you want to create?
    選擇你要建立的專案類型
    這邊我選擇第一個 New Extension(Typescript) 選項

  • What’s the name of your extension?
    Extension 名稱: 對應到 package.json “displayName”
    這裡我輸入: wconvert

  • What’s the identifier of your extension?
    識別碼:對應到 package.json “name”
    這裡我輸入: wconvert

  • What’s the description of your extension?
    描述: 對應到 package.json “description”
    這裡我輸入: my first vscode extension practice.

  • Initialize a git repository?
    是否要使用 git

  • Bundle the source code with webpack?
    是否要使用 webpack 做原始碼的 bundle

  • Which package manager to use?
    要使用哪個套件管理工具

讓子彈飛一會兒,最後 generator 會詢問是否使用 vscode 打開,這邊選擇打開。


Extension 專案重點相關檔案介紹

打開後,我們會看到一個 nodejs 的 typescript 專案,結構如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
    ├── .vscode
    │   ├── extensions.json
    │   ├── launch.json
    │   ├── settings.json
    │   └── tasks.json
    ├── node_modules
    ├── src
    │   ├── extension.ts
    │   └── test
    │       ├── runTest.ts
    │       └── suite
    │           ├── extension.test.ts
    │           └── index.ts
    ├── .eslintrc.json
    ├── .gitignore
    ├── .vscodeignore
    ├── CHANGELOG.md
    ├── README.md
    ├── package-lock.json
    ├── package.json
    ├── tsconfig.json
    └── vsc-extension-quickstart.md
  • .vscode/*: workspace 設定
檔案名稱說明
task.json設定 defaultBuild Task,用於 compile extension 專案的 typescript 程式。
launch.json配置 debug mode 的兩個選項:Run extensionTest extension,用於執行 extension 主程式與相關測試程式,程式執行前,會先執行 defulat buildTask
settings.jsonextension 專案的設定檔,此處的設定會覆蓋 user settingsdefault settings
extensions.json設定用於輔助 extension 專案安裝的 extension recommendations list,此處推薦安裝 eslint extension
  • src/extensions.ts: 主程式檔案
  • src/test.ts: 測試程式檔案
  • .eslintrc.json: 用於 extension 專案的 eslint 設置
  • .vscodeignore: 用於忽略不打包進發布套件的專案檔案
  • tsconfig.json: 用於專案 ts compiler 的設定選項
  • package.json: 用於配置 node 相關依賴與相關 npm script,在 extension 專案裡,package.json 亦用於配置 extension 重要相關設定 (詳見: Extension Manifest)
  • vsc-extension-quickstart.md: 產生的 extension 專案的 markdown 說明文件

Extension 專案程式簡介

讓我們打開 extension.ts 吧,打開後可以看見 extension.ts 裡面有個 active functiondeactive function

active function 為 extension 程式的進入點。當 extension 被 active 事件啟動時,即會執行 extension 程式。

因此我們可以查看 pakcage.json,package.json 配置了 activeEvents 清單,可以看到清單裡列出跟active 有關的設定 activationEvents

package.json
1
2
3
4
5
6
7
{
  // ...
  "activationEvents": [
    "onCommand:wconvert.helloWorld"
  ],
  // ...
}

activationEvents 指定了一個 hello world command,此即是說,當使用者執行 hello world command 時,extension 即會活躍並執行 active function

此處的 onCommand 語法為:

1
onCommand: ${commandId}

那麼,command 的 id 是在哪裡配置的呢?

一樣是在 package.json 裡,我們可以到 contributes 屬性 (Contribution Point: 詳見 Contribution Point) 下面查看:

package.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  // ...
  "contributes": {
    "commands": [
      {
        "command": "wconvert.helloWorld",
        "title": "Hello World"
      }
    ]
  },
  // ...
}

可以看到在 contrubutes 屬性下面已經配置了 commands 清單,裡面條列著一個 command 設定,此處 command 設定的語法為:

1
2
3
4
5
6
7
{
  /**
   * 被產生的Command會預設使用extension的id作為namespace,亦可自訂其他Namespace名稱
   */
  "command": ${自定義的command-id},
  "title": ${Command的標題內容}
}

當我們使用 Command Palette (Cmd/Ctrl + Shift + P) 搜尋 Command 並執行時,是使用 Command 的 Title 搜尋。

好,現在我們已經了解如何在 Contribution Point 設定簡單的 Command id 跟 title,以及設定它為活躍 extension 的 event。現在回到 extension.ts 的 active function 吧!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
export function activate(context: vscode.ExtensionContext) {

	// Use the console to output diagnostic information (console.log) and errors (console.error)
	// This line of code will only be executed once when your extension is activated
	console.log('Congratulations, your extension "wconvert" is now active!');

	// The command has been defined in the package.json file
	// Now provide the implementation of the command with registerCommand
	// The commandId parameter must match the command field in package.json
	let disposable = vscode.commands.registerCommand('wconvert.helloWorld', () => {
		// The code you place here will be executed every time your command is executed
		// Display a message box to the user
		vscode.window.showInformationMessage('Hello World from wconvert!');
	});

	context.subscriptions.push(disposable);
}

我們可以看到,active function 裡面在 extenions 活躍後將註冊我們在 Contribution Point 那邊設定的 command id,並且有一個 callback 函式,當我們配置的 command 執行之後,即會在 vscode 裡跳出顯示「Hello World from day05-first-command!」的訊息。

這個註冊過後的 command 函式 (disposable) 會再 push 進 extension context 裡面,如此當 extension 被關閉後,VSCode 就可以自動釋放 listen 這個 command 的相關資源。


執行 extension 專案應用程式

讓我們來執行 extension 吧,vscode 專案已經幫我們配置好了 lanunch.json,因此我們可以在 debug mode 裡執行 extension。讓我們點開「Run and Debug」,再點擊 sidebar 上方的 Run Extension 旁的執行按鈕開始 extension 吧!(此處的快捷鍵為 F5)

執行後,vscode 會開啟一個新的 vscode 的 window 視窗,window 視窗上的 title 會註明這個視窗為 [Extension Development Host] 並預設載入 extension 了,我們可以在這個視窗操作我們開發中的 extension,並且使用中斷點偵錯。

檢查已被註冊的 command

先來檢查一下剛才註冊的 command,在 Contribution Point 裡宣告的 Command,一樣在 Keyboard Shortcuts 下方可以搜尋的到。從 Manager > Keyboard Shortcuts 進入 Keyboard Shortcuts 頁中,並在搜尋條上輸入 wconvert,使用 extension id 來列出剛才註冊的 command。

輸入後我們可以看到,Command 已正確被註冊。

執行註冊後的 Command

現在,我們打開 Command Palette (快捷鍵:Cmd/Ctrl + Shift + P),輸入 hello world,使用註冊的 Command Title 查找到 command。

然後,點擊下去,我們可以看到 vscode 的 window 正確跳出「Hello World from day05-first-command!」訊息。

然後,我們回到原本專案的 Vscode Window,我們可以在專案的 debug console 檢視 active function 下面的 console.log 訊息。


加入套件邏輯

這邊可以依照你的需求進行套件邏輯的開發,我只是寫來玩而已,此處參考即可!

註冊自己想要使用的 Command

我打算製作以下幾個功能:

  1. 輸入指定的時間格式,會在當前檔案指標位置插入產生的時間戳
  2. 輸入指定的時間格式,僅會在vscode 的 window 顯示
  3. 將選取起來的時間戳,轉換為指定時區的時間格式(這邊以 YYYY-MM-DD hh:mm:ss.milsecond 格式為主)

附上完成後的程式碼:

extension.ts
 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
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
	const input = async (placeHolder: string) => await vscode.window.showInputBox({
		placeHolder
	}) || '';

	function pad(num: string | number, size: number): string {
		num = num.toString();
		while (num.length < size) {
			num = "0" + num;
		}
		return num;
	}

	// show timestamp at vscode Information Window
	function showTimestamp(datetime: string) {
		let timestamp: number | undefined = NaN;
		try {
			timestamp = new Date(datetime).getTime();
			if (typeof timestamp !== "number" || isNaN(timestamp) || timestamp.toString() === "NaN") {
				return askDateTime();
			};
			vscode.window.showInformationMessage(timestamp.toString());
		} catch(e) {
			console.log(e);
			vscode.window.showErrorMessage("Invalid Date.");
			askDateTime();
		}
	}

	// create input with asking DateTime
	async function askDateTime() {
		const fullDateTime = await input("DateTime (format: YYYY-MM-DD hh:mm:ss.ms) (example: 2022-12-31 23:59:59.999):");
		if (fullDateTime) {
			showTimestamp(fullDateTime);
		}
	}

	// Insert timestamp at cursor position
	vscode.commands.registerTextEditorCommand("wconvert.genTimestamp", async (editor, edit) => {
		const fullDateTime = await input("DateTime (format: YYYY-MM-DD hh:mm:ss.ms) (example: 2022-12-31 23:59:59.999):");
		if (!fullDateTime) {
			return;
		}

		try {
			let timestamp = new Date(fullDateTime).getTime();
			if (!isNaN(timestamp) && typeof timestamp === "number" && timestamp.toString() !== "NaN") {
				editor.edit((editBuilder) => {
					editor.selections.forEach((selectionItem) => {
						editBuilder.insert(selectionItem.active, timestamp.toString());
					});
				});
			} else {
				vscode.window.showErrorMessage("Datetime convert failed.");
			}
		} catch(e) {
			vscode.window.showErrorMessage("Invalid Date.");
		}
	});

	// only show generated timestamp
	let generateTimestamp = vscode.commands.registerCommand("wconvert.genTimestampJustShow", askDateTime);

	// format timestamp to YYYY-MM-DD HH:mm:ss that selections
	let timestamp2DateTime = vscode.commands.registerTextEditorCommand("wconvert.timestamp2dateTime", async (editor, edit) => {

		const offsetStr = await input("Time zone (example: +8):");
		const offset = Number(offsetStr);

		editor.edit((editBuilder) => {
			editor.selections.forEach((selectionItem) => {
				try {
					const selection = editor.document.getText(selectionItem);
					let dateArr = new Date(parseInt(selection) - (-offset * 60 * 60 * 1000)).toISOString().split("T");
					const [YYYYMMDD, hhmmss] = dateArr;
					const [year, month, day] = YYYYMMDD.split("-");
					const [hour, minute, fullSecond] = hhmmss.split(":");
					const [second, millionSecondZ] = fullSecond.split(".");
					const millionSecond = millionSecondZ.replace("Z", "");

					let fullDateTime = `"${year}-${pad(month, 2)}-${pad(day, 2)} ${pad(hour, 2)}:${pad(minute, 2)}:${pad(second, 2)}.${pad(millionSecond, 3)}"`;
					
					editBuilder.replace(selectionItem, fullDateTime);
				} catch (e) {
					vscode.window.showErrorMessage("Invalid Date.");
				}
			});
		});
	});

	context.subscriptions.push(timestamp2DateTime, generateTimestamp);
}

export function deactivate() {}

package.jsonactivationEvents 記得加上欲註冊使用的 command,也可直接使用 "*",將全部的 command 暴露出去!

package.json
 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
{
  "name": "wconvert",
  "displayName": "wconvert",
  "description": "help you convert datetime value",
  "version": "1.0.3",
  "icon": "photo.jpeg",
  "publisher": "4006wayne",
  "repository": {
    "type": "git",
    "url": "https://github.com/wjdesign/vscode-extension-wconvert.git"
  },
  "homepage": "https://github.com/wjdesign/vscode-extension-wconvert/blob/master/README.md",
  "pricing": "Free",
  "engines": {
    "vscode": "^1.73.0"
  },
  "categories": [
    "Other"
  ],
  "activationEvents": [
    "onCommand:wconvert.timestamp2dateTime",
    "onCommand:wconvert.genTimestamp",
    "onCommand:wconvert.genTimestampJustShow"
  ],
  "main": "./out/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "wconvert.timestamp2dateTime",
        "title": "WConvert: DateTime to timestamp"
      },
      {
        "command": "wconvert.genTimestamp",
        "title": "WConvert: Generate Timestamp"
      },
      {
        "command": "wconvert.genTimestampJustShow",
        "title": "WConvert: Generate Timestamp Only Show"
      }
    ]
  },
  "scripts": {
    "vscode:prepublish": "npm run compile",
    "compile": "tsc -p ./",
    "watch": "tsc -watch -p ./",
    "pretest": "npm run compile && npm run lint",
    "lint": "eslint src --ext ts",
    "test": "node ./out/test/runTest.js"
  },
  "devDependencies": {
    "@types/vscode": "^1.73.0",
    "@types/glob": "^8.0.0",
    "@types/mocha": "^10.0.0",
    "@types/node": "16.x",
    "@typescript-eslint/eslint-plugin": "^5.42.0",
    "@typescript-eslint/parser": "^5.42.0",
    "eslint": "^8.26.0",
    "glob": "^8.0.3",
    "mocha": "^10.1.0",
    "typescript": "^4.8.4",
    "@vscode/test-electron": "^2.2.0"
  }
}

範例 repo


vsce

打包並發布 Extension,官方文件

使用 vsce打包發布我的 Extension

首先全局安裝 vsce:

1
npm i -g vsce

打包

1
vsce package

下完指令後,會因為沒有 LICENSE 檔案而詢問是否繼續,這邊選擇 Y 直接下一步即可。

完成後會在指定位置產生一個 .vsix 檔案,此即為我的 Extensions

本地安裝我的 Extension

若只是要製作一個自己用的 extension,沒有打算發布到網上,到此步驟即可。

於 VScode 的 Extensions 頁籤,選擇 Install from VSIX,選擇剛剛產生的 .vsix 檔,即可完成安裝,在 Extensions 的 INSTALLED 內即可看到自己的 extension。


發布到 Marketplace 前的準備

記住!每次發佈都要幫版本號加一下!

package.json
1
2
3
4
5
{
  // ...
  "version": "0.0.1",
  // ...
}

vsce 要求 Personal Access Token 個人訪問金鑰,以下的步驟圖片來源為官方說明文件

首先請到 https://dev.azure.com/ 申請帳號並登入,在右上角打開 Personal access tokens 頁面。

點選 New Token 按鈕。

給這個新 Token一個名字、過期時間,重點是 Organization 請選擇 All accessiable organization,點選 Custom defined 並點選 Show all scopes 打開所有選項。

找到 Marketplace 項目勾選 AcquireManage 後點選 Create

然後你會得到一個 Token 請複製下來。

建立發佈器 - Publisher

建立一個 Publisher 用來儲存發佈的 Token ,請在 package.json 中指定要使用的發佈器"publisher": (publisher name)

package.json
1
2
3
4
5
{
  // ...
  "publisher": "{your publisher name}",
  // ...
}

使用 Marketplace 後台建立 Publisher

請到 https://marketplace.visualstudio.com/manage/publishers/ 建立一個 Publisher。

使用 vsce 指令建立 Publisher

1
vsce create-publisher (publisher name)

輸入剛剛的 Personal access token,vsce 將會記得這個 publisher 與 token 以方便快速發佈。

Token 更新

在我們建立 token 時,token 是有效期的,當 token 過期時我們就需要以下這個指來更新 token:

1
vsce login (publisher name) 

發布方式(一):使用 vsce 發布 extension

下指令發布到 Marketplace:

1
vsce publish

使用 vsce show 確認 extension 的資訊:

1
2
# vsce show {publisher name}.{extension id}
vsce show 4006wayne.wconvert

也可以在 vsce publish 時直接指定 token:

1
vsce publish -p <token>

發布方式(二):使用 Marketplace 後台上傳,發布 extension

前往網址 https://marketplace.visualstudio.com/manage/publishers/,選擇剛剛創建的 Publisher。

點選 New extension > Visual Studio Code,選擇剛剛 package 出來的 .vsix 檔案後上傳。

上傳後等待驗證,驗證完畢後即可看到我的 extension:


結語

到這裡我已經成功發佈我的第一個 Extension 了!

這是我寫的第一個 Extension,功能很陽春,用途是產生指定時間的時間戳與時間戳轉回指定時區的時間格式。

非常推薦使用 Typescript 會比較容易開發,不然要查官方API會看得很痛苦,畢竟範例不多或者都要下載才能看。

實際撰寫時會遇到 VScode API 到底有哪些功能可以使用的問題,甚至要去了解 VScode 的設計結構!


常見問題

Extension validation error

出現如圖的問題,自行排查 extension 程式碼後發現非程式碼邏輯的問題,可以上 issues 將資訊反應給 vsmarketplace,此可能為 marketplace 驗證 extension 時偵測到特定字元組合而阻擋掉,會專人協助處理。

附上本人遇到此問題的 Issues


comments powered by Disqus