vscode語音註釋, 讓資訊更豐富 (上)

lulu_up發表於2022-02-20

vscode語音註釋, 讓資訊更豐富 (上)

     這個系列我會將我製作"語音註釋"外掛的完整過程分享出來, 還是那句話'過程'比'結果'更有趣。

用法展示

下載: voice-annotation
image.png

配置+建立 語音檔案存放地址

image.png

右鍵撥出'錄製音訊註釋'

image.png

錄製完成點選'儲存到專案'

image.png

懸停即可播放語音

image.png

背景

     當你開始閱讀一份工程程式碼時, 也許偶爾就會讀到一些不知其意的程式碼, 如果其有註釋但是看後仍是一頭霧水或者其就壓根沒寫註釋, 那麼解決這個問題最方便的辦法就是直接問問原本的開發者, 同學這裡為啥這麼寫啊?

     不僅是 js 語言裡, 大部分語言都已經提供了"文字註釋"的語法, 為什麼我們還是會遇到上述問題那? 假設我們每段程式碼裡都寫了註釋但還是看不懂, 看看都有哪些問題是我們可以通過語音外掛"緩解"的:

  1. 寫的挺明白了, 看不懂的確是讀者自身的問題 (無法解決)
  2. 寫了但是沒寫明白, 開發者自身表達能力 (無法解決, 介於"達克效應"&"知識的詛咒"無法自我覺察)
  3. 也許與某幾個需求有關, 用文字描述清楚甚是 '複雜', 索性不寫了(也許可以緩解)
  4. 文字的形式說不明白, 我們日常溝通中比較常見, 線上說不清楚了, 直接面對面solo (也許可以緩解)
  5. 跨頁的描述, 比如闡述大段的"除錯方法", 隱藏哪個變數, 強行賦值某個變數, 某些操作可以達到某些效果, 常用於前端mock某些場景 (也許可以緩解)

沒做過vscode外掛的同學推薦先讀讀我寫的入門教程:

一、功能點'技術方案'分析

① 識別特定註釋

     我們需要約定一個特定的寫法, 外掛將這種寫法識別為"語音註釋", 並且當滑鼠懸停在其上時可以正確的進行播報, 當前我使用的就是// voice_annotation + _數字這種形式。

② 播放註釋音訊

     既然寫在vscode裡面, 那其實第一選擇就是使用node來控制音訊輸出裝置, 如果採用開啟一個web新頁面的方式播放聲音, 會增長使用者的操作鏈路, 調研了市場上現有的音訊外掛, 以音樂播放外掛為主還是通過開啟web頁面的方式, 但是我們這裡對播放沒有其餘的操作, 比如快進迴圈播放等稍微複雜一點的需求, 所以選擇使用node播放。

③ 錄製註釋內容

     想吃雞蛋當然要有?下蛋, 錄音功能必須要可以將使用者的錄音放入對應的工程, 返回給使用者'特定註釋'的寫法, 假設返回voice_annotation_20220201, 使用者直接貼上到工程裡就可以正常使用了, 這個功能使用場景應該不多, 所以要考慮效能相關問題。

④ 儲存音訊資訊

     大部分開發者電腦上不止一個工程, 如何能夠讓每個工程都可以準確播放對應工程下的語音, 並且每次錄音結束都可以將生成的音訊檔案放入對應工程, 並且可以支援團隊使用。

二、初始工程, 這次我們使用ts

     全域性安裝vscode開發工具, 並初始化專案:

npm install -g yo generator-code 

yo code

image.png

     (ts確實太香了) 前面幾次開發外掛都是使用原生js開發, 由於這次開發的外掛會用到一些'生僻"的api, 坦白說vscode的文件不是很友好, 所以我們直接看程式碼的ts型別才最方便。

image.png

    "activationEvents": [
        "onStartupFinished"
    ],

來到package.json檔案裡面, 修改一下這裡外掛啟用的配置:

image.png

image.png

onStartupFinished 規定了外掛在vscode初始化啟動完成後啟動,它不會佔用使用者初始化vscode時的效能, 並且我們的語音註釋也並不是那種很緊急啟動的程式。

來到extension.ts檔案裡面, 將無用的程式碼全部清空

清空前

image.png

清空後

image.png

     執行yarn watch後開始編譯程式碼,fn + F5除錯程式碼會報錯:
image.png

     看到上面的報錯我也很蒙, 之前用原生js寫好像沒遇到過, 我還給vscode編輯器升了級還是報錯, 然後就去程式碼裡查了一下:

image.png

     這就很明朗了, 我們圈定一個相容範圍就好了, 檢視了一下官網日誌, 最後選定了最低2020年 10月左右的版本。

    "engines": {
        "vscode": "^1.50.0"
    },

果然再次除錯就可以出現提示框了:
image.png

要注意:
     下方的@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作用域

     它的作用於jststsx檔案。

  "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"
      }
    }
  }
}
  1. scopeName顧名思義定義了作用域名稱。
  2. injectionSelector 定義了作用域生效的範圍, 這裡的意思就是對 // 後面的內容生效。
  3. jojo.match進行關鍵詞的匹配。
  4. jojo.name這個有點特殊, 它是定義這個關鍵字樣式的, 但並不是普通的css樣式, 而是需要你在定義語言樣式的檔案裡定義好, 然後在這邊只能進行使用。

image.png

     這種方案弊端很多, 比如不好'定製樣式', 不好動態更改設定等等, 最後我並沒有選擇這種實現方式。

五、 方案二: 文件檢索

     也就是獲取當前所在'文件'的所有文字內容, 為其賦予類似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() { }
  1. context.subscriptions.push 是在註冊命令, 這裡是註冊了一個生命週期事件。
  2. 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)
    })}

這裡的知識點很有意思, 配合截圖講一下

  1. vscode.window.visibleTextEditors 你可以理解為獲取到當前'啟用'的程式碼編輯頁的屬性, 裡面包括當前翻閱到了第幾行, 某些樣式是否啟用等屬性。
  2. res.document.getText()獲取當前啟用頁面的內容。
  3. vscode.window.createTextEditorDecorationType製造一個樣式, 這裡只定義固定的幾種樣式, 但是厲害的點是這裡可以製造'偽元素'這樣玩法就多樣性了, 我們可以把它的返回結果列印看看。
    middle_img_v2_44ef2b11-ceec-4921-8544-b51955a04b7g.jpg

    這裡返回的key其實是個className

  4. ranges用來記錄voice-annotation所在的起始位置與結束位置。
  5. 之所以選擇while迴圈來處理, 是因為可能存在多個voice-annotation
  6. n.index + 3 是因為// voice-annotation的前面是// 這個我選擇保留原樣。

image.png

具體的調整樣式我們可以通過vscode開發者工具

image.png

image.png

六、懸停// 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('播放中 ...')
        }
    }
})
  1. documennt.getText(documennt.getWordRangeAtPosition(position))可以獲取到當前hover的文字內容。
  2. testTargetReg.exec(word) 校驗當前hover的文字是否為目標文字。
  3. return new vscode.Hover('播放中 ...') 這裡返回的文字只能是markdown的格式。
  4. stopFn是後期要做的停止播放邏輯, 因為有的音訊可能很長, 聽了一半我們不想聽了, 亦或者聽著某個音訊的同時我又懸停在了另一個語音註釋上, 那麼我們應該停止上一個音訊並播放當前的音訊。

end

     下一篇該講到vscode如何播放聲音, 音訊檔案如何傳遞與儲存, 等多坑點等我來跳 這次就是這樣, 希望與你一起進步。

相關文章