vscode語音註釋, 讓資訊更豐富 (上)
這個系列我會將我製作"語音註釋"外掛的完整過程分享出來, 還是那句話'過程'比'結果'更有趣。
用法展示
下載: voice-annotation
配置+建立 語音檔案存放地址
右鍵撥出'錄製音訊註釋'
錄製完成點選'儲存到專案'
懸停即可播放語音
背景
當你開始閱讀一份工程程式碼時, 也許偶爾就會讀到一些不知其意的程式碼, 如果其有註釋但是看後仍是一頭霧水或者其就壓根沒寫註釋, 那麼解決這個問題最方便的辦法就是直接問問原本的開發者, 同學這裡為啥這麼寫啊?
不僅是 js 語言裡, 大部分語言都已經提供了"文字註釋"的語法, 為什麼我們還是會遇到上述問題那? 假設我們每段程式碼裡都寫了註釋但還是看不懂, 看看都有哪些問題是我們可以通過語音外掛"緩解"的:
- 寫的挺明白了, 看不懂的確是讀者自身的問題 (
無法解決
) - 寫了但是沒寫明白, 開發者自身表達能力 (
無法解決
, 介於"達克效應"&"知識的詛咒"無法自我覺察) - 也許與某幾個需求有關, 用文字描述清楚甚是 '複雜', 索性不寫了(
也許可以緩解
) - 文字的形式說不明白, 我們日常溝通中比較常見, 線上說不清楚了, 直接面對面solo (
也許可以緩解
) - 跨頁的描述, 比如闡述大段的"除錯方法", 隱藏哪個變數, 強行賦值某個變數, 某些操作可以達到某些效果, 常用於前端mock某些場景 (
也許可以緩解
)
沒做過vscode外掛的同學推薦先讀讀我寫的入門教程:
一、功能點'技術方案'分析
① 識別特定註釋
我們需要約定一個特定的寫法, 外掛將這種寫法識別為"語音註釋", 並且當滑鼠懸停在其上時可以正確的進行播報, 當前我使用的就是// voice_annotation + _數字
這種形式。
② 播放註釋音訊
既然寫在vscode裡面, 那其實第一選擇就是使用node來控制音訊輸出裝置, 如果採用開啟一個web新頁面的方式播放聲音, 會增長使用者的操作鏈路, 調研了市場上現有的音訊外掛, 以音樂播放外掛為主還是通過開啟web頁面的方式, 但是我們這裡對播放沒有其餘的操作, 比如快進迴圈播放等稍微複雜一點的需求, 所以選擇使用node播放。
③ 錄製註釋內容
想吃雞蛋當然要有?下蛋, 錄音功能必須要可以將使用者的錄音放入對應的工程, 返回給使用者'特定註釋'的寫法, 假設返回voice_annotation_20220201
, 使用者直接貼上到工程裡就可以正常使用了, 這個功能使用場景應該不多, 所以要考慮效能相關問題。
④ 儲存音訊資訊
大部分開發者電腦上不止一個工程, 如何能夠讓每個工程都可以準確播放對應工程下的語音, 並且每次錄音結束都可以將生成的音訊檔案放入對應工程, 並且可以支援團隊使用。
二、初始工程, 這次我們使用ts
全域性安裝vscode開發工具, 並初始化專案:
npm install -g yo generator-code
yo code
(ts
確實太香了) 前面幾次開發外掛都是使用原生js開發, 由於這次開發的外掛會用到一些'生僻"的api
, 坦白說vscode
的文件不是很友好, 所以我們直接看程式碼的ts
型別才最方便。
"activationEvents": [
"onStartupFinished"
],
來到package.json
檔案裡面, 修改一下這裡外掛啟用的配置:
onStartupFinished
規定了外掛在vscode初始化啟動完成後啟動,它不會佔用使用者初始化vscode時的效能, 並且我們的語音註釋也並不是那種很緊急啟動的程式。
來到extension.ts
檔案裡面, 將無用的程式碼全部清空
清空前
清空後
執行yarn watch
後開始編譯程式碼,fn
+ F5
除錯程式碼會報錯:
看到上面的報錯我也很蒙, 之前用原生js
寫好像沒遇到過, 我還給vscode編輯器升了級還是報錯, 然後就去程式碼裡查了一下:
這就很明朗了, 我們圈定一個相容範圍就好了, 檢視了一下官網日誌, 最後選定了最低2020年 10月左右的版本。
"engines": {
"vscode": "^1.50.0"
},
果然再次除錯就可以出現提示框了:
要注意:
下方的@types/vscode
也要改成與上面相同的版本號, 不然只是現階段不報錯, 打包釋出的時候會報錯。
"dependencies": {
"@types/vscode": "^1.50.0",
}
三、識別特別註釋
這個功能是識別出頁面上的"voice-annotation"並高亮顯示, 之所以優先開發這個功能是因為其自身功能上比較獨立, 並且可以很直觀的看到效果, 瀏覽vscode官網的時候我發現兩種方式可以實現特定語句'高亮'的效果。
這裡我們可以預先定義幾個識別 "語音註釋" 的正則, 假設有這樣的一串字元// voice_annotation_202202070612135678
, 首先我們要識別出 //
註釋符, 拿到'//'後面的voice_annotation_202202070612135678
這一串字元才能為其賦予'樣式', 並且匹配內容中的數字202202070612135678
,這樣才能準確播放語音。
在src/util/index.ts
export const targetName = "voice_annotation"; // 單獨拿出來方便後續的更改
export const getAllTargetReg = new RegExp(`//\\s(${targetName}_\\d+)`, "g");
export const testTargetReg = new RegExp(`${targetName}_(\\d+)`);
四、 方案一: 自定義語法高亮
比如js中這種寫法 function (){}
, 就是函式的語法, in
屬於關鍵字, 這些有特定意義的寫法都會被賦予各種'高亮', 那我們可以將// voice_annotation
設定為關鍵字,
第一步: 在package.json
檔案加入 jojo
作用域
它的作用於js
、ts
、tsx
檔案。
"contributes": {
"grammars": [
{
"path": "./syntaxes/injection.json",
"scopeName": "jojo.injection",
"injectTo": [
"source.js",
"source.ts",
"source.tsx"
]
}
]
},
第二步: 建立injection.json
檔案
{
"scopeName": "jojo.injection",
"injectionSelector": "L:comment.line.double-slash",
"patterns": [
{
"include": "#jojo"
}
],
"repository": {
"jojo": {
"match": "jojo_黃金體驗",
"name": "support.constant.color.0xFF00.css",
"settings": {
"foreground": "FFFFFF"
}
}
}
}
scopeName
顧名思義定義了作用域名稱。injectionSelector
定義了作用域生效的範圍, 這裡的意思就是對//
後面的內容生效。jojo.match
進行關鍵詞的匹配。jojo.name
這個有點特殊, 它是定義這個關鍵字樣式的, 但並不是普通的css樣式, 而是需要你在定義語言樣式的檔案裡定義好, 然後在這邊只能進行使用。
這種方案弊端很多, 比如不好'定製樣式', 不好動態更改設定等等, 最後我並沒有選擇這種實現方式。
五、 方案二: 文件檢索
也就是獲取當前所在'文件'的所有文字內容, 為其賦予類似css樣式的能力, 這種方式更靈活, 並且可以通過偽元素
實現一些有意思的效果。
首先我們需要單獨抽象出一個initVoiceAnnotationStyle
方法, 這個方法專門用來設定voice-annotation
的樣式, 方便隨時隨地識別出"語音註釋":
extension.ts
檔案內進行修改
import * as vscode from 'vscode';
import initVoiceAnnotationStyle from './initVoiceAnnotationStyle'; // 新增
export function activate(context: vscode.ExtensionContext) {
initVoiceAnnotationStyle() // 新增
context.subscriptions.push( // 新增
vscode.window.onDidChangeActiveTextEditor(() => {
initVoiceAnnotationStyle()
})
)
}
export function deactivate() { }
context.subscriptions.push
是在註冊命令, 這裡是註冊了一個生命週期事件。vscode.window.onDidChangeActiveTextEditor
監聽了當我們在vscode中切換開發檔案的事件, 也就是每次開啟檔案時, 之所以使用這個, 是因為我們們的'語音註釋'幾乎不會被改動, 沒必要佔用vscode的效能。
定義樣式
initVoiceAnnotationStyle
檔案
import * as vscode from 'vscode';
import { getAllTargetReg } from './util/index'
export default function () {
vscode.window.visibleTextEditors.map((res) => {
const documentText = res.document.getText();
const decorator = vscode.window.createTextEditorDecorationType({
color: 'gray',
cursor: "pointer",
textDecoration: "underline",
before: {
contentText: "?",
width: "10px",
height: '10px',
margin: "0 8px 0 0"
}
})
let n;
const ranges = [];
while (n = getAllTargetReg.exec(documentText)) {
const startPos = res.document.positionAt(n.index + 3);
const endPos = res.document.positionAt(n.index + 3 + n[1].length);
const range = new vscode.Range(startPos, endPos)
ranges.push(range);
}
res.setDecorations(decorator, ranges)
})}
這裡的知識點很有意思, 配合截圖講一下
vscode.window.visibleTextEditors
你可以理解為獲取到當前'啟用'的程式碼編輯頁的屬性, 裡面包括當前翻閱到了第幾行, 某些樣式是否啟用等屬性。res.document.getText()
獲取當前啟用頁面的內容。vscode.window.createTextEditorDecorationType
製造一個樣式, 這裡只定義固定的幾種樣式, 但是厲害的點是這裡可以製造'偽元素'這樣玩法就多樣性了, 我們可以把它的返回結果列印看看。
這裡返回的
key
其實是個className
。ranges
用來記錄voice-annotation
所在的起始位置與結束位置。- 之所以選擇
while
迴圈來處理, 是因為可能存在多個voice-annotation
。 n.index + 3
是因為// voice-annotation
的前面是//
這個我選擇保留原樣。
具體的調整樣式我們可以通過vscode
開發者工具
六、懸停// voice_annotation
播放樣式
首先這需要我們新定義一個hover
模組, 這個模組負責hover後的樣式
+播放音訊
。
extension.ts
增加:
import * as vscode from 'vscode';
import hover from './hover'; // 新增
import initVoiceAnnotationStyle from './initVoiceAnnotationStyle';
export function activate(context: vscode.ExtensionContext) {
initVoiceAnnotationStyle()
context.subscriptions.push(hover); // 新增
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(() => {
initVoiceAnnotationStyle()
})
)
}
export function deactivate() { }
定義檔案內容
hover.ts
import * as vscode from 'vscode';
import { getVoiceAnnotationDirPath, targetName, testTargetReg } from './util'
let stopFn: () => void;
function playVoice(id: string) {
// 這裡是具體播放音訊的邏輯...
}
export default vscode.languages.registerHoverProvider("*", {
provideHover(documennt: vscode.TextDocument, position: vscode.Position) {
stopFn?.()
const word = documennt.getText(documennt.getWordRangeAtPosition(position));
const testTargetRes = testTargetReg.exec(word);
if (testTargetRes) {
playVoice(testTargetRes[1])
return new vscode.Hover('播放中 ...')
}
}
})
documennt.getText(documennt.getWordRangeAtPosition(position))
可以獲取到當前hover
的文字內容。testTargetReg.exec(word)
校驗當前hover的文字是否為目標文字。return new vscode.Hover('播放中 ...')
這裡返回的文字只能是markdown
的格式。stopFn
是後期要做的停止播放邏輯, 因為有的音訊可能很長, 聽了一半我們不想聽了, 亦或者聽著某個音訊的同時我又懸停在了另一個語音註釋
上, 那麼我們應該停止上一個音訊並播放當前的音訊。
end
下一篇該講到vscode如何播放聲音, 音訊檔案如何傳遞與儲存, 等多坑點等我來跳 這次就是這樣, 希望與你一起進步。