浏览代码

feat: 结合vscode支持快速查看文档、自动补全以及props提示

suzigang 3 年之前
父节点
当前提交
9acf0147c9

+ 3 - 0
.gitignore

@@ -11,6 +11,9 @@ package-lock.json
 /src/packages/nutui.vue.build.ts
 /src/packages/nutui.taro.vue.ts
 /src/packages/nutui.taro.vue.build.ts
+src/packages/vscode-extension/node_modules
+src/packages/vscode-extension/*.vsix
+src/packages/vscode-extension/yarn.lock
 /.nyc_output
 /coverage
 /tsc/test

+ 105 - 0
jd/createAttributes.js

@@ -0,0 +1,105 @@
+const path = require('path');
+const fs = require('fs');
+const MarkdownIt = require('markdown-it')();
+
+const basePath = path.resolve(__dirname, './../src/packages/__VUE');
+const componentDirs = fs.readdirSync(basePath, 'utf8');
+const TYPE_IDENTIFY_OPEN = 'tbody_open';
+const TYPE_IDENTIFY_CLOSE = 'tbody_close';
+const TR_TYPE_IDENTIFY_OPEN = 'tr_open';
+const TR_TYPE_IDENTIFY_CLOSE = 'tr_close';
+
+const getSubSources = (sources) => {
+  let sourcesMap = [];
+  const startIndex = sources.findIndex((source) => source.type === TYPE_IDENTIFY_OPEN);
+  const endIndex = sources.findIndex((source) => source.type === TYPE_IDENTIFY_CLOSE);
+  sources = sources.slice(startIndex, endIndex + 1);
+  while (sources.filter((source) => source.type === TR_TYPE_IDENTIFY_OPEN).length) {
+    let trStartIndex = sources.findIndex((source) => source.type === TR_TYPE_IDENTIFY_OPEN);
+    let trEndIndex = sources.findIndex((source) => source.type === TR_TYPE_IDENTIFY_CLOSE);
+    sourcesMap.push(sources.slice(trStartIndex, trEndIndex + 1));
+    sources.splice(trStartIndex, trEndIndex - trStartIndex + 1);
+  }
+  return sourcesMap;
+};
+
+const genaratorTags = () => {
+  let componentTags = {};
+  if (!componentDirs.length) return;
+
+  for (let componentDir of componentDirs) {
+    let stat = fs.lstatSync(`${basePath}/${componentDir}`);
+    if (stat.isDirectory()) {
+      const absolutePath = path.join(`${basePath}/${componentDir}`, 'doc.md');
+      if (!fs.existsSync(absolutePath)) continue;
+      const data = fs.readFileSync(absolutePath, 'utf8');
+      let sources = MarkdownIt.parse(data, {});
+      let sourcesMap = getSubSources(sources);
+      componentTags[`nut-${componentDir}`] = { attributes: [] };
+      for (let sourceMap of sourcesMap) {
+        let propItem = sourceMap.filter((source) => source.type === 'inline').length
+          ? `${sourceMap.filter((source) => source.type === 'inline')[0].content}`
+          : '';
+        componentTags[`nut-${componentDir}`]['attributes'].push(propItem);
+      }
+    }
+  }
+
+  return componentTags;
+};
+
+const genaratorAttributes = () => {
+  let componentTags = {};
+  if (!componentDirs.length) return;
+  for (let componentDir of componentDirs) {
+    let stat = fs.lstatSync(`${basePath}/${componentDir}`);
+    if (stat.isDirectory()) {
+      const absolutePath = path.join(`${basePath}/${componentDir}`, 'doc.md');
+      if (!fs.existsSync(absolutePath)) continue;
+      const data = fs.readFileSync(absolutePath, 'utf8');
+      let sources = MarkdownIt.parse(data, {});
+      let sourcesMap = getSubSources(sources);
+      for (let sourceMap of sourcesMap) {
+        const inlineItem = sourceMap.filter((source) => source.type === 'inline').length
+          ? sourceMap.filter((source) => source.type === 'inline')
+          : [];
+        const propItem = inlineItem.length ? `${inlineItem[0].content}` : '';
+        const infoItem = inlineItem.length ? `${inlineItem[1].content}` : '';
+        const typeItem = inlineItem.length ? `${inlineItem[2].content.toLowerCase()}` : '';
+        const defaultItem = inlineItem.length ? `${inlineItem[3].content}` : '';
+        componentTags[`nut-${componentDir}/${propItem}`] = {
+          type: `${typeItem}`,
+          description: `属性说明:${infoItem},默认值:${defaultItem}`
+        };
+      }
+    }
+  }
+
+  return componentTags;
+};
+
+const writeTags = () => {
+  const componentTags = genaratorTags();
+  let innerText = `${JSON.stringify(componentTags, null, 2)}`;
+  const distPath = path.resolve(__dirname, './../dist');
+  const componentTagsPath = path.resolve(__dirname, './../dist/smartips/tags.json');
+  if (!fs.existsSync(path.join(distPath + '/smartips'))) {
+    fs.mkdirSync(path.join(distPath + '/smartips'));
+  }
+
+  fs.writeFileSync(componentTagsPath, innerText);
+};
+
+const writeAttributes = () => {
+  const componentAttributes = genaratorAttributes();
+  let innerText = `${JSON.stringify(componentAttributes, null, 2)}`;
+  const distPath = path.resolve(__dirname, './../dist');
+  const componentAttributespPath = path.resolve(__dirname, './../dist/smartips/attributes.json');
+  if (!fs.existsSync(path.join(distPath + '/smartips'))) {
+    fs.mkdirSync(path.join(distPath + '/smartips'));
+  }
+  fs.writeFileSync(componentAttributespPath, innerText);
+};
+
+writeTags();
+writeAttributes();

+ 8 - 3
package.json

@@ -6,6 +6,10 @@
   "module": "dist/nutui.es.js",
   "style": "dist/style.css",
   "typings": "dist/types/index.d.ts",
+  "vetur": {
+    "tags": "dist/smartips/tags.json",
+    "attributes": "dist/smartips/attributes.json"
+  },
   "keywords": [
     "nutui",
     "nutui2",
@@ -40,8 +44,8 @@
     "dev:taro:h5": "npm run createTaroConfig && npm run checked:taro:vue && cd src/sites/mobile-taro/vue/ && npm run dev:h5",
     "build:site": "npm run checked && vite build",
     "build:site:oss": "npm run checked && vite build --base=/nutui/3x/",
-    "build": "npm run checked && vite build --config vite.config.build.ts && vite build --config vite.config.build.disperse.ts && npm run generate:types && npm run generate:themes && vite build --config vite.config.build.css.ts && vite build --config vite.config.build.locale.ts",
-    "build:taro:vue": "npm run checked:taro:vue && vite build --config vite.config.build.taro.vue.ts && vite build --config vite.config.build.taro.vue.disperse.ts && npm run generate:types:taro && npm run generate:themes && vite build --config vite.config.build.css.ts && vite build --config vite.config.build.locale.ts",
+    "build": "npm run checked && vite build --config vite.config.build.ts && vite build --config vite.config.build.disperse.ts && npm run generate:types && npm run generate:themes && vite build --config vite.config.build.css.ts && vite build --config vite.config.build.locale.ts && npm run attrs",
+    "build:taro:vue": "npm run checked:taro:vue && vite build --config vite.config.build.taro.vue.ts && vite build --config vite.config.build.taro.vue.disperse.ts && npm run generate:types:taro && npm run generate:themes && vite build --config vite.config.build.css.ts && vite build --config vite.config.build.locale.ts && npm run attrs",
     "serve": "vite preview",
     "upload": "yarn build:site:oss && node ./jd/upload.js",
     "add": "node jd/createComponentMode.js",
@@ -57,7 +61,8 @@
     "release": "standard-version -a",
     "codeformat": "prettier --write .",
     "copydocs": "node ./jd/copymd.js",
-    "createTaroConfig": "node ./jd/generate-taro-route.js"
+    "createTaroConfig": "node ./jd/generate-taro-route.js",
+    "attrs": "node ./jd/createAttributes.js"
   },
   "standard-version": {
     "scripts": {

+ 24 - 0
src/packages/vscode-extension/.eslintrc.json

@@ -0,0 +1,24 @@
+{
+    "root": true,
+    "parser": "@typescript-eslint/parser",
+    "parserOptions": {
+        "ecmaVersion": 6,
+        "sourceType": "module"
+    },
+    "plugins": [
+        "@typescript-eslint"
+    ],
+    "rules": {
+        "@typescript-eslint/naming-convention": "warn",
+        "@typescript-eslint/semi": "warn",
+        "curly": "warn",
+        "eqeqeq": "warn",
+        "no-throw-literal": "warn",
+        "semi": "off"
+    },
+    "ignorePatterns": [
+        "out",
+        "dist",
+        "**/*.d.ts"
+    ]
+}

+ 6 - 0
src/packages/vscode-extension/.gitignore

@@ -0,0 +1,6 @@
+out
+dist
+node_modules
+.vscode-test/
+*.vsix
+yarn.lock

+ 11 - 0
src/packages/vscode-extension/.vscodeignore

@@ -0,0 +1,11 @@
+.vscode/**
+.vscode-test/**
+out/**
+node_modules/**
+.gitignore
+.yarnrc
+vsc-extension-quickstart.md
+**/tsconfig.json
+**/.eslintrc.json
+**/*.map
+**/*.ts

+ 1 - 0
src/packages/vscode-extension/.yarnrc

@@ -0,0 +1 @@
+--ignore-engines true

+ 6 - 0
src/packages/vscode-extension/CHANGELOG.md

@@ -0,0 +1,6 @@
+# Change Log
+
+## v0.0.1
+
+* 支持快速查看组件文档
+* 支持自动补全功能

+ 21 - 0
src/packages/vscode-extension/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 京东前端
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 1 - 0
src/packages/vscode-extension/README.md

@@ -0,0 +1 @@
+# NutUI组件库vscode插件

+ 55 - 0
src/packages/vscode-extension/package.json

@@ -0,0 +1,55 @@
+{
+	"name": "nutui-vscode-extension",
+	"private": true,
+	"displayName": "nutui-vscode-extension",
+	"description": "nutui extension for vscode",
+	"version": "0.0.1",
+	"icon": "src/nutui.png",
+	"engines": {
+		"vscode": "^1.66.0"
+	},
+	"repository": {
+		"type": "git",
+		"url": "git+https://github.com/jdf2e/nutui.git"
+	},
+	"categories": [
+		"Other"
+	],
+	"publisher": "nutui",
+	"activationEvents": [
+		"onLanguage:vue",
+		"onLanguage:javascript",
+		"onLanguage:typescript"
+	],
+	"main": "./dist/extension.js",
+	"contributes": {
+		"commands": []
+	},
+	"scripts": {
+		"build": "webpack --mode production --devtool hidden-source-map",
+		"package": "yarn gen && yarn build && vsce package",
+		"publish": "vsce publish",
+		"unpublish": "vsce unpublish nutui.nutui-vscode-extension",
+		"gen": "node ./scripts/createComponentMap.js"
+	},
+	"devDependencies": {
+		"@types/glob": "^7.2.0",
+		"@types/mocha": "^9.0.0",
+		"@types/node": "14.x",
+		"@types/vscode": "^1.65.0",
+		"@typescript-eslint/eslint-plugin": "^5.9.1",
+		"@typescript-eslint/parser": "^5.9.1",
+		"@vscode/test-electron": "^2.0.3",
+		"eslint": "^8.6.0",
+		"glob": "^7.2.0",
+		"markdown-it": "^12.3.2",
+		"mocha": "^9.1.3",
+		"ts-loader": "^9.2.6",
+		"typescript": "^4.5.4",
+		"webpack": "^5.66.0",
+		"webpack-cli": "^4.9.1"
+	},
+	"dependencies": {
+		"vsce": "^2.7.0"
+	}
+}

+ 58 - 0
src/packages/vscode-extension/scripts/createComponentMap.js

@@ -0,0 +1,58 @@
+const path = require('path');
+const fs = require('fs');
+const MarkdownIt = require('markdown-it')();
+
+const basePath = path.resolve(__dirname, './../../__VUE');
+const componentDirs = fs.readdirSync(basePath, 'utf8');
+const TYPE_IDENTIFY_OPEN = 'tbody_open';
+const TYPE_IDENTIFY_CLOSE = 'tbody_close';
+const TR_TYPE_IDENTIFY_OPEN = 'tr_open';
+const TR_TYPE_IDENTIFY_CLOSE = 'tr_close';
+
+const getSubSources = (sources) => {
+  const startIndex = sources.findIndex((source) => source.type === TYPE_IDENTIFY_OPEN);
+  const endIndex = sources.findIndex((source) => source.type === TYPE_IDENTIFY_CLOSE);
+  sources = sources.slice(startIndex, endIndex + 1);
+  const trStartIndex = sources.findIndex((source) => source.type === TR_TYPE_IDENTIFY_OPEN);
+  const trEndIndex = sources.findIndex((source) => source.type === TR_TYPE_IDENTIFY_CLOSE);
+  return sources.slice(trStartIndex, trEndIndex + 1);
+};
+
+const genaratorComponentMap = () => {
+  let componentMap = {};
+  if (!componentDirs.length) return;
+
+  for (let componentDir of componentDirs) {
+    let stat = fs.lstatSync(`${basePath}/${componentDir}`);
+    if (stat.isDirectory()) {
+      const absolutePath = path.join(`${basePath}/${componentDir}`, 'doc.md');
+      if (!fs.existsSync(absolutePath)) continue;
+      const data = fs.readFileSync(absolutePath, 'utf8');
+      let sources = MarkdownIt.parse(data, {});
+      sources = getSubSources(sources);
+      componentMap[componentDir] = {
+        site: `/${componentDir}`,
+        props: sources.filter((source) => source.type === 'inline').length
+          ? [`${sources.filter((source) => source.type === 'inline')[0].content}=''`]
+          : ['']
+      };
+    }
+  }
+
+  return componentMap;
+};
+
+const writeFileInComponentMap = () => {
+  const componentMap = genaratorComponentMap();
+  let innerText = `
+import { ComponentDesc } from './componentDesc';
+
+export const componentMap: Record<string, ComponentDesc> = ${JSON.stringify(componentMap, null, 2)}
+`;
+
+  const componentMapPath = path.resolve(__dirname, './../src/componentMap.ts');
+
+  fs.writeFileSync(componentMapPath, innerText);
+};
+
+writeFileInComponentMap();

+ 4 - 0
src/packages/vscode-extension/src/componentDesc.ts

@@ -0,0 +1,4 @@
+export interface ComponentDesc {
+  site: string;
+  props?: string[];
+}

+ 324 - 0
src/packages/vscode-extension/src/componentMap.ts

@@ -0,0 +1,324 @@
+import { ComponentDesc } from './componentDesc';
+
+export const componentMap: Record<string, ComponentDesc> = {
+  actionsheet: {
+    site: '/actionsheet',
+    props: ["v-model:visible=''"]
+  },
+  address: {
+    site: '/address',
+    props: ["v-model:visible=''"]
+  },
+  addresslist: {
+    site: '/addresslist',
+    props: ["data=''"]
+  },
+  audio: {
+    site: '/audio',
+    props: ["url=''"]
+  },
+  audiooperate: {
+    site: '/audiooperate',
+    props: ['']
+  },
+  avatar: {
+    site: '/avatar',
+    props: ["size=''"]
+  },
+  backtop: {
+    site: '/backtop',
+    props: ["el-id=''"]
+  },
+  badge: {
+    site: '/badge',
+    props: ["value=''"]
+  },
+  barrage: {
+    site: '/barrage',
+    props: ["danmu=''"]
+  },
+  button: {
+    site: '/button',
+    props: ["type=''"]
+  },
+  calendar: {
+    site: '/calendar',
+    props: ["v-model:visible=''"]
+  },
+  card: {
+    site: '/card',
+    props: ["img-url=''"]
+  },
+  cascader: {
+    site: '/cascader',
+    props: ["v-model=''"]
+  },
+  category: {
+    site: '/category',
+    props: ["type=''"]
+  },
+  cell: {
+    site: '/cell',
+    props: ["title=''"]
+  },
+  checkbox: {
+    site: '/checkbox',
+    props: ["v-model=''"]
+  },
+  circleprogress: {
+    site: '/circleprogress',
+    props: ["progress=''"]
+  },
+  collapse: {
+    site: '/collapse',
+    props: ["v-model=''"]
+  },
+  comment: {
+    site: '/comment',
+    props: ["headerType=''"]
+  },
+  countdown: {
+    site: '/countdown',
+    props: ["v-model=''"]
+  },
+  countup: {
+    site: '/countup',
+    props: ["init-num=''"]
+  },
+  datepicker: {
+    site: '/datepicker',
+    props: ["v-model=''"]
+  },
+  dialog: {
+    site: '/dialog',
+    props: ["title=''"]
+  },
+  divider: {
+    site: '/divider',
+    props: ["dashed=''"]
+  },
+  drag: {
+    site: '/drag',
+    props: ["attract=''"]
+  },
+  ecard: {
+    site: '/ecard',
+    props: ["modelValue=''"]
+  },
+  elevator: {
+    site: '/elevator',
+    props: ["height=''"]
+  },
+  empty: {
+    site: '/empty',
+    props: ["image=''"]
+  },
+  fixednav: {
+    site: '/fixednav',
+    props: ["visible=''"]
+  },
+  form: {
+    site: '/form',
+    props: ["model-value=''"]
+  },
+  grid: {
+    site: '/grid',
+    props: ["column-num=''"]
+  },
+  icon: {
+    site: '/icon',
+    props: ["name=''"]
+  },
+  imagepreview: {
+    site: '/imagepreview',
+    props: ["show=''"]
+  },
+  indicator: {
+    site: '/indicator',
+    props: ["current=''"]
+  },
+  infiniteloading: {
+    site: '/infiniteloading',
+    props: ["has-more=''"]
+  },
+  input: {
+    site: '/input',
+    props: ["v-model=''"]
+  },
+  inputnumber: {
+    site: '/inputnumber',
+    props: ["v-model=''"]
+  },
+  layout: {
+    site: '/layout',
+    props: ["type=''"]
+  },
+  list: {
+    site: '/list',
+    props: ["height=''"]
+  },
+  menu: {
+    site: '/menu',
+    props: ["active-color=''"]
+  },
+  navbar: {
+    site: '/navbar',
+    props: ["title=''"]
+  },
+  noticebar: {
+    site: '/noticebar',
+    props: ["direction=''"]
+  },
+  notify: {
+    site: '/notify',
+    props: ["type=''"]
+  },
+  numberkeyboard: {
+    site: '/numberkeyboard',
+    props: ["v-model:visible=''"]
+  },
+  oldpicker: {
+    site: '/oldpicker',
+    props: ["v-model:visible=''"]
+  },
+  overlay: {
+    site: '/overlay',
+    props: ["v-model:visible=''"]
+  },
+  pagination: {
+    site: '/pagination',
+    props: ["v-model=''"]
+  },
+  picker: {
+    site: '/picker',
+    props: ["v-model:value=''"]
+  },
+  popover: {
+    site: '/popover',
+    props: ["list=''"]
+  },
+  popup: {
+    site: '/popup',
+    props: ["v-model:visible=''"]
+  },
+  price: {
+    site: '/price',
+    props: ["price=''"]
+  },
+  progress: {
+    site: '/progress',
+    props: ["percentage=''"]
+  },
+  pullrefresh: {
+    site: '/pullrefresh',
+    props: ["useWindow=''"]
+  },
+  radio: {
+    site: '/radio',
+    props: ["disabled=''"]
+  },
+  range: {
+    site: '/range',
+    props: ["v-model=''"]
+  },
+  rate: {
+    site: '/rate',
+    props: ["v-model=''"]
+  },
+  searchbar: {
+    site: '/searchbar',
+    props: ["max-length=''"]
+  },
+  shortpassword: {
+    site: '/shortpassword',
+    props: ["v-model=''"]
+  },
+  sidenavbar: {
+    site: '/sidenavbar',
+    props: ["offset=''"]
+  },
+  signature: {
+    site: '/signature',
+    props: ["custom-class=''"]
+  },
+  skeleton: {
+    site: '/skeleton',
+    props: ["loading=''"]
+  },
+  sku: {
+    site: '/sku',
+    props: ["v-model:visible=''"]
+  },
+  steps: {
+    site: '/steps',
+    props: ["direction=''"]
+  },
+  sticky: {
+    site: '/sticky',
+    props: ["position=''"]
+  },
+  swipe: {
+    site: '/swipe',
+    props: ["name=''"]
+  },
+  swiper: {
+    site: '/swiper',
+    props: ["width=''"]
+  },
+  swiperitem: {
+    site: '/swiperitem',
+    props: ['']
+  },
+  switch: {
+    site: '/switch',
+    props: ["v-model=''"]
+  },
+  tabbar: {
+    site: '/tabbar',
+    props: ["v-model:visible=''"]
+  },
+  table: {
+    site: '/table',
+    props: ["bordered=''"]
+  },
+  tabs: {
+    site: '/tabs',
+    props: ["v-model=''"]
+  },
+  tag: {
+    site: '/tag',
+    props: ["type=''"]
+  },
+  temp: {
+    site: '/temp',
+    props: ["name=''"]
+  },
+  textarea: {
+    site: '/textarea',
+    props: ["v-model=''"]
+  },
+  timedetail: {
+    site: '/timedetail',
+    props: ["height=''"]
+  },
+  timepannel: {
+    site: '/timepannel',
+    props: ["height=''"]
+  },
+  timeselect: {
+    site: '/timeselect',
+    props: ["visible=''"]
+  },
+  toast: {
+    site: '/toast',
+    props: ["Toast.text=''"]
+  },
+  uploader: {
+    site: '/uploader',
+    props: ["auto-upload=''"]
+  },
+  video: {
+    site: '/video',
+    props: ["source=''"]
+  }
+};

+ 80 - 0
src/packages/vscode-extension/src/extension.ts

@@ -0,0 +1,80 @@
+import * as vscode from 'vscode';
+import { kebabCase, bigCamelize } from './utils';
+import { componentMap } from './componentMap';
+import { ComponentDesc } from './componentDesc';
+
+const DOC = 'https://nutui.jd.com/#';
+
+const LINK_REG = /(?<=<nut-)([\w-]+)/g;
+const BIG_LINK_REG = /(?<=<Nut-)([\w-])+/g;
+const files = ['vue', 'typescript', 'javascript', 'react'];
+
+const provideHover = (document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) => {
+  const line = document.lineAt(position);
+  const componentLink = line.text.match(LINK_REG) ?? [];
+  const componentBigLink = line.text.match(BIG_LINK_REG) ?? [];
+  const components = [...new Set([...componentLink, ...componentBigLink.map(kebabCase)])];
+
+  if (components.length) {
+    const text = components
+      .filter((item: string) => componentMap[item])
+      .map((item: string) => {
+        const { site } = componentMap[item];
+
+        return new vscode.MarkdownString(
+          `[NutUI -> $(references) 请查看 ${bigCamelize(item)} 组件官方文档](${DOC}${site})\n`,
+          true
+        );
+      });
+
+    return new vscode.Hover(text);
+  }
+};
+
+const provideCompletionItems = () => {
+  const completionItems: vscode.CompletionItem[] = [];
+  Object.keys(componentMap).forEach((key: string) => {
+    completionItems.push(
+      new vscode.CompletionItem(`nut-${key}`, vscode.CompletionItemKind.Field),
+      new vscode.CompletionItem(bigCamelize(`nut-${key}`), vscode.CompletionItemKind.Field)
+    );
+  });
+  return completionItems;
+};
+
+const resolveCompletionItem = (item: vscode.CompletionItem) => {
+  const name = kebabCase(<string>item.label).slice(4);
+  const descriptor: ComponentDesc = componentMap[name];
+
+  const propsText = descriptor.props ? descriptor.props : '';
+  const tagSuffix = `</${item.label}>`;
+  item.insertText = `<${item.label} ${propsText}>${tagSuffix}`;
+
+  item.command = {
+    title: 'nutui-move-cursor',
+    command: 'nutui-move-cursor',
+    arguments: [-tagSuffix.length - 2]
+  };
+  return item;
+};
+
+const moveCursor = (characterDelta: number) => {
+  const active = vscode.window.activeTextEditor!.selection.active!;
+  const position = active.translate({ characterDelta });
+  vscode.window.activeTextEditor!.selection = new vscode.Selection(position, position);
+};
+
+export function activate(context: vscode.ExtensionContext) {
+  vscode.commands.registerCommand('nutui-move-cursor', moveCursor);
+  context.subscriptions.push(
+    vscode.languages.registerHoverProvider(files, {
+      provideHover
+    }),
+    vscode.languages.registerCompletionItemProvider(files, {
+      provideCompletionItems,
+      resolveCompletionItem
+    })
+  );
+}
+
+export function deactivate() {}

二进制
src/packages/vscode-extension/src/nutui.png


+ 12 - 0
src/packages/vscode-extension/src/utils.ts

@@ -0,0 +1,12 @@
+export const kebabCase = (str: string): string => {
+  str = str.replace(str.charAt(0), str.charAt(0).toLocaleLowerCase());
+  return str.replace(/([a-z])([A-Z])/g, (_, p1, p2) => p1 + '-' + p2.toLowerCase());
+};
+
+export const camelize = (str: string): string => {
+  return str.replace(/-(\w)/g, (_: any, p: string) => p.toUpperCase());
+};
+
+export const bigCamelize = (str: string): string => {
+  return camelize(str).replace(str.charAt(0), str.charAt(0).toUpperCase());
+};

+ 16 - 0
src/packages/vscode-extension/tsconfig.json

@@ -0,0 +1,16 @@
+{
+	"compilerOptions": {
+		"module": "commonjs",
+		"target": "ES2020",
+		"lib": [
+			"ES2020"
+		],
+		"sourceMap": true,
+		"rootDir": "src",
+		"strict": true   /* enable all strict type-checking options */
+		/* Additional Checks */
+		// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+		// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+		// "noUnusedParameters": true,  /* Report errors on unused parameters. */
+	}
+}

+ 48 - 0
src/packages/vscode-extension/webpack.config.js

@@ -0,0 +1,48 @@
+//@ts-check
+
+'use strict';
+
+const path = require('path');
+
+//@ts-check
+/** @typedef {import('webpack').Configuration} WebpackConfig **/
+
+/** @type WebpackConfig */
+const extensionConfig = {
+  target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
+  mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
+
+  entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
+  output: {
+    // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
+    path: path.resolve(__dirname, 'dist'),
+    filename: 'extension.js',
+    libraryTarget: 'commonjs2'
+  },
+  externals: {
+    vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
+    // modules added here also need to be added in the .vscodeignore file
+  },
+  resolve: {
+    // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
+    extensions: ['.ts', '.js']
+  },
+  module: {
+    rules: [
+      {
+        test: /\.ts$/,
+        exclude: /node_modules/,
+        use: [
+          {
+            loader: 'ts-loader'
+          }
+        ]
+      }
+    ]
+  },
+  devtool: 'nosources-source-map',
+  infrastructureLogging: {
+    level: 'log' // enables logging required for problem matchers
+  }
+};
+module.exports = [extensionConfig];