通过凌鲨本地接口实现vscode插件

凌鲨的vscode插件是基于凌鲨本地API实现的。源代码是开源的。

开发流程

生成本地API的ts代码

本地接口定义使用openapi定义的。我们使用boats来定义和生成openapi的描述文件。 我们使用git subtree 把proto定义引入到我们的插件项目中。然后通过@openapitools/openapi-generator-cli生成ts代码

bash 复制代码
npx @openapitools/openapi-generator-cli generate -i proto/index_0.1.19.yml -g typescript-axios -o src/proto-gen

搭建代码框架

在extension.ts里面注册相关命令和数据提供器。

typescript 复制代码
export function activate(context: vscode.ExtensionContext) {
    //初始化配置命令
	vscode.commands.registerCommand('linksaas.initCfg', () => initProjectTokenCfg());

    //缺陷数据提供器
	const myBugProvider = new MyBugProvider();
	vscode.window.registerTreeDataProvider("myBugList", myBugProvider);
	vscode.commands.registerCommand('myBugList.sync', () => myBugProvider.sync());
	vscode.commands.registerCommand('myBugList.showItem', (item: BugItem) => item.show());
	vscode.commands.registerCommand('myBugList.shortNote', (item: BugItem) => item.shortNote());

    //任务数据提供器
	const myTaskProvider = new MyTaskProvider();
	vscode.window.registerTreeDataProvider("myTaskList", myTaskProvider);
	vscode.commands.registerCommand('myTaskList.sync', () => myTaskProvider.sync());
	vscode.commands.registerCommand('myTaskList.showItem', (item: TaskItem) => item.show());
	vscode.commands.registerCommand('myTaskList.shortNote', (item: TaskItem) => item.shortNote());

    //微应用数据提供器
	const minappProvider = new MinappProvider();
	vscode.window.registerTreeDataProvider("minappList", minappProvider);
	vscode.commands.registerCommand('minappList.sync', () => minappProvider.sync());
	vscode.commands.registerCommand('minappList.showItem', (item: MinappItem) => item.show());

    //注册代码评论相关命令
	vscode.commands.registerCommand('codeComment.newThread', () => createThread());
	const commentController = vscode.comments.createCommentController("linksaas", "linksaas comment controller");
	threadManager.init(commentController);
	commentController.commentingRangeProvider = {
		provideCommentingRanges(document: vscode.TextDocument, _token: vscode.CancellationToken): vscode.ProviderResult<vscode.Range[]> {
			threadManager.onDocChange(document);
			return null;
		}
	};
	vscode.commands.registerCommand("codeComment.syncThread", (thread: vscode.CommentThread) => threadManager.syncThread(thread));
	vscode.commands.registerCommand("codeComment.add", (reply: vscode.CommentReply) => threadManager.addComment(reply));
	vscode.commands.registerCommand("codeComment.update", (comment: threadManager.CodeComment) => threadManager.setEditComment(comment));
	vscode.commands.registerCommand("codeComment.remove", (comment: threadManager.CodeComment) => threadManager.removeComment(comment));
	vscode.commands.registerCommand("codeComment.cancelSave", (comment: threadManager.CodeComment) => threadManager.cancelSaveComment(comment));
	vscode.commands.registerCommand("codeComment.save", (comment: threadManager.CodeComment) => threadManager.saveComment(comment));
}

配置界面布局和命令关联

vscode的配置通过扩展了package.json来实现的。

json 复制代码
{
  "name": "local-api",
  "displayName": "linksaas local-api",
  "description": "linksaas local-api",
  "version": "0.1.2",
  "publisher": "linksaas",
  "repository": {
    "type": "git",
    "url": "https://atomgit.com/openlinksaas/vs-extension"
  },
  "engines": {
    "vscode": "^1.75.0"
  },
  "categories": [
    "Other"
  ],
  "main": "./out/extension.js",
  "activationEvents": [
    "onStartupFinished"
  ],
  "contributes": {
    "commands": [
      {
        "command": "linksaas.initCfg",
        "title": "linksaas: init linksaas cfg"
      },
      {
        "command": "myBugList.sync",
        "title": "linksaas: sync my bug list",
        "icon": {
          "light": "resources/light/refresh.svg",
          "dark": "resources/dark/refresh.svg"
        }
      },
      {
        "command": "myBugList.showItem",
        "title": "linksaas: show detail bug",
        "icon": "$(link)"
      },
      {
        "command": "myBugList.shortNote",
        "title": "linksaas: show bug as short note",
        "icon": "$(link-external)"
      },
      {
        "command": "myTaskList.sync",
        "title": "linksaas: sync my task list",
        "icon": {
          "light": "resources/light/refresh.svg",
          "dark": "resources/dark/refresh.svg"
        }
      },
      {
        "command": "myTaskList.showItem",
        "title": "linksaas: show detail task",
        "icon": "$(link)"
      },
      {
        "command": "myTaskList.shortNote",
        "title": "linksaas: show task as short note",
        "icon": "$(link-external)"
      },
      {
        "command": "minappList.sync",
        "title": "linksaas: sync minapp list",
        "icon": {
          "light": "resources/light/refresh.svg",
          "dark": "resources/dark/refresh.svg"
        }
      },
      {
        "command": "minappList.showItem",
        "title": "linksaas: start minapp",
        "icon": "$(debug-start)"
      },
      {
        "command": "codeComment.newThread",
        "title": "linksaas: create new comment thread"
      },
      {
        "command": "codeComment.syncThread",
        "title": "linksaas: sync comments in thread",
        "icon": {
          "dark": "resources/dark/refresh.svg",
          "light": "resources/light/refresh.svg"
        }
      },
      {
        "command": "codeComment.cancelSave",
        "title": "Cancel"
      },
      {
        "command": "codeComment.save",
        "title": "Save"
      },
      {
        "command": "codeComment.add",
        "title": "Add",
        "enablement": "!commentIsEmpty"
      },
      {
        "command": "codeComment.update",
        "title": "Update",
        "icon": {
          "dark": "resources/dark/edit.svg",
          "light": "resources/light/edit.svg"
        }
      },
      {
        "command": "codeComment.remove",
        "title": "Remove",
        "icon": {
          "dark": "resources/dark/close.svg",
          "light": "resources/light/close.svg"
        }
      }
    ],
    "viewsContainers": {
      "activitybar": [
        {
          "id": "linksaas",
          "title": "linksaas",
          "icon": "media/linksaas.svg"
        }
      ]
    },
    "views": {
      "linksaas": [
        {
          "id": "myTaskList",
          "name": "我的任务列表",
          "icon": "media/linksaas.svg",
          "contextualTitle": "linksaas"
        },
        {
          "id": "myBugList",
          "name": "我的缺陷列表",
          "icon": "media/linksaas.svg",
          "contextualTitle": "linksaas"
        },
        {
          "id": "minappList",
          "name": "微应用列表",
          "icon": "media/linksaas.svg",
          "contextualTitle": "linksaas"
        }
      ]
    },
    "menus": {
      "commandPalette": [
        {
          "command": "myBugList.showItem",
          "when": "false"
        },
        {
          "command": "myBugList.shortNote",
          "when": "false"
        },
        {
          "command": "myTaskList.showItem",
          "when": "false"
        },
        {
          "command": "myTaskList.shortNote",
          "when": "false"
        },
        {
          "command": "minappList.showItem",
          "when": "false"
        },
        {
          "command": "codeComment.newThread",
          "when": "false"
        },
        {
          "command": "codeComment.syncThread",
          "when": "false"
        },
        {
          "command": "codeComment.cancelSave",
          "when": "false"
        },
        {
          "command": "codeComment.save",
          "when": "false"
        },
        {
          "command": "codeComment.add",
          "when": "false"
        },
        {
          "command": "codeComment.update",
          "when": "false"
        },
        {
          "command": "codeComment.remove",
          "when": "false"
        }
      ],
      "view/title": [
        {
          "command": "myBugList.sync",
          "when": "view == myBugList",
          "group": "navigation"
        },
        {
          "command": "myTaskList.sync",
          "when": "view == myTaskList",
          "group": "navigation"
        },
        {
          "command": "minappList.sync",
          "when": "view == minappList",
          "group": "navigation"
        }
      ],
      "view/item/context": [
        {
          "command": "myTaskList.showItem",
          "when": "view == myTaskList && viewItem == task",
          "group": "inline"
        },
        {
          "command": "myTaskList.shortNote",
          "when": "view == myTaskList && viewItem == task",
          "group": "inline"
        },
        {
          "command": "myBugList.showItem",
          "when": "view == myBugList && viewItem == bug",
          "group": "inline"
        },
        {
          "command": "myBugList.shortNote",
          "when": "view == myBugList && viewItem == bug",
          "group": "inline"
        },
        {
          "command": "minappList.showItem",
          "when": "view == minappList && viewItem == minapp",
          "group": "inline"
        }
      ],
      "editor/context": [
        {
          "command": "codeComment.newThread",
          "group": "9_cutcopypaste",
          "when": "editorTextFocus"
        }
      ],
      "comments/commentThread/title": [
        {
          "command": "codeComment.syncThread",
          "group": "navigation"
        }
      ],
      "comments/comment/context": [
        {
          "command": "codeComment.cancelSave",
          "group": "inline@2",
          "when": "commentController == linksaas"
        },
        {
          "command": "codeComment.save",
          "group": "inline@1",
          "when": "commentController == linksaas"
        }
      ],
      "comments/commentThread/context": [
        {
          "command": "codeComment.add",
          "group": "inline",
          "when": "commentController == linksaas"
        }
      ],
      "comments/comment/title": [
        {
          "command": "codeComment.update",
          "group": "group@1",
          "when": "commentController == linksaas && comment =~ /update/"
        },
        {
          "command": "codeComment.remove",
          "group": "group@2",
          "when": "commentController == linksaas && comment =~ /remove/"
        }
      ]
    }
  },
  "scripts": {
    "gencode": "npx @openapitools/openapi-generator-cli generate -i proto/index_0.1.19.yml -g typescript-axios -o src/proto-gen",
    "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": {
    "@openapitools/openapi-generator-cli": "^2.7.0",
    "@types/glob": "^8.0.0",
    "@types/mocha": "^10.0.0",
    "@types/node": "16.x",
    "@types/vscode": "^1.75.0",
    "@typescript-eslint/eslint-plugin": "^5.42.0",
    "@typescript-eslint/parser": "^5.42.0",
    "@vscode/test-electron": "^2.2.0",
    "eslint": "^8.26.0",
    "glob": "^8.0.3",
    "mocha": "^10.1.0",
    "typescript": "^4.8.4"
  },
  "dependencies": {
    "axios": "1.1.0",
    "moment": "2.29.4",
    "nanoid": "^3.0.0",
    "yaml": "^2.1.3"
  },
  "icon": "linksaas.png"
}

实现命令和数据提供器

数据提供器通过实现vscode.TreeDataProvider来完成的。根据接口返回树状结构数据即可。 下面是微应用的数据提供器

typescript 复制代码
export class MinappProvider implements vscode.TreeDataProvider<MinappItem> {
    private _onDidChangeTreeData: vscode.EventEmitter<MinappItem | undefined | void> = new vscode.EventEmitter<MinappItem | undefined | void>();
    readonly onDidChangeTreeData: vscode.Event<MinappItem | undefined | void> = this._onDidChangeTreeData.event;

    getTreeItem(element: MinappItem): vscode.TreeItem | Thenable<vscode.TreeItem> {
        return element;
    }

    getChildren(element?: MinappItem | undefined): vscode.ProviderResult<MinappItem[]> {
        if (element) {
            return [];
        }
        const basePath = getBasePath();
        if (basePath == null) {
            return [];
        }
        const api = new MinappApi(new Configuration({
            basePath: basePath,
        }));
        return new Promise<MinappItem[]>((resolve, _) => {
            api.minappGet().then(res => {
                const result = res.data.map(item => new MinappItem(item));
                resolve(result);
            }).catch(e => {
                console.log(e);
                vscode.window.showErrorMessage(`${e}`);
                resolve([]);
            });
        });
    }

    sync() {
        this._onDidChangeTreeData.fire();
    }
}

export class MinappItem extends vscode.TreeItem {
    constructor(minappInfo: MinappInfo) {
        super(minappInfo.minappName ?? "");
        this.contextValue = "minapp";
        this.id = minappInfo.minappId ?? "";
        this.iconPath = new vscode.ThemeIcon("extensions");
    }

    async show(): Promise<void> {
        const basePath = getBasePath();
        if (basePath == null) {
            return;
        }
        const api = new MinappApi(new Configuration({
            basePath: basePath,
        }));

        await api.minappMinappIdGet(this.id ?? "");
    }
}

实现代码评论

代码评论是通过vscode.CommentController来实现的。维护好vscode.CommentThread和vscode.Comment数据结构即可。

typescript 复制代码
export class CodeComment implements vscode.Comment {
    commentId: string;
    contextValue?: string;
    oldBody?: string | vscode.MarkdownString;
    constructor(commentId: string,
        public body: string | vscode.MarkdownString,
        public mode: vscode.CommentMode,
        public author: vscode.CommentAuthorInformation,
        public timestamp: Date,
        public thread: vscode.CommentThread,
    ) {
        this.commentId = commentId;
    }
}

const usageComment: vscode.Comment = {
    body: '所有评论都是以markdown的格式显示。评论内容通过凌鲨客户端获取。',
    mode: vscode.CommentMode.Preview,
    author: {
        name: "说明"
    },
    contextValue: "useage"
};


let commentController: vscode.CommentController | null = null;
let curDocUri = "";
let curDocThreadMap: Map<string, vscode.CommentThread> = new Map();

export function init(controller: vscode.CommentController) {
    commentController = controller;
}

export function onDocChange(document: vscode.TextDocument) {
    if (commentController == null) {
        return;
    }
    if (document.uri.toString() != curDocUri) {
        //清除所有thread
        for (const thread of curDocThreadMap.values()) {
            thread.dispose();
        }
        curDocThreadMap = new Map();
        curDocUri = document.uri.toString();
    }
    //分析页面
    const newThreadRangeMap = parseDocument(document.getText());
    //清除不存在的thread
    let curKeys = Array.from(curDocThreadMap.keys());
    for (const curKey of curKeys) {
        if (!newThreadRangeMap.has(curKey)) {
            const thread = curDocThreadMap.get(curKey);
            if (thread != undefined) {
                thread.dispose();
            }
            curDocThreadMap.delete(curKey);
        }
    }
    //增加新的thread
    for (const newKeyValue of newThreadRangeMap.entries()) {
        if (!curDocThreadMap.has(newKeyValue[0])) {
            createThread(document.uri, newKeyValue[0], newKeyValue[1]);
        }
    }
}

async function createThread(docUri: vscode.Uri, threadUri: string, range: vscode.Range) {
    if (commentController == null) {
        return;
    }
    const thread = commentController.createCommentThread(docUri, range, [usageComment]);
    thread.label = threadUri;
    syncThread(thread);
    curDocThreadMap.set(threadUri, thread);
}

export async function syncThread(thread: vscode.CommentThread) {
    if (thread.label == undefined) {
        return;
    }
    const threadId = thread.label.replace("linksaas://comment/", "");
    const basePath = getBasePath();
    if (basePath == null) {
        return;
    }
    const api = new ProjectCodeCommentApi(new Configuration({
        basePath: basePath,
    }));
    const tokenInfo = getProjectToken();
    if (tokenInfo == null) {
        return;
    }
    const res = await api.projectProjectIdCodeCommentCommentThreadIdGet(tokenInfo.projectId, threadId);
    let commentList: vscode.Comment[] = [];
    res.data.forEach(item => {
        const comment = convertComment(item, thread);
        commentList.push(comment);
    });
    if(commentList.length == 0){
        commentList = [usageComment];
    }
    thread.comments = commentList;
}

function parseDocument(content: string): Map<string, vscode.Range> {
    const retMap = new Map();
    const regex = /(linksaas:\/\/comment\/[a-zA-Z0-9]{21})/;
    content.split("\n").forEach((line: string, lineIndex: number) => {
        const match = line.match(regex);
        if (match != null && match.index != undefined) {
            const range = new vscode.Range(
                new vscode.Position(lineIndex, match.index),
                new vscode.Position(lineIndex, match.index + match[1].length)
            );
            retMap.set(match[1], range);
        }
    })
    return retMap;
}

export async function addComment(reply: vscode.CommentReply) {
    if (reply.thread.label == undefined) {
        return;
    }
    const threadId = reply.thread.label.replace("linksaas://comment/", "");
    const basePath = getBasePath();
    if (basePath == null) {
        return;
    }
    const api = new ProjectCodeCommentApi(new Configuration({
        basePath: basePath,
    }));
    const tokenInfo = getProjectToken();
    if (tokenInfo == null) {
        return;
    }
    const addRes = await api.projectProjectIdCodeCommentCommentThreadIdPut(tokenInfo?.projectId, threadId,
        {
            contentType: "markdown",
            content: reply.text,
        });
    const getRes = await api.projectProjectIdCodeCommentCommentThreadIdCommentIdGet(tokenInfo.projectId, threadId,
        addRes.data.commentId ?? "");
    reply.thread.comments = [...reply.thread.comments, convertComment(getRes.data, reply.thread)];
}

function convertComment(info: CodeCommentInfo, thread: vscode.CommentThread): CodeComment {
    const comment = new CodeComment(info.commentId ?? "", new vscode.MarkdownString(info.content ?? ""),
        vscode.CommentMode.Preview,
        {
            name: info.userDisplayName ?? "",
        }, new Date(info.updateTime ?? 0), thread);
    const canUpdate = info.canUpdate ?? false;
    const canRemove = info.canRemove ?? false;
    if (canUpdate && canRemove) {
        comment.contextValue = "update|remove";
    } else if (canUpdate) {
        comment.contextValue = "update";
    } else if (canRemove) {
        comment.contextValue = "remove";
    }
    return comment;
}

export function setEditComment(comment: CodeComment) {
    const comments = comment.thread.comments.map(cmt => {
        if ((cmt as CodeComment).commentId == comment.commentId) {
            (cmt as CodeComment).oldBody = cmt.body;
            cmt.mode = vscode.CommentMode.Editing;
        }
        return cmt;
    })
    comment.thread.comments = comments;
}

export function cancelSaveComment(comment: CodeComment) {
    const comments = comment.thread.comments.map(cmt => {
        if ((cmt as CodeComment).commentId == comment.commentId) {
            const oldBody = (cmt as CodeComment).oldBody;
            if (oldBody != undefined) {
                cmt.body = oldBody;
            }
            cmt.mode = vscode.CommentMode.Preview;
        }
        return cmt;
    });
    comment.thread.comments = comments;
}

export async function saveComment(comment: CodeComment) {
    if (comment.thread.label == undefined) {
        return;
    }
    const threadId = comment.thread.label.replace("linksaas://comment/", "");
    const basePath = getBasePath();
    if (basePath == null) {
        return;
    }
    const api = new ProjectCodeCommentApi(new Configuration({
        basePath: basePath,
    }));
    const tokenInfo = getProjectToken();
    if (tokenInfo == null) {
        return;
    }
    await api.projectProjectIdCodeCommentCommentThreadIdCommentIdPost(tokenInfo.projectId, threadId, comment.commentId, {
        contentType: "markdown",
        content: typeof (comment.body) == "string" ? comment.body : comment.body.value,
    });
    const comments = comment.thread.comments.map(cmt => {
        if ((cmt as CodeComment).commentId == comment.commentId) {
            (cmt as CodeComment).oldBody = undefined;
            (cmt as CodeComment).timestamp = new Date();
            cmt.mode = vscode.CommentMode.Preview;
        }
        return cmt;
    });
    comment.thread.comments = comments;
}

export async function removeComment(comment: CodeComment) {
    if (comment.thread.label == undefined) {
        return;
    }
    const threadId = comment.thread.label.replace("linksaas://comment/", "");
    const basePath = getBasePath();
    if (basePath == null) {
        return;
    }
    const api = new ProjectCodeCommentApi(new Configuration({
        basePath: basePath,
    }));
    const tokenInfo = getProjectToken();
    if (tokenInfo == null) {
        return;
    }
    await api.projectProjectIdCodeCommentCommentThreadIdCommentIdDelete(tokenInfo.projectId, threadId, comment.commentId);
    const comments = comment.thread.comments.filter(cmt => (cmt as CodeComment).commentId != comment.commentId);
    comment.thread.comments = comments;
}

本地接口调用流程

调用本地接口需要本地接口服务地址和对应的项目ID。

  1. 通过~/.linksaas/local_api文件获取接口服务地址
  2. 通过项目中的.linksaas.yml文件获取项目ID

如果无法获取服务地址或项目ID,则返回空数据。

发布vscode插件

  1. 通过vsce package命令生成vsix文件
  2. 在vscode应用市场上传vsix文件
相关推荐
什么鬼昵称6 分钟前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色24 分钟前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_23442 分钟前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河44 分钟前
CSS总结
前端·css
BigYe程普1 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H1 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍1 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai2 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默2 小时前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_857297912 小时前
招联金融2025校招内推
java·前端·算法·金融·求职招聘