@
- 建立和插入程式碼片段
- 程式碼片段列表
- 程式碼片段預覽
- 程式碼片段編輯
- 自定義對映
- 預設對映
- 自動完成
- 專案地址
建立和插入程式碼片段
VS Code擴充套件提供了資料儲存,其中globalState是使用全域性儲存的Key-Value方式來儲存使用者狀態,支援在不同計算機上保留某些使用者狀態,詳情請參考官方文件
若在編輯器區域有選中的文字,點選右鍵選單中點選建立Snippet,則呼叫extension.snippetCraft.createSnipp
命令,執行建立程式碼片段。
建立服務類 SnippService.ts
,程式碼如下
export async function AddSnipp(context: ExtensionContext, state: Partial<ISnipp>) {
const content = await getSnippText();
const trimmedName = content?.text?.trim().substring(0, 20) || '';
await _addOrUpdateSnipp(context, { ...state, name: trimmedName }, content)
}
在_addOrUpdateSnipp
方法中對snipps
進行更新操作
async function _addOrUpdateSnipp(context: ExtensionContext, state: Partial<ISnipp>, content?: {
text: string | undefined;
type: string | undefined;
}, snippIndex?: number) {
...
context.globalState.update("snipps", updatedSnipps);
若在編輯器區域右鍵選單中點選插入Snippet,或在程式碼片段檢視中點選條目,則呼叫extension.snippetCraft.insertSnipps
命令,它會呼叫InsertSnipp
方法執行插入程式碼片段操作。
在服務類 SnippService.ts
,插入如下程式碼
export async function InsertSnipp(context: ExtensionContext, snipp: ISnipp) {
const editor = window.activeTextEditor;
if (editor && SnippDataProvider.isSnipp(snipp)) {
const position = editor?.selection.active;
editor.edit(async (edit) => {
edit.insert(position, snipp.content || '');
});
}
}
程式碼片段列表
程式碼片段顯示為一個樹形結構,根據建立時的檔案內容型別,分組顯示程式碼片段條目
建立程式碼片段和分組條目的介面型別
import * as vscode from "vscode";
export interface ISnipp {
name: string;
content: string;
contentType: string;
created: Date;
lastUsed: Date;
}
export interface IGroup {
name: string;
contentType: string | undefined;
}
在SnippItem中建立獲取所有分組型別的get訪問器,和獲取分組下的條目getChildren方法
export class SnippItem {
constructor(
readonly view: string,
private context: vscode.ExtensionContext
) { }
public get roots(): Thenable<IGroup[]> {
const snipps = this.context?.globalState?.get("snipps", []);
const types = snipps
.map((snipp: ISnipp) => snipp.contentType)
.filter((value, index, self) => self.indexOf(value) === index)
.map((type) => ({ name: type, contentType: undefined }));
return Promise.resolve(types);
}
public getChildren(node: IGroup): Thenable<ISnipp[]> {
const snipps = this.context?.globalState
?.get("snipps", [])
.filter((snipp: ISnipp) => {
return snipp.contentType === node.name;
})
.sort((a: ISnipp, b: ISnipp) => a.name.localeCompare(b.name));
return Promise.resolve(snipps);
}
export class GroupItem { }
VS Code擴充套件的側邊欄中顯示內容需為樹形結構,透過實現TreeDataProvider
為內容提供資料,請參考官方說明
實現getChildren方法
export class SnippDataProvider
implements
vscode.TreeDataProvider<ISnipp | IGroup>
{
public getChildren(
element?: ISnipp | IGroup
): ISnipp[] | Thenable<ISnipp[]> | IGroup[] | Thenable<IGroup[]> {
return element ? this.model.getChildren(element) : this.model.roots;
}
}
程式碼片段預覽
實現getTreeItem方法,顯示預覽
點選時呼叫extension.snippetCraft.insertEntry
命令實現插入程式碼片段,command部分在上一章節有介紹。
滑鼠移動到程式碼片段條目上時,顯示tooltip預覽
程式碼如下:
public getTreeItem(element: ISnipp | IGroup): vscode.TreeItem {
const t = element.name;
const isSnip = SnippDataProvider.isSnipp(element);
const snippcomm = {
command: "extension.snippetCraft.insertEntry",
title: '',
arguments: [element],
};
let snippetInfo: string = `[${element.contentType}] ${element.name}`;
return {
// @ts-ignore
label: isSnip ? element.name : element.name,
command: isSnip ? snippcomm : undefined,
iconPath:isSnip ? new ThemeIcon("code"):new ThemeIcon("folder"),
tooltip: isSnip
? new vscode.MarkdownString(
// @ts-ignore
`**標題:**${snippetInfo}\n\n**修改時間:**${element.created}\n\n**最近使用:**${element.lastUsed}\n\n**預覽:**\n\`\`\`${element.contentType}\n${element.content}\n\`\`\``
)
: undefined,
collapsibleState: !isSnip
? vscode.TreeItemCollapsibleState.Collapsed
: undefined,
};
}
程式碼片段編輯
編輯器是一個輸入框,由於VS Code的輸入框不支援多行輸入,所以需要使用webview實現多行輸入。同時需要提交按鈕與取消按鈕
首先建立一個多行文字框的WebView,
在服務類 SnippService.ts
,建立一個函式getWebviewContent
,返回一個HTML字串,用於建立一個多行輸入框。
function getWebviewContent(placeholder: string, initialValue: string): string {
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multiline Input</title>
</head>
<body>
<textarea id="inputBox" rows="10" cols="50" placeholder="${placeholder}">${initialValue}</textarea>
<br>
<button onclick="submitText()">提交</button>
<button onclick="cancel()">取消</button>
<script>
const vscode = acquireVsCodeApi();
function submitText() {
const text = document.getElementById('inputBox').value;
vscode.postMessage({ command: 'submit', text: text });
}
function cancel() {
vscode.postMessage({ command: 'cancel' });
}
</script>
</body>
</html>
`;
}
新增處理函式,當使用者點選“提交“時,將文字輸入框中的內容返回,同時關閉輸入框視窗。
async function showInputBoxWithMultiline(context: ExtensionContext, placeholder: string, initialValue: string): Promise<string | undefined> {
const panel = window.createWebviewPanel(
'multilineInput',
'Multiline Input',
ViewColumn.One,
{
enableScripts: true
}
);
panel.webview.html = getWebviewContent(placeholder, initialValue);
return new Promise<string | undefined>((resolve) => {
panel.webview.onDidReceiveMessage(
message => {
switch (message.command) {
case 'submit':
resolve(message.text);
panel.dispose();
return;
case 'cancel':
resolve(undefined);
panel.dispose();
return;
}
},
undefined,
context.subscriptions
);
});
}
在新增程式碼片段和編輯程式碼片段時觸發函式
export async function AddSnippFromEditor(context: ExtensionContext, state: Partial<ISnipp>) {
const content = await showInputBoxWithMultiline(context, '請輸入Snippet內容', '');
if (content) {
_addOrUpdateSnipp(context, state, { text: content, type: "TEXT" })
}
}
export async function EditSnipp(context: ExtensionContext, state: Partial<ISnipp>, snippIndex: number) {
const content = await showInputBoxWithMultiline(context, '請輸入Snippet內容', state.content ?? '');
if (content) {
_addOrUpdateSnipp(context, state, { text: content, type: state.contentType ?? "TEXT" }, snippIndex)
}
}
自定義對映
對映是插入程式碼片段時,自動替換的變數,他們透過Key-Value形式儲存於globalState中。
程式碼片段中透過設定佔位符(如${AUTHOR}
),在插入程式碼片段時,將自動替換為全域性變數中的值。
當自定義對映值未設定或者不可用時,將直接顯示變數佔位符
擴充套件初始化時,插入了三個常用的自定義對映,你可以自由更改或新增自定義對映。
${AUTHOR}
: 作者姓名${COMPANY}
: 公司名稱${MAIL}
: 郵箱地址
擴充套件中所有的自定義對映,呈現於“對映表”樹檢視中。
示例:
程式碼片段內容
value of 'AUTHOR' is: ${AUTHOR}
value of 'COMPANY' is: ${COMPANY}
value of 'MAIL' is: ${MAIL}
value of 'FOOBAR' (non-exist) is: ${FOOBAR}
插入程式碼片段後,顯示如下:
value of 'AUTHOR' is: 林曉lx
value of 'COMPANY' is: my-company
value of 'MAIL' is: jevonsflash@qq.com
value of 'FOOBAR' (non-exist) is: ${FOOBAR}
首先定義KVItem類:
export class KVItem extends vscode.TreeItem {
constructor(
public readonly key: string,
public readonly value: string | undefined
) {
super(key, vscode.TreeItemCollapsibleState.None);
this.tooltip = `${this.key}: ${this.value}`;
this.description = this.value;
this.contextValue = 'kvItem';
}
}
“對映表”樹檢視中顯示內容需為樹形結構,同樣需要定義KVTreeDataProvider
,在此實現重新整理、新增、刪除、獲取子節點等方法。
export class KVTreeDataProvider implements vscode.TreeDataProvider<KVItem> {
private _onDidChangeTreeData: vscode.EventEmitter<KVItem | undefined> = new vscode.EventEmitter<KVItem | undefined>();
readonly onDidChangeTreeData: vscode.Event<KVItem | undefined> = this._onDidChangeTreeData.event;
constructor(private globalState: vscode.Memento) {}
getTreeItem(element: KVItem): vscode.TreeItem {
return element;
}
getChildren(element?: KVItem): Thenable<KVItem[]> {
if (element) {
return Promise.resolve([]);
} else {
const kvObject = this.globalState.get<{ [key: string]: string }>('key-value', {});
const keys = Object.keys(kvObject);
return Promise.resolve(keys.map(key => new KVItem(key, kvObject[key])));
}
}
refresh(): void {
this._onDidChangeTreeData.fire(undefined);
}
addOrUpdateKey(key: string, value: string): void {
const kvObject = this.globalState.get<{ [key: string]: string }>('key-value', {});
kvObject[key] = value;
this.globalState.update('key-value', kvObject);
this.refresh();
}
deleteKey(key: string): void {
const kvObject = this.globalState.get<{ [key: string]: string }>('key-value', {});
delete kvObject[key];
this.globalState.update('key-value', kvObject);
this.refresh();
}
}
預設對映
預設對映是擴充套件內建的對映功能,可用的對映如下
檔案和編輯器相關:
- TM_SELECTED_TEXT: 當前選定的文字或空字串
- TM_CURRENT_LINE: 當前行的內容
- TM_CURRENT_WORD: 游標下的單詞或空字串的內容
- TM_LINE_INDEX: 基於零索引的行號
- TM_LINE_NUMBER: 基於一個索引的行號
- TM_FILENAME: 當前文件的檔名
- TM_FILENAME_BASE: 當前文件的檔名(不含副檔名)
- TM_DIRECTORY: 當前文件的目錄
- TM_FILEPATH: 當前文件的完整檔案路徑
- RELATIVE_FILEPATH: 當前文件的相對檔案路徑(相對於開啟的工作區或資料夾)
- CLIPBOARD: 剪貼簿的內容
- WORKSPACE_NAME: 開啟的工作區或資料夾的名稱
- WORKSPACE_FOLDER: 開啟的工作區或資料夾的路徑
- CURSOR_INDEX: 基於零索引的遊標編號
- CURSOR_NUMBER: 基於單索引的遊標編號
時間相關:
- CURRENT_YEAR: 本年度
- CURRENT_YEAR_SHORT: 當年的最後兩位數字
- CURRENT_MONTH: 兩位數字的月份(例如“02”)
- CURRENT_MONTH_NAME: 月份的全名(例如“July”)
- CURRENT_MONTH_NAME_SHORT: 月份的簡短名稱(例如“Jul”)
- CURRENT_DATE: 以兩位數字表示的月份中的某一天(例如“08”)
- CURRENT_DAY_NAME: 日期的名稱(例如“星期一”)
- CURRENT_DAY_NAME_SHORT: 當天的簡短名稱(例如“Mon”)
- CURRENT_HOUR24: 小時制格式的當前小時
- CURRENT_MINUTE: 兩位數的當前分鐘數
- CURRENT_SECOND: 當前秒數為兩位數
- CURRENT_SECONDS_UNIX: 自 Unix 紀元以來的秒數
- CURRENT_TIMEZONE_OFFSET當前 UTC 時區偏移量為 +HH:MM 或者 -HH:MM (例如“-07:00”)。
其他:
- RANDOM6: 個隨機 Base-10 數字
- RANDOM_HEX6: 個隨機 Base-16 數字
- UUID: 第四版UUID
這些專案參考至VS Code 程式碼片段變數,請檢視VSCode官方文件
與自定義對映一樣,當預設對映值未設定或者不可用時,將直接顯示變數佔位符
實現方法如下:
export async function ReplacePlaceholders(text: string, context: ExtensionContext): Promise<string> {
const editor = window.activeTextEditor;
const clipboard = await env.clipboard.readText();
const workspaceFolders = workspace.workspaceFolders;
const currentDate = new Date();
const kvObject = context.globalState.get<{ [key: string]: string }>('key-value', {});
const replacements: { [key: string]: string } = {
'${TM_SELECTED_TEXT}': editor?.document.getText(editor.selection) || '',
'${TM_CURRENT_LINE}': editor?.document.lineAt(editor.selection.active.line).text || '',
'${TM_CURRENT_WORD}': editor?.document.getText(editor.document.getWordRangeAtPosition(editor.selection.active)) || '',
'${TM_LINE_INDEX}': (editor?.selection.active.line ?? 0).toString(),
'${TM_LINE_NUMBER}': ((editor?.selection.active.line ?? 0) + 1).toString(),
'${TM_FILENAME}': editor ? path.basename(editor.document.fileName) : '',
'${TM_FILENAME_BASE}': editor ? path.basename(editor.document.fileName, path.extname(editor.document.fileName)) : '',
'${TM_DIRECTORY}': editor ? path.dirname(editor.document.fileName) : '',
'${TM_FILEPATH}': editor?.document.fileName || '',
'${RELATIVE_FILEPATH}': editor && workspaceFolders ? path.relative(workspaceFolders[0].uri.fsPath, editor.document.fileName) : '',
'${CLIPBOARD}': clipboard,
'${WORKSPACE_NAME}': workspaceFolders ? workspaceFolders[0].name : '',
'${WORKSPACE_FOLDER}': workspaceFolders ? workspaceFolders[0].uri.fsPath : '',
'${CURSOR_INDEX}': (editor?.selections.indexOf(editor.selection) ?? 0).toString(),
'${CURSOR_NUMBER}': ((editor?.selections.indexOf(editor.selection) ?? 0) + 1).toString(),
'${CURRENT_YEAR}': currentDate.getFullYear().toString(),
'${CURRENT_YEAR_SHORT}': currentDate.getFullYear().toString().slice(-2),
'${CURRENT_MONTH}': (currentDate.getMonth() + 1).toString().padStart(2, '0'),
'${CURRENT_MONTH_NAME}': currentDate.toLocaleString('default', { month: 'long' }),
'${CURRENT_MONTH_NAME_SHORT}': currentDate.toLocaleString('default', { month: 'short' }),
'${CURRENT_DATE}': currentDate.getDate().toString().padStart(2, '0'),
'${CURRENT_DAY_NAME}': currentDate.toLocaleString('default', { weekday: 'long' }),
'${CURRENT_DAY_NAME_SHORT}': currentDate.toLocaleString('default', { weekday: 'short' }),
'${CURRENT_HOUR}': currentDate.getHours().toString().padStart(2, '0'),
'${CURRENT_MINUTE}': currentDate.getMinutes().toString().padStart(2, '0'),
'${CURRENT_SECOND}': currentDate.getSeconds().toString().padStart(2, '0'),
'${CURRENT_SECONDS_UNIX}': Math.floor(currentDate.getTime() / 1000).toString(),
'${CURRENT_TIMEZONE_OFFSET}': formatTimezoneOffset(currentDate.getTimezoneOffset()),
'${RANDOM}': Math.random().toString().slice(2, 8),
'${RANDOM_HEX}': Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0'),
'${UUID}': generateUUID()
};
Object.keys(kvObject).forEach(key => {
replacements[`$\{${key}\}`] = kvObject[key];
});
return text.replace(/\$\{(\w+)\}/g, (match, key) => {
return replacements[match] || match;
});
}
自動完成
自動完成是VS Code編輯器提供的一個功能,用於在編輯器中顯示自動提示和補全內容。擴充套件提供了基於程式碼片段的自動完成功能。
CompletionItemProvider
用於註冊自動完成的規則,提供者約定了在指定的文件型別下,當輸入的字元匹配時,將出現自動完成上下文選單。
上下文選單中列出所有可用的自動完成條目,每個條目由CompletionItem
定義,點選對應條目後,將處理後的字串返回,填寫到編輯器當前游標處。
languages.registerCompletionItemProvider用於註冊自動完成的規則提供者。
在extension.ts
中註冊初始化時,所有的自動完成條目
const providers = contentTypes
.filter((value, index, self) => self.indexOf(value) === index)
.map(type =>
languages.registerCompletionItemProvider(type, {
provideCompletionItems(
document: TextDocument,
position: Position,
token: CancellationToken,
context: CompletionContext
) {
return new Promise<CompletionItem[]>((resolve, reject) => {
var result = snipps
.filter((snipp: ISnipp) => {
return snipp.contentType === type;
})
.map(async (snipp: ISnipp) => {
const replacedContentText = await ReplacePlaceholders(snipp.content, extensionContext);
const commandCompletion = new CompletionItem(snipp.name);
commandCompletion.insertText = replacedContentText || '';
return commandCompletion;
});
Promise.all(result).then(resolve);
});
}
})
);
context.subscriptions.push(...providers);
SnippService.ts
_addOrUpdateSnipp方法中配置修改或新增的自動完成條目
if (content?.type && state.name) {
languages.registerCompletionItemProvider(content.type, {
provideCompletionItems(
document: TextDocument,
position: Position,
token: CancellationToken,
context: CompletionContext
) {
return new Promise<CompletionItem[]>((resolve, reject) => {
ReplacePlaceholders(state.content || '', extensionContext).then(res => {
const replacedContentText = res;
const commandCompletion = new CompletionItem(state.name || '');
commandCompletion.insertText = replacedContentText || '';
resolve([commandCompletion]);
});
});
}
});
}
專案地址
Github:snippet-craft