🚀 在 VS Code 中取得

教學課程:使用語言模型 API 產生 AI 驅動的程式碼註解

在本教學課程中,您將學習如何建立 VS Code 擴充功能來建置 AI 驅動的程式碼導師。您將使用語言模型 (LM) API 產生建議來改進您的程式碼,並利用 VS Code 擴充功能 API 將其無縫整合到編輯器中,作為使用者可以將滑鼠游標停留在上方以取得更多資訊的內嵌註解。完成本教學課程後,您將了解如何在 VS Code 中實作自訂 AI 功能。

VS Code displaying custom annotations from GitHub Copilot as annotations

先決條件

您需要下列工具和帳戶才能完成本教學課程

搭建擴充功能

首先,使用 Yeoman 和 VS Code 擴充功能產生器來搭建一個適用於開發的 TypeScript 或 JavaScript 專案。

npx --package yo --package generator-code -- yo code

選取下列選項以完成新的擴充功能精靈...

# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? Code Tutor

### Press <Enter> to choose default for all options below ###

# ? What's the identifier of your extension? code-tutor
# ? What's the description of your extension? LEAVE BLANK
# ? Initialize a git repository? Yes
# ? Bundle the source code with webpack? No
# ? Which package manager to use? npm

# ? Do you want to open the new folder with Visual Studio Code? Open with `code`

修改 package.json 檔案以包含正確的命令

搭建的專案在 package.json 檔案中包含單一「helloWorld」命令。此命令是您的擴充功能安裝後在命令面板中顯示的內容。

"contributes": {
  "commands": [
      {
      "command": "code-tutor.helloWorld",
      "title": "Hello World"
      }
  ]
}

由於我們正在建置程式碼導師擴充功能,它將為程式碼行新增註解,因此我們需要一個命令來允許使用者開啟和關閉這些註解。更新 commandtitle 屬性

"contributes": {
  "commands": [
      {
      "command": "code-tutor.annotate",
      "title": "Toggle Tutor Annotations"
      }
  ]
}

雖然 package.json 定義了擴充功能的命令和 UI 元素,但 src/extension.ts 檔案是您放置應針對這些命令執行的程式碼的位置。

開啟 src/extension.ts 檔案並變更 registerCommand 方法,使其符合 package.json 檔案中的 command 屬性。

const disposable = vscode.commands.registerCommand('code-tutor.annotate', () => {

按下 F5 來執行擴充功能。這將開啟一個安裝了擴充功能的新 VS Code 執行個體。按下 ⇧⌘P (Windows、Linux Ctrl+Shift+P) 開啟命令面板,並搜尋「tutor」。您應該會看到「Tutor Annotations」命令。

The "Toggle Tutor Annotations" command in the VS Code Command Palette

如果您選取「Tutor Annotations」命令,您會看到「Hello World」通知訊息。

The message 'Hello World from Code Tutor' displayed in a notification

實作「annotate」命令

為了讓我們的程式碼導師註解運作,我們需要將一些程式碼傳送給它,並要求它提供註解。我們將分三個步驟完成此操作

  1. 從使用者開啟的目前索引標籤取得包含行號的程式碼。
  2. 將該程式碼連同自訂提示傳送至語言模型 API,指示模型如何提供註解。
  3. 剖析註解並將其顯示在編輯器中。

步驟 1:取得包含行號的程式碼

若要從目前的索引標籤取得程式碼,我們需要參考使用者開啟的索引標籤。我們可以透過修改 registerCommand 方法為 registerTextEditorCommand 來取得該參考。這兩個命令之間的差異在於,後者為我們提供了使用者開啟的索引標籤的參考,稱為 TextEditor

const disposable = vscode.commands.registerTextEditorCommand('code-tutor.annotate', async (textEditor: vscode.TextEditor) => {

現在我們可以使用 textEditor 參考來取得「可檢視編輯器空間」中的所有程式碼。這是可以在畫面上看到的程式碼 - 它不包含可檢視編輯器空間上方或下方的程式碼。

將下列方法直接新增到 extension.ts 檔案底部的 export function deactivate() { } 行上方。

function getVisibleCodeWithLineNumbers(textEditor: vscode.TextEditor) {
  // get the position of the first and last visible lines
  let currentLine = textEditor.visibleRanges[0].start.line;
  const endLine = textEditor.visibleRanges[0].end.line;

  let code = '';

  // get the text from the line at the current position.
  // The line number is 0-based, so we add 1 to it to make it 1-based.
  while (currentLine < endLine) {
    code += `${currentLine + 1}: ${textEditor.document.lineAt(currentLine).text} \n`;
    // move to the next line position
    currentLine++;
  }
  return code;
}

此程式碼使用 TextEditor 的 visibleRanges 屬性來取得目前在編輯器中可見的程式碼行位置。然後,它從第一行位置開始並移動到最後一行位置,將每一行程式碼連同行號新增至字串。最後,它會傳回包含所有可檢視程式碼以及行號的字串。

現在我們可以從 code-tutor.annotate 命令呼叫此方法。修改命令的實作,使其看起來像這樣

const disposable = vscode.commands.registerTextEditorCommand(
  'code-tutor.annotate',
  async (textEditor: vscode.TextEditor) => {
    // Get the code with line numbers from the current editor
    const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor);
  }
);

步驟 2:將程式碼和提示傳送至語言模型 API

下一步是呼叫 GitHub Copilot 語言模型,並將使用者的程式碼連同建立註解的指示傳送給它。

若要執行此操作,我們首先需要指定要使用的聊天模型。我們在此處選取 4o,因為它對於我們正在建置的互動類型來說是一個快速且功能強大的模型。

const disposable = vscode.commands.registerTextEditorCommand(
  'code-tutor.annotate',
  async (textEditor: vscode.TextEditor) => {
    // Get the code with line numbers from the current editor
    const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor);

    // select the 4o chat model
    let [model] = await vscode.lm.selectChatModels({
      vendor: 'copilot',
      family: 'gpt-4o'
    });
  }
);

我們需要指示 - 或「提示」- 來告知模型建立註解,以及我們希望回應採用的格式。將下列程式碼新增至檔案頂端,緊接在匯入下方。

const ANNOTATION_PROMPT = `You are a code tutor who helps students learn how to write better code. Your job is to evaluate a block of code that the user gives you and then annotate any lines that could be improved with a brief suggestion and the reason why you are making that suggestion. Only make suggestions when you feel the severity is enough that it will impact the readability and maintainability of the code. Be friendly with your suggestions and remember that these are students so they need gentle guidance. Format each suggestion as a single JSON object. It is not necessary to wrap your response in triple backticks. Here is an example of what your response should look like:

{ "line": 1, "suggestion": "I think you should use a for loop instead of a while loop. A for loop is more concise and easier to read." }{ "line": 12, "suggestion": "I think you should use a for loop instead of a while loop. A for loop is more concise and easier to read." }
`;

這是一個特殊的提示,指示語言模型如何產生註解。它也包含模型應如何格式化其回應的範例。這些範例 (也稱為「多樣本」) 讓我們能夠定義回應將採用的格式,以便我們可以剖析它並將其顯示為註解。

我們在陣列中將訊息傳遞至模型。此陣列可以包含您想要的任意多個訊息。在我們的案例中,它包含提示,後接使用者的程式碼以及行號。

const disposable = vscode.commands.registerTextEditorCommand(
  'code-tutor.annotate',
  async (textEditor: vscode.TextEditor) => {
    // Get the code with line numbers from the current editor
    const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor);

    // select the 4o chat model
    let [model] = await vscode.lm.selectChatModels({
      vendor: 'copilot',
      family: 'gpt-4o'
    });

    // init the chat message
    const messages = [
      vscode.LanguageModelChatMessage.User(ANNOTATION_PROMPT),
      vscode.LanguageModelChatMessage.User(codeWithLineNumbers)
    ];
  }
);

若要將訊息傳送至模型,我們需要先確保選取的模型可用。這會處理擴充功能尚未準備好或使用者未登入 GitHub Copilot 的情況。然後,我們將訊息傳送至模型。

const disposable = vscode.commands.registerTextEditorCommand(
  'code-tutor.annotate',
  async (textEditor: vscode.TextEditor) => {
    // Get the code with line numbers from the current editor
    const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor);

    // select the 4o chat model
    let [model] = await vscode.lm.selectChatModels({
      vendor: 'copilot',
      family: 'gpt-4o'
    });

    // init the chat message
    const messages = [
      vscode.LanguageModelChatMessage.User(ANNOTATION_PROMPT),
      vscode.LanguageModelChatMessage.User(codeWithLineNumbers)
    ];

    // make sure the model is available
    if (model) {
      // send the messages array to the model and get the response
      let chatResponse = await model.sendRequest(
        messages,
        {},
        new vscode.CancellationTokenSource().token
      );

      // handle chat response
      await parseChatResponse(chatResponse, textEditor);
    }
  }
);

聊天回應以片段形式傳入。這些片段通常包含單字,但有時只包含標點符號。為了在回應串流傳入時顯示註解,我們希望等到我們擁有完整的註解再顯示它。由於我們已指示模型傳回其回應的方式,因此我們知道當我們看到結束 } 時,我們擁有完整的註解。然後,我們可以剖析註解並將其顯示在編輯器中。

extension.ts 檔案中的 getVisibleCodeWithLineNumbers 方法上方新增遺失的 parseChatResponse 函式。

async function parseChatResponse(
  chatResponse: vscode.LanguageModelChatResponse,
  textEditor: vscode.TextEditor
) {
  let accumulatedResponse = '';

  for await (const fragment of chatResponse.text) {
    accumulatedResponse += fragment;

    // if the fragment is a }, we can try to parse the whole line
    if (fragment.includes('}')) {
      try {
        const annotation = JSON.parse(accumulatedResponse);
        applyDecoration(textEditor, annotation.line, annotation.suggestion);
        // reset the accumulator for the next line
        accumulatedResponse = '';
      } catch (e) {
        // do nothing
      }
    }
  }
}

我們需要最後一個方法來實際顯示註解。VS Code 將這些稱為「裝飾」。在 extension.ts 檔案中的 parseChatResponse 方法上方新增下列方法。

function applyDecoration(editor: vscode.TextEditor, line: number, suggestion: string) {
  const decorationType = vscode.window.createTextEditorDecorationType({
    after: {
      contentText: ` ${suggestion.substring(0, 25) + '...'}`,
      color: 'grey'
    }
  });

  // get the end of the line with the specified line number
  const lineLength = editor.document.lineAt(line - 1).text.length;
  const range = new vscode.Range(
    new vscode.Position(line - 1, lineLength),
    new vscode.Position(line - 1, lineLength)
  );

  const decoration = { range: range, hoverMessage: suggestion };

  vscode.window.activeTextEditor?.setDecorations(decorationType, [decoration]);
}

此方法會接收來自模型的已剖析註解,並使用它來建立裝飾。這是透過先建立 TextEditorDecorationType 來完成,該類型指定裝飾的外觀。在本案例中,我們只是新增灰色註解並將其截斷為 25 個字元。當使用者將滑鼠游標停留在訊息上方時,我們會顯示完整訊息。

然後,我們設定裝飾應顯示的位置。我們需要它位於註解中指定的行號上,並位於行尾。

最後,我們在作用中的文字編輯器上設定裝飾,這會導致註解顯示在編輯器中。

如果您的擴充功能仍在執行中,請從偵錯工具列中選取綠色箭頭來重新啟動它。如果您關閉了偵錯工作階段,請按下 F5 來執行擴充功能。在新開啟的 VS Code 視窗執行個體中開啟程式碼檔案。當您從命令面板中選取「Toggle Tutor Annotations」時,您應該會看到程式碼註解顯示在編輯器中。

A code file with annotations from GitHub Copilot

在編輯器標題列中新增按鈕

您可以讓您的命令從命令面板以外的其他位置叫用。在我們的案例中,我們可以將按鈕新增至目前索引標籤的頂端,讓使用者可以輕鬆切換註解。

若要執行此操作,請如下修改 package.json 的「contributes」部分

"contributes": {
  "commands": [
    {
      "command": "code-tutor.annotate",
      "title": "Toggle Tutor Annotations",
      "icon": "$(comment)"
    }
  ],
  "menus": {
    "editor/title": [
      {
        "command": "code-tutor.annotate",
        "group": "navigation"
      }
    ]
  }
}

這會導致按鈕顯示在編輯器標題列的導覽區域 (右側)。「icon」來自產品圖示參考

使用綠色箭頭重新啟動您的擴充功能,或在擴充功能尚未執行時按下 F5。您現在應該會看到一個註解圖示,它將觸發「Toggle Tutor Annotations」命令。

A comment icon appears in the title bar of the active tab in VS Code

後續步驟

在本教學課程中,您學習了如何建立 VS Code 擴充功能,以使用語言模型 API 將 AI 整合到編輯器中。您使用了 VS Code 擴充功能 API 從目前索引標籤取得程式碼,將其連同自訂提示傳送至模型,然後剖析模型結果並使用裝飾直接顯示在編輯器中。

接下來,您可以擴充您的程式碼導師擴充功能,以包含聊天參與者,這也將允許使用者透過 GitHub Copilot 聊天介面直接與您的擴充功能互動。您也可以探索 VS Code 中的完整 API 範圍,以探索建置自訂 AI 編輯器體驗的新方法。

您可以在 vscode-extensions-sample 儲存庫中找到本教學課程的完整原始碼。