├── README.md ├── .github └── FUNDING.yml ├── pnpm-workspace.yaml ├── .gitignore ├── packages ├── vscode │ ├── .vscodeignore │ ├── images │ │ ├── preview-bg.png │ │ ├── nuxt-logo.svg │ │ └── vite-logo.svg │ ├── tsconfig.build.json │ ├── src │ │ ├── utils │ │ │ ├── fs.ts │ │ │ ├── http.ts │ │ │ └── quickPick.ts │ │ └── extension.ts │ ├── LICENSE │ ├── scripts │ │ └── build.js │ └── package.json └── core │ ├── tsconfig.build.json │ ├── package.json │ ├── LICENSE │ ├── bin │ ├── nuxi.js │ ├── nuxi │ │ ├── configExtraContent.ts │ │ └── plugin.ts │ ├── vite.js │ └── vite │ │ └── config.ts │ └── src │ └── index.ts ├── tsconfig.build.json ├── lerna.json ├── .vscode └── launch.json ├── tsconfig.json ├── package.json └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # Vue Preview 2 | 3 | TODO 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: johnsoncodehk 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | *.tsbuildinfo 5 | *.vsix 6 | -------------------------------------------------------------------------------- /packages/vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | out 2 | src 3 | scripts 4 | tsconfig.build.json 5 | tsconfig.build.tsbuildinfo 6 | -------------------------------------------------------------------------------- /packages/vscode/images/preview-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnsoncodehk/vue-preview/HEAD/packages/vscode/images/preview-bg.png -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "include": [], 4 | "references": [ 5 | // Extensions 6 | { 7 | "path": "./packages/vscode/tsconfig.build.json" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/lerna-lite/lerna-lite/main/packages/cli/schemas/lerna-schema.json", 3 | "npmClient": "pnpm", 4 | "packages": [ 5 | "packages/*" 6 | ], 7 | "version": "0.1.0" 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "outDir": "out", 6 | "rootDir": "src", 7 | }, 8 | "include": [ 9 | "src" 10 | ], 11 | "exclude": [ 12 | "node_modules", 13 | ".vscode-test" 14 | ], 15 | } -------------------------------------------------------------------------------- /packages/vscode/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "outDir": "out", 6 | "rootDir": "src", 7 | }, 8 | "include": [ 9 | "src", 10 | ], 11 | "references": [ 12 | { 13 | "path": "../core/tsconfig.build.json" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /packages/vscode/src/utils/fs.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export async function readFile(uri: vscode.Uri) { 4 | const data = await vscode.workspace.fs.readFile(uri); 5 | return new TextDecoder('utf8').decode(data); 6 | } 7 | 8 | export async function exists(uri: vscode.Uri) { 9 | try { 10 | await vscode.workspace.fs.stat(uri); 11 | return true; 12 | } 13 | catch { 14 | return false; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/vscode/images/nuxt-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-preview/core", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "files": [ 6 | "bin", 7 | "out/**/*.js", 8 | "out/**/*.d.ts" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/johnsoncodehk/vue-preview.git", 13 | "directory": "packages/core" 14 | }, 15 | "main": "out/index.js", 16 | "devDependencies": { 17 | "@types/ws": "^8.5.4" 18 | }, 19 | "dependencies": { 20 | "ws": "^8.12.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/vscode/src/utils/http.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | 3 | export function isLocalHostPortUsing(port: number) { 4 | return new Promise(resolve => { 5 | http.get(`http://localhost:${port}/`, { 6 | headers: { 7 | accept: "*/*", // if not set, always get 404 from vite server 8 | }, 9 | }, res => { 10 | resolve(res.statusCode === 200); 11 | }).on('error', () => resolve(false)).end(); 12 | }); 13 | } 14 | 15 | export async function getLocalHostAvailablePort(port: number) { 16 | if (await isLocalHostPortUsing(port)) { 17 | port++; 18 | } 19 | return port; 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--disable-extensions", 12 | "--extensionDevelopmentPath=${workspaceRoot}/packages/vscode" 13 | ], 14 | "outFiles": [ 15 | "${workspaceRoot}/*/*/out/**/*.js" 16 | ], 17 | "preLaunchTask": { 18 | "type": "npm", 19 | "script": "watch" 20 | } 21 | }, 22 | ], 23 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2016", 4 | "lib": [ 5 | "WebWorker", 6 | "ES2021", 7 | ], 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "sourceMap": true, 11 | "composite": true, 12 | "declaration": true, 13 | "strict": true, 14 | "alwaysStrict": false, 15 | "noImplicitUseStrict": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "baseUrl": "./", 21 | "paths": { 22 | "@vue-preview/core": [ 23 | "packages/core/src" 24 | ], 25 | }, 26 | "noEmit": true, 27 | }, 28 | "include": [ 29 | "*/*/src", 30 | ], 31 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "tsc -b tsconfig.build.json", 5 | "watch": "npm run build && (npm run watch:base & npm run watch:vscode)", 6 | "watch:base": "tsc -b tsconfig.build.json -w", 7 | "watch:vscode": "cd ./packages/vscode && npm run watch", 8 | "prerelease": "npm run build", 9 | "release": "npm run release:base && npm run release:vscode", 10 | "release:base": "lerna publish --exact --force-publish --yes --sync-workspace-lock", 11 | "release:vscode": "cd ./packages/vscode && npm run release" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "latest", 15 | "typescript": "latest" 16 | }, 17 | "optionalDependencies": { 18 | "@lerna-lite/cli": "latest" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Johnson Chu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Johnson Chu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/vscode/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Johnson Chu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/vscode/scripts/build.js: -------------------------------------------------------------------------------- 1 | require('esbuild').build({ 2 | entryPoints: { 3 | extension: './out/extension.js', 4 | }, 5 | bundle: true, 6 | metafile: process.argv.includes('--metafile'), 7 | outdir: './dist', 8 | external: [ 9 | 'vscode', 10 | ], 11 | format: 'cjs', 12 | platform: 'node', 13 | tsconfig: '../../tsconfig.build.json', 14 | define: { 'process.env.NODE_ENV': '"production"' }, 15 | minify: process.argv.includes('--minify'), 16 | watch: process.argv.includes('--watch'), 17 | plugins: [ 18 | { 19 | name: 'umd2esm', 20 | setup(build) { 21 | build.onResolve({ filter: /^(vscode-.*|estree-walker|jsonc-parser)/ }, args => { 22 | const pathUmdMay = require.resolve(args.path, { paths: [args.resolveDir] }) 23 | // Call twice the replace is to solve the problem of the path in Windows 24 | const pathEsm = pathUmdMay.replace('/umd/', '/esm/').replace('\\umd\\', '\\esm\\') 25 | return { path: pathEsm } 26 | }) 27 | }, 28 | }, 29 | require('esbuild-plugin-copy').copy({ 30 | resolveFrom: 'cwd', 31 | assets: { 32 | from: ['./node_modules/@volar/preview/bin/**/*'], 33 | to: ['./dist/bin'], 34 | }, 35 | keepStructure: true, 36 | }), 37 | ], 38 | }).catch(() => process.exit(1)) 39 | -------------------------------------------------------------------------------- /packages/core/bin/nuxi.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const readFileSync = fs.readFileSync; 6 | 7 | const workspace = process.cwd(); 8 | const nuxiBinPath = require.resolve('nuxi/cli', { paths: [workspace] }); 9 | const jsConfigPath = path.resolve(workspace, 'nuxt.config.js'); 10 | const tsConfigPath = path.resolve(workspace, 'nuxt.config.ts'); 11 | 12 | fs.readFileSync = (...args) => { 13 | if (args[0] === jsConfigPath || args[0] === tsConfigPath) { 14 | const configExtraContent = readFileSync(path.resolve(__dirname, 'nuxi', 'configExtraContent.ts'), { encoding: 'utf8' }); 15 | return readFileSync(...args) + configExtraContent; 16 | } 17 | return readFileSync(...args); 18 | }; 19 | 20 | createNuxtPlugin(); 21 | 22 | import('file://' + nuxiBinPath); 23 | 24 | function createNuxtPlugin() { 25 | 26 | if (!fs.existsSync(path.resolve(workspace, 'node_modules', '.vue-preview'))) { 27 | fs.mkdirSync(path.resolve(workspace, 'node_modules', '.vue-preview')); 28 | } 29 | 30 | const proxyConfigPath = path.resolve(workspace, 'node_modules', '.vue-preview', 'nuxt.plugin.ts'); 31 | const pluginContent = fs.readFileSync(path.resolve(__dirname, 'nuxi', 'plugin.ts'), { encoding: 'utf8' }); 32 | 33 | fs.writeFileSync(proxyConfigPath, pluginContent); 34 | } 35 | -------------------------------------------------------------------------------- /packages/vscode/src/utils/quickPick.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function quickPick(groups: T | T[], placeholder?: string) { 4 | return new Promise(resolve => { 5 | const quickPick = vscode.window.createQuickPick(); 6 | const items: vscode.QuickPickItem[] = []; 7 | for (const group of Array.isArray(groups) ? groups : [groups]) { 8 | const groupItems = Object.values(group); 9 | if (groupItems.length) { 10 | if (items.length) { 11 | items.push({ label: '', kind: vscode.QuickPickItemKind.Separator }); 12 | } 13 | for (const item of groupItems) { 14 | if (item) { 15 | items.push(item); 16 | } 17 | } 18 | } 19 | } 20 | quickPick.items = items; 21 | quickPick.placeholder = placeholder; 22 | quickPick.onDidChangeSelection(selection => { 23 | if (selection[0]) { 24 | for (const options of Array.isArray(groups) ? groups : [groups]) { 25 | for (let key in options) { 26 | const option = options[key]; 27 | if (selection[0] === option) { 28 | resolve(key); 29 | quickPick.hide(); 30 | break; 31 | } 32 | } 33 | } 34 | } 35 | }); 36 | quickPick.onDidHide(() => { 37 | quickPick.dispose(); 38 | resolve(undefined); 39 | }); 40 | quickPick.show(); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /packages/vscode/images/vite-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/core/bin/nuxi/configExtraContent.ts: -------------------------------------------------------------------------------- 1 | 2 | if (!module.exports.default) 3 | module.exports.default = {}; 4 | 5 | if (!module.exports.default.vue) 6 | module.exports.default.vue = {}; 7 | 8 | if (!module.exports.default.vue.compilerOptions) 9 | module.exports.default.vue.compilerOptions = {}; 10 | 11 | if (!module.exports.default.vue.compilerOptions.nodeTransforms) 12 | module.exports.default.vue.compilerOptions.nodeTransforms = []; 13 | 14 | module.exports.default.vue.compilerOptions.nodeTransforms.push( 15 | (node, ctx) => { 16 | if (node.type === 1) { 17 | const start = node.loc.start.offset; 18 | const end = node.loc.end.offset; 19 | addEvent(node, 'pointerenter', `$event ? $volar.highlight($event.target, $.type.__file, [${start},${end}]) : undefined`); 20 | addEvent(node, 'pointerleave', '$event ? $volar.unHighlight($event.target) : undefined'); 21 | addEvent(node, 'vnode-mounted', `$event ? $volar.vnodeMounted($event.el, $.type.__file, [${start},${end}]) : undefined`); 22 | addEvent(node, 'vnode-unmounted', '$event ? $volar.vnodeUnmounted($event.el) : undefined'); 23 | } 24 | } 25 | ); 26 | 27 | if (!module.exports.default.plugins) 28 | module.exports.default.plugins = []; 29 | 30 | module.exports.default.plugins.push({ src: './node_modules/.vue-preview/nuxt.plugin.ts', ssr: false }); 31 | 32 | function addEvent(node, name: string, exp: string) { 33 | node.props.push({ 34 | type: 7, 35 | name: 'on', 36 | exp: { 37 | type: 4, 38 | content: exp, 39 | isStatic: false, 40 | constType: 0, 41 | loc: node.loc, 42 | }, 43 | arg: { 44 | type: 4, 45 | content: name, 46 | isStatic: true, 47 | constType: 3, 48 | loc: node.loc, 49 | }, 50 | modifiers: [], 51 | loc: node.loc, 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as WebSocket from 'ws'; 2 | 3 | export function createPreviewConnection(options: { 4 | onGotoCode: (fileName: string, range: [number, number], cancelToken: { readonly isCancelled: boolean; }) => void, 5 | getFileHref: (fileName: string, range: [number, number]) => string, 6 | }) { 7 | 8 | const wsList: WebSocket.WebSocket[] = []; 9 | let wss: WebSocket.Server | undefined; 10 | let goToTemplateReq = 0; 11 | 12 | wss = new WebSocket.Server({ port: 56789 }); 13 | wss.on('connection', ws => { 14 | 15 | wsList.push(ws); 16 | 17 | ws.on('message', msg => { 18 | 19 | const message = JSON.parse(msg.toString()); 20 | 21 | if (message.command === 'goToTemplate') { 22 | 23 | const req = ++goToTemplateReq; 24 | const data = message.data as { 25 | fileName: string, 26 | range: [number, number], 27 | }; 28 | const token = { 29 | get isCancelled() { 30 | return req !== goToTemplateReq; 31 | } 32 | }; 33 | 34 | options.onGotoCode(data.fileName, data.range, token); 35 | } 36 | 37 | if (message.command === 'requestOpenFile') { 38 | 39 | const data = message.data as { 40 | fileName: string, 41 | range: [number, number], 42 | }; 43 | const url = options.getFileHref(data.fileName, data.range); 44 | 45 | ws.send(JSON.stringify({ 46 | command: 'openFile', 47 | data: url, 48 | })); 49 | } 50 | }); 51 | }); 52 | 53 | return { 54 | stop, 55 | highlight, 56 | unhighlight, 57 | }; 58 | 59 | function stop() { 60 | wss?.close(); 61 | wsList.length = 0; 62 | } 63 | 64 | function highlight(fileName: string, ranges: { start: number, end: number; }[], isDirty: boolean) { 65 | const msg = { 66 | command: 'highlightSelections', 67 | data: { 68 | fileName, 69 | ranges, 70 | isDirty, 71 | }, 72 | }; 73 | for (const ws of wsList) { 74 | ws.send(JSON.stringify(msg)); 75 | } 76 | } 77 | 78 | function unhighlight() { 79 | const msg = { 80 | command: 'highlightSelections', 81 | data: undefined, 82 | }; 83 | for (const ws of wsList) { 84 | ws.send(JSON.stringify(msg)); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/core/bin/vite.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | const workspace = process.cwd(); 7 | const vitePkgPath = require.resolve('vite/package.json', { paths: [workspace] }); 8 | const viteDir = path.dirname(vitePkgPath); 9 | const viteBinPath = require.resolve('./bin/vite.js', { paths: [viteDir] }); 10 | const viteVersion = require(vitePkgPath).version; 11 | 12 | try { 13 | const vuePluginPath = require.resolve('@vitejs/plugin-vue', { paths: [workspace] }); 14 | const viteExtraCode = ` 15 | function __proxyExport(rawOptions = {}) { 16 | 17 | if (!rawOptions) 18 | rawOptions = {}; 19 | 20 | if (!rawOptions.template) 21 | rawOptions.template = {}; 22 | 23 | if (!rawOptions.template.compilerOptions) 24 | rawOptions.template.compilerOptions = {}; 25 | 26 | if (!rawOptions.template.compilerOptions.nodeTransforms) 27 | rawOptions.template.compilerOptions.nodeTransforms = []; 28 | 29 | rawOptions.template.compilerOptions.nodeTransforms.push((node, ctx) => { 30 | if (node.type === 1) { 31 | const start = node.loc.start.offset; 32 | const end = node.loc.end.offset; 33 | addEvent(node, 'pointerenter', \`$event ? $volar.highlight($event.target, $.type.__file, [\${start},\${end}]) : undefined\`); 34 | addEvent(node, 'pointerleave', '$event ? $volar.unHighlight($event.target) : undefined'); 35 | addEvent(node, 'vnode-mounted', \`$event ? $volar.vnodeMounted($event.el, $.type.__file, [\${start},\${end}]) : undefined\`); 36 | addEvent(node, 'vnode-unmounted', '$event ? $volar.vnodeUnmounted($event.el) : undefined'); 37 | } 38 | }); 39 | 40 | return __originalExport(rawOptions); 41 | 42 | function addEvent(node, name, exp) { 43 | node.props.push({ 44 | type: 7, 45 | name: 'on', 46 | exp: { 47 | type: 4, 48 | content: exp, 49 | isStatic: false, 50 | constType: 0, 51 | loc: node.loc, 52 | }, 53 | arg: { 54 | type: 4, 55 | content: name, 56 | isStatic: true, 57 | constType: 3, 58 | loc: node.loc, 59 | }, 60 | modifiers: [], 61 | loc: node.loc, 62 | }); 63 | } 64 | } 65 | 66 | const __originalExport = module.exports; 67 | module.exports = __proxyExport; 68 | `; 69 | 70 | const readFileSync = fs.readFileSync; 71 | fs.readFileSync = (...args) => { 72 | if (args[0] === vuePluginPath) { 73 | return readFileSync(...args) + viteExtraCode; 74 | } 75 | return readFileSync(...args); 76 | }; 77 | } catch (e) { 78 | console.warn('Volar: @vitejs/plugin-vue not found, skip vite extra code. (vue@2 only)') 79 | } 80 | 81 | createViteConfig(); 82 | 83 | if (Number(viteVersion.split('.')[0]) >= 3) { 84 | import('file://' + viteBinPath); 85 | } 86 | else { 87 | require(viteBinPath); 88 | } 89 | 90 | function createViteConfig() { 91 | let proxyConfigContent = fs.readFileSync(path.resolve(__dirname, 'vite', 'config.ts'), { encoding: 'utf8' }); 92 | proxyConfigContent = proxyConfigContent.replace('{CONFIG_PATH}', JSON.stringify(path.resolve(workspace, 'vite.config'))); 93 | 94 | if (!fs.existsSync(path.resolve(workspace, 'node_modules', '.vue-preview'))) { 95 | fs.mkdirSync(path.resolve(workspace, 'node_modules', '.vue-preview')); 96 | } 97 | 98 | const proxyConfigPath = path.resolve(workspace, 'node_modules', '.vue-preview', 'vite.config.ts'); 99 | fs.writeFileSync(proxyConfigPath, proxyConfigContent); 100 | 101 | process.argv.push('--config', proxyConfigPath); 102 | } 103 | -------------------------------------------------------------------------------- /packages/vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "vscode-vue-preview", 4 | "version": "0.0.2", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/johnsoncodehk/vue-preview.git", 8 | "directory": "packages/vscode" 9 | }, 10 | "sponsor": { 11 | "url": "https://github.com/sponsors/johnsoncodehk" 12 | }, 13 | "displayName": "Vue and Nuxt Preview (from Volar)", 14 | "description": "Component and App Preview For Vue and Nuxt", 15 | "author": "johnsoncodehk", 16 | "publisher": "johnsoncodehk", 17 | "engines": { 18 | "vscode": "^1.67.0" 19 | }, 20 | "activationEvents": [ 21 | "onLanguage:vue", 22 | "onWebviewPanel:preview" 23 | ], 24 | "main": "./dist/extension.js", 25 | "contributes": { 26 | "views": { 27 | "explorer": [ 28 | { 29 | "id": "vueComponentPreview", 30 | "name": "Vue Component Preview", 31 | "type": "webview", 32 | "when": "vue-preview.foundViteDir || vue-preview.foundNuxtDir" 33 | } 34 | ] 35 | }, 36 | "languages": [ 37 | { 38 | "id": "vue", 39 | "extensions": [ 40 | ".vue" 41 | ] 42 | } 43 | ], 44 | "configuration": { 45 | "type": "object", 46 | "title": "Vue Preview", 47 | "properties": { 48 | "vue-preview.script.vite": { 49 | "type": "string", 50 | "default": "node {VITE_BIN} --port={PORT}" 51 | }, 52 | "vue-preview.script.nuxi": { 53 | "type": "string", 54 | "default": "node {NUXI_BIN} dev --port {PORT}" 55 | }, 56 | "vue-preview.port": { 57 | "type": "number", 58 | "default": 3333, 59 | "description": "Default port for component preview server." 60 | }, 61 | "vue-preview.root": { 62 | "type": "string", 63 | "default": ".", 64 | "description": "Component preview root directory. (For Nuxt, it should be \"./src\" by default.)" 65 | }, 66 | "vue-preview.backgroundColor": { 67 | "type": "string", 68 | "default": "#fff", 69 | "description": "Component preview background color." 70 | }, 71 | "vue-preview.transparentGrid": { 72 | "type": "boolean", 73 | "default": false, 74 | "description": "Component preview background style." 75 | } 76 | } 77 | }, 78 | "commands": [ 79 | { 80 | "command": "vue-preview.action.vite", 81 | "title": "Experimental Features for Vite", 82 | "category": "Vue Preview", 83 | "icon": "images/vite-logo.svg" 84 | }, 85 | { 86 | "command": "vue-preview.action.nuxt", 87 | "title": "Experimental Features for Nuxt", 88 | "category": "Vue Preview", 89 | "icon": "images/nuxt-logo.svg" 90 | } 91 | ], 92 | "menus": { 93 | "commandPalette": [ 94 | { 95 | "command": "vue-preview.action.vite", 96 | "when": "editorLangId == vue" 97 | }, 98 | { 99 | "command": "vue-preview.action.nuxt", 100 | "when": "editorLangId == vue" 101 | } 102 | ], 103 | "editor/title": [ 104 | { 105 | "command": "vue-preview.action.vite", 106 | "when": "editorLangId == vue && vue-preview.foundViteDir", 107 | "group": "navigation" 108 | }, 109 | { 110 | "command": "vue-preview.action.nuxt", 111 | "when": "editorLangId == vue && vue-preview.foundNuxtDir", 112 | "group": "navigation" 113 | } 114 | ] 115 | } 116 | }, 117 | "scripts": { 118 | "prebuild": "cd ../.. && npm run build", 119 | "build": "node scripts/build", 120 | "watch": "npm run build -- --watch", 121 | "prepack": "npm run prebuild && npm run build -- --minify", 122 | "pack": "npm run prepack && vsce package", 123 | "release": "npm run prepack && vsce publish" 124 | }, 125 | "devDependencies": { 126 | "@types/vscode": "1.67.0", 127 | "@vue-preview/core": "0.1.0", 128 | "esbuild": "0.15.18", 129 | "esbuild-plugin-copy": "latest", 130 | "typesafe-path": "^0.2.2", 131 | "vscode-html-languageservice": "^5.0.4", 132 | "vsce": "latest" 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /packages/core/bin/nuxi/plugin.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // remove defineNuxtPlugin() to fixed https://github.com/yaegassy/coc-volar-tools/pull/3#issuecomment-1109155402 3 | export default app => { 4 | 5 | if (process.server) 6 | return; 7 | 8 | const ws = new WebSocket('ws://localhost:56789'); 9 | const finderApis = installGoToCode(); 10 | const highlightApis = installSelectionHighlight(); 11 | 12 | return { 13 | provide: { 14 | volar: { 15 | ...finderApis, 16 | ...highlightApis, 17 | }, 18 | }, 19 | }; 20 | 21 | function installSelectionHighlight() { 22 | 23 | let selection: { 24 | fileName: string, 25 | ranges: { 26 | start, 27 | end, 28 | }[], 29 | isDirty: boolean, 30 | } | undefined; 31 | let updateTimeout: number | undefined; 32 | const nodes = new Map(); 36 | const cursorInOverlays = new Map(); 37 | const rangeCoverOverlays = new Map(); 38 | const cursorInResizeObserver = new ResizeObserver(scheduleUpdate); 39 | const rangeCoverResizeObserver = new ResizeObserver(scheduleUpdate); 40 | 41 | window.addEventListener('scroll', scheduleUpdate); 42 | 43 | ws.addEventListener('message', event => { 44 | const data = JSON.parse(event.data); 45 | if (data?.command === 'highlightSelections') { 46 | selection = data.data; 47 | updateHighlights(); 48 | } 49 | }); 50 | 51 | return { 52 | vnodeMounted, 53 | vnodeUnmounted, 54 | }; 55 | 56 | function vnodeMounted(node: unknown, fileName: string, range: [number, number]) { 57 | if (node instanceof Element) { 58 | nodes.set(node, { 59 | fileName, 60 | range, 61 | }); 62 | scheduleUpdate(); 63 | } 64 | } 65 | function vnodeUnmounted(node: unknown) { 66 | if (node instanceof Element) { 67 | nodes.delete(node); 68 | scheduleUpdate(); 69 | } 70 | } 71 | function scheduleUpdate() { 72 | if (updateTimeout === undefined) { 73 | updateTimeout = setTimeout(() => { 74 | updateHighlights(); 75 | updateTimeout = undefined; 76 | }, 0); 77 | } 78 | } 79 | function updateHighlights() { 80 | 81 | if (selection?.isDirty) { 82 | for (const [_, overlay] of cursorInOverlays) { 83 | overlay.style.opacity = '0.5'; 84 | } 85 | for (const [_, overlay] of rangeCoverOverlays) { 86 | overlay.style.opacity = '0.5'; 87 | } 88 | return; 89 | } 90 | else { 91 | for (const [_, overlay] of cursorInOverlays) { 92 | overlay.style.opacity = '1'; 93 | } 94 | for (const [_, overlay] of rangeCoverOverlays) { 95 | overlay.style.opacity = '1'; 96 | } 97 | } 98 | 99 | const cursorIn = new Set(); 100 | const rangeConver = new Set(); 101 | 102 | if (selection) { 103 | for (const range of selection.ranges) { 104 | for (const [el, loc] of nodes) { 105 | if (loc.fileName.replace(/\\/g, '/').toLowerCase() === selection.fileName.replace(/\\/g, '/').toLowerCase()) { 106 | if (range.start <= loc.range[0] && range.end >= loc.range[1]) { 107 | rangeConver.add(el); 108 | } 109 | else if ( 110 | range.start >= loc.range[0] && range.start <= loc.range[1] 111 | || range.end >= loc.range[0] && range.end <= loc.range[1] 112 | ) { 113 | cursorIn.add(el); 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | for (const [el, overlay] of [...cursorInOverlays]) { 121 | if (!cursorIn.has(el)) { 122 | overlay.remove(); 123 | cursorInOverlays.delete(el); 124 | cursorInResizeObserver.disconnect(el); 125 | } 126 | } 127 | for (const [el, overlay] of [...rangeCoverOverlays]) { 128 | if (!rangeConver.has(el)) { 129 | overlay.remove(); 130 | rangeCoverOverlays.delete(el); 131 | rangeCoverResizeObserver.disconnect(el); 132 | } 133 | } 134 | 135 | for (const el of cursorIn) { 136 | let overlay = cursorInOverlays.get(el); 137 | if (!overlay) { 138 | overlay = createCursorInOverlay(); 139 | cursorInOverlays.set(el, overlay); 140 | cursorInResizeObserver.observe(el); 141 | } 142 | const rect = el.getBoundingClientRect(); 143 | overlay.style.width = ~~rect.width + 'px'; 144 | overlay.style.height = ~~rect.height + 'px'; 145 | overlay.style.top = ~~rect.top + 'px'; 146 | overlay.style.left = ~~rect.left + 'px'; 147 | } 148 | for (const el of rangeConver) { 149 | let overlay = rangeCoverOverlays.get(el); 150 | if (!overlay) { 151 | overlay = createRangeCoverOverlay(); 152 | rangeCoverOverlays.set(el, overlay); 153 | rangeCoverResizeObserver.observe(el); 154 | } 155 | const rect = el.getBoundingClientRect(); 156 | overlay.style.width = ~~rect.width + 'px'; 157 | overlay.style.height = ~~rect.height + 'px'; 158 | overlay.style.top = ~~rect.top + 'px'; 159 | overlay.style.left = ~~rect.left + 'px'; 160 | } 161 | } 162 | function createCursorInOverlay() { 163 | const overlay = document.createElement('div'); 164 | overlay.style.position = 'fixed'; 165 | overlay.style.zIndex = '99999999999999'; 166 | overlay.style.pointerEvents = 'none'; 167 | overlay.style.borderRadius = '3px'; 168 | overlay.style.borderStyle = 'dashed'; 169 | overlay.style.borderColor = 'rgb(196, 105, 183)'; 170 | overlay.style.borderWidth = '1px'; 171 | overlay.style.boxSizing = 'border-box'; 172 | document.body.appendChild(overlay); 173 | return overlay; 174 | } 175 | function createRangeCoverOverlay() { 176 | const overlay = createCursorInOverlay(); 177 | overlay.style.backgroundColor = 'rgba(196, 105, 183, 0.1)'; 178 | return overlay; 179 | } 180 | } 181 | 182 | function installGoToCode() { 183 | 184 | window.addEventListener('scroll', updateOverlay); 185 | window.addEventListener('pointerdown', event => { 186 | disable(true); 187 | }); 188 | window.addEventListener('keydown', event => { 189 | if (event.key === 'Alt') { 190 | enable(); 191 | } 192 | }); 193 | window.addEventListener('keyup', event => { 194 | if (event.key === 'Alt') { 195 | disable(false); 196 | } 197 | }); 198 | 199 | ws.addEventListener('message', event => { 200 | const data = JSON.parse(event.data); 201 | if (data?.command === 'openFile') { 202 | window.open(data.data); 203 | } 204 | }); 205 | 206 | const overlay = createOverlay(); 207 | const clickMask = createClickMask(); 208 | 209 | let highlightNodes: [Element, string, [number, number]][] = []; 210 | let enabled = false; 211 | let lastCodeLoc: any | undefined; 212 | 213 | return { 214 | highlight, 215 | unHighlight, 216 | }; 217 | 218 | function enable() { 219 | enabled = true; 220 | clickMask.style.pointerEvents = 'none'; 221 | document.body.appendChild(clickMask); 222 | updateOverlay(); 223 | } 224 | function disable(openEditor: boolean) { 225 | if (enabled) { 226 | enabled = false; 227 | clickMask.style.pointerEvents = ''; 228 | highlightNodes = []; 229 | updateOverlay(); 230 | if (lastCodeLoc) { 231 | ws.send(JSON.stringify(lastCodeLoc)); 232 | if (openEditor) { 233 | ws.send(JSON.stringify({ 234 | command: 'requestOpenFile', 235 | data: lastCodeLoc.data, 236 | })); 237 | } 238 | lastCodeLoc = undefined; 239 | } 240 | } 241 | } 242 | function goToTemplate(fileName: string, range: [number, number]) { 243 | if (!enabled) return; 244 | lastCodeLoc = { 245 | command: 'goToTemplate', 246 | data: { 247 | fileName, 248 | range, 249 | }, 250 | }; 251 | ws.send(JSON.stringify(lastCodeLoc)); 252 | } 253 | function highlight(node: unknown, fileName: string, range: [number, number]) { 254 | if (node instanceof Element) { 255 | highlightNodes.push([node, fileName, range]); 256 | } 257 | updateOverlay(); 258 | } 259 | function unHighlight(node: Element) { 260 | highlightNodes = highlightNodes.filter(hNode => hNode[0] !== node); 261 | updateOverlay(); 262 | } 263 | function createOverlay() { 264 | const overlay = document.createElement('div'); 265 | overlay.style.backgroundColor = 'rgba(65, 184, 131, 0.35)'; 266 | overlay.style.position = 'fixed'; 267 | overlay.style.zIndex = '99999999999999'; 268 | overlay.style.pointerEvents = 'none'; 269 | overlay.style.display = 'flex'; 270 | overlay.style.alignItems = 'center'; 271 | overlay.style.justifyContent = 'center'; 272 | overlay.style.borderRadius = '3px'; 273 | return overlay; 274 | } 275 | function createClickMask() { 276 | const overlay = document.createElement('div'); 277 | overlay.style.position = 'fixed'; 278 | overlay.style.zIndex = '99999999999999'; 279 | overlay.style.pointerEvents = 'none'; 280 | overlay.style.display = 'flex'; 281 | overlay.style.left = '0'; 282 | overlay.style.right = '0'; 283 | overlay.style.top = '0'; 284 | overlay.style.bottom = '0'; 285 | overlay.addEventListener('pointerup', () => { 286 | if (overlay.parentNode) { 287 | overlay.parentNode?.removeChild(overlay); 288 | } 289 | }); 290 | return overlay; 291 | } 292 | function updateOverlay() { 293 | if (enabled && highlightNodes.length) { 294 | document.body.appendChild(overlay); 295 | const highlight = highlightNodes[highlightNodes.length - 1]; 296 | const highlightNode = highlight[0]; 297 | const rect = highlightNode.getBoundingClientRect(); 298 | overlay.style.width = ~~rect.width + 'px'; 299 | overlay.style.height = ~~rect.height + 'px'; 300 | overlay.style.top = ~~rect.top + 'px'; 301 | overlay.style.left = ~~rect.left + 'px'; 302 | goToTemplate(highlight[1], highlight[2]); 303 | } 304 | else if (overlay.parentNode) { 305 | overlay.parentNode.removeChild(overlay); 306 | } 307 | } 308 | } 309 | }; 310 | -------------------------------------------------------------------------------- /packages/core/bin/vite/config.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import config from {CONFIG_PATH}; 3 | 4 | if (!config.plugins) 5 | config.plugins = []; 6 | 7 | const installCode = ` 8 | function __createAppProxy(...args) { 9 | 10 | const app = createApp(...args); 11 | 12 | const ws = new WebSocket('ws://localhost:56789'); 13 | const finderApis = installGoToCode(); 14 | const highlightApis = installSelectionHighlight(); 15 | 16 | app.config.globalProperties.$volar = { 17 | ...finderApis, 18 | ...highlightApis, 19 | }; 20 | 21 | return app; 22 | 23 | function installSelectionHighlight() { 24 | 25 | let selection; 26 | let updateTimeout; 27 | const nodes = new Map(); 28 | const cursorInOverlays = new Map(); 29 | const rangeCoverOverlays = new Map(); 30 | const cursorInResizeObserver = new ResizeObserver(scheduleUpdate); 31 | const rangeCoverResizeObserver = new ResizeObserver(scheduleUpdate); 32 | 33 | window.addEventListener('scroll', scheduleUpdate); 34 | 35 | ws.addEventListener('message', event => { 36 | const data = JSON.parse(event.data); 37 | if (data?.command === 'highlightSelections') { 38 | selection = data.data; 39 | updateHighlights(); 40 | } 41 | }); 42 | 43 | return { 44 | vnodeMounted, 45 | vnodeUnmounted, 46 | }; 47 | 48 | function vnodeMounted(node, fileName, range) { 49 | if (node instanceof Element) { 50 | nodes.set(node, { 51 | fileName, 52 | range, 53 | }); 54 | scheduleUpdate(); 55 | } 56 | } 57 | function vnodeUnmounted(node) { 58 | if (node instanceof Element) { 59 | nodes.delete(node); 60 | scheduleUpdate(); 61 | } 62 | } 63 | function scheduleUpdate() { 64 | if (updateTimeout === undefined) { 65 | updateTimeout = setTimeout(() => { 66 | updateHighlights(); 67 | updateTimeout = undefined; 68 | }, 0); 69 | } 70 | } 71 | function updateHighlights() { 72 | 73 | if (selection?.isDirty) { 74 | for (const [_, overlay] of cursorInOverlays) { 75 | overlay.style.opacity = '0.5'; 76 | } 77 | for (const [_, overlay] of rangeCoverOverlays) { 78 | overlay.style.opacity = '0.5'; 79 | } 80 | return; 81 | } 82 | else { 83 | for (const [_, overlay] of cursorInOverlays) { 84 | overlay.style.opacity = '1'; 85 | } 86 | for (const [_, overlay] of rangeCoverOverlays) { 87 | overlay.style.opacity = '1'; 88 | } 89 | } 90 | 91 | const cursorIn = new Set(); 92 | const rangeConver = new Set(); 93 | 94 | if (selection) { 95 | for (const range of selection.ranges) { 96 | for (const [el, loc] of nodes) { 97 | if (loc.fileName === selection.fileName) { 98 | if (range.start <= loc.range[0] && range.end >= loc.range[1]) { 99 | rangeConver.add(el); 100 | } 101 | else if ( 102 | range.start >= loc.range[0] && range.start <= loc.range[1] 103 | || range.end >= loc.range[0] && range.end <= loc.range[1] 104 | ) { 105 | cursorIn.add(el); 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | for (const [el, overlay] of [...cursorInOverlays]) { 113 | if (!cursorIn.has(el)) { 114 | overlay.remove(); 115 | cursorInOverlays.delete(el); 116 | cursorInResizeObserver.disconnect(el); 117 | } 118 | } 119 | for (const [el, overlay] of [...rangeCoverOverlays]) { 120 | if (!rangeConver.has(el)) { 121 | overlay.remove(); 122 | rangeCoverOverlays.delete(el); 123 | rangeCoverResizeObserver.disconnect(el); 124 | } 125 | } 126 | 127 | for (const el of cursorIn) { 128 | let overlay = cursorInOverlays.get(el); 129 | if (!overlay) { 130 | overlay = createCursorInOverlay(); 131 | cursorInOverlays.set(el, overlay); 132 | cursorInResizeObserver.observe(el); 133 | } 134 | const rect = el.getBoundingClientRect(); 135 | overlay.style.width = ~~rect.width + 'px'; 136 | overlay.style.height = ~~rect.height + 'px'; 137 | overlay.style.top = ~~rect.top + 'px'; 138 | overlay.style.left = ~~rect.left + 'px'; 139 | } 140 | for (const el of rangeConver) { 141 | let overlay = rangeCoverOverlays.get(el); 142 | if (!overlay) { 143 | overlay = createRangeCoverOverlay(); 144 | rangeCoverOverlays.set(el, overlay); 145 | rangeCoverResizeObserver.observe(el); 146 | } 147 | const rect = el.getBoundingClientRect(); 148 | overlay.style.width = ~~rect.width + 'px'; 149 | overlay.style.height = ~~rect.height + 'px'; 150 | overlay.style.top = ~~rect.top + 'px'; 151 | overlay.style.left = ~~rect.left + 'px'; 152 | } 153 | } 154 | function createCursorInOverlay() { 155 | const overlay = document.createElement('div'); 156 | overlay.style.position = 'fixed'; 157 | overlay.style.zIndex = '99999999999999'; 158 | overlay.style.pointerEvents = 'none'; 159 | overlay.style.borderRadius = '3px'; 160 | overlay.style.borderStyle = 'dashed'; 161 | overlay.style.borderColor = 'rgb(196, 105, 183)'; 162 | overlay.style.borderWidth = '1px'; 163 | overlay.style.boxSizing = 'border-box'; 164 | document.body.appendChild(overlay); 165 | return overlay; 166 | } 167 | function createRangeCoverOverlay() { 168 | const overlay = createCursorInOverlay(); 169 | overlay.style.backgroundColor = 'rgba(196, 105, 183, 0.1)'; 170 | return overlay; 171 | } 172 | } 173 | function installGoToCode() { 174 | window.addEventListener('scroll', updateOverlay); 175 | window.addEventListener('pointerdown', function (ev) { 176 | disable(true); 177 | }); 178 | window.addEventListener('keydown', event => { 179 | if (event.key === 'Alt') { 180 | enable(); 181 | } 182 | }); 183 | window.addEventListener('keyup', event => { 184 | if (event.key === 'Alt') { 185 | disable(false); 186 | } 187 | }); 188 | 189 | ws.addEventListener('message', event => { 190 | const data = JSON.parse(event.data); 191 | if (data?.command === 'openFile') { 192 | window.open(data.data); 193 | } 194 | }); 195 | 196 | var overlay = createOverlay(); 197 | var clickMask = createClickMask(); 198 | var highlightNodes = []; 199 | var enabled = false; 200 | var lastCodeLoc; 201 | 202 | return { 203 | highlight, 204 | unHighlight, 205 | }; 206 | 207 | function enable() { 208 | enabled = true; 209 | clickMask.style.pointerEvents = 'none'; 210 | document.body.appendChild(clickMask); 211 | updateOverlay(); 212 | } 213 | function disable(openEditor) { 214 | if (enabled) { 215 | enabled = false; 216 | clickMask.style.pointerEvents = ''; 217 | highlightNodes = []; 218 | updateOverlay(); 219 | if (lastCodeLoc) { 220 | ws.send(JSON.stringify(lastCodeLoc)); 221 | if (openEditor) { 222 | ws.send(JSON.stringify({ 223 | command: 'requestOpenFile', 224 | data: lastCodeLoc.data, 225 | })); 226 | } 227 | lastCodeLoc = undefined; 228 | } 229 | } 230 | } 231 | function goToTemplate(fileName, range) { 232 | if (!enabled) 233 | return; 234 | lastCodeLoc = { 235 | command: 'goToTemplate', 236 | data: { 237 | fileName: fileName, 238 | range, 239 | }, 240 | }; 241 | ws.send(JSON.stringify(lastCodeLoc)); 242 | } 243 | function highlight(node, fileName, range) { 244 | if (node instanceof Element) { 245 | highlightNodes.push([node, fileName, range]); 246 | } 247 | updateOverlay(); 248 | } 249 | function unHighlight(node) { 250 | highlightNodes = highlightNodes.filter(function (hNode) { return hNode[0] !== node; }); 251 | updateOverlay(); 252 | } 253 | function createOverlay() { 254 | var overlay = document.createElement('div'); 255 | overlay.style.backgroundColor = 'rgba(65, 184, 131, 0.35)'; 256 | overlay.style.position = 'fixed'; 257 | overlay.style.zIndex = '99999999999999'; 258 | overlay.style.pointerEvents = 'none'; 259 | overlay.style.display = 'flex'; 260 | overlay.style.alignItems = 'center'; 261 | overlay.style.justifyContent = 'center'; 262 | overlay.style.borderRadius = '3px'; 263 | return overlay; 264 | } 265 | function createClickMask() { 266 | var overlay = document.createElement('div'); 267 | overlay.style.position = 'fixed'; 268 | overlay.style.zIndex = '99999999999999'; 269 | overlay.style.pointerEvents = 'none'; 270 | overlay.style.display = 'flex'; 271 | overlay.style.left = '0'; 272 | overlay.style.right = '0'; 273 | overlay.style.top = '0'; 274 | overlay.style.bottom = '0'; 275 | overlay.addEventListener('pointerup', function () { 276 | var _a; 277 | if (overlay.parentNode) { 278 | (_a = overlay.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(overlay); 279 | } 280 | }); 281 | return overlay; 282 | } 283 | function updateOverlay() { 284 | if (enabled && highlightNodes.length) { 285 | document.body.appendChild(overlay); 286 | var highlight_1 = highlightNodes[highlightNodes.length - 1]; 287 | var highlightNode = highlight_1[0]; 288 | var rect = highlightNode.getBoundingClientRect(); 289 | overlay.style.width = ~~rect.width + 'px'; 290 | overlay.style.height = ~~rect.height + 'px'; 291 | overlay.style.top = ~~rect.top + 'px'; 292 | overlay.style.left = ~~rect.left + 'px'; 293 | goToTemplate(highlight_1[1], highlight_1[2]); 294 | } 295 | else if (overlay.parentNode) { 296 | overlay.parentNode.removeChild(overlay); 297 | } 298 | } 299 | } 300 | } 301 | `; 302 | 303 | config.plugins.push({ 304 | name: '__volar_preview', 305 | transform(this, code, id, options?) { 306 | const createAppText = 'createApp,'; 307 | if (id.indexOf('/vue.js?') >= 0 && code.indexOf(createAppText) >= 0 && code.indexOf('__createAppProxy') === -1) { 308 | const createAppOffset = code.lastIndexOf(createAppText); 309 | code = 310 | code.substring(0, createAppOffset) 311 | + '__createAppProxy as createApp,' 312 | + code.substring(createAppOffset + createAppText.length) 313 | + `${installCode}`; 314 | } 315 | return code; 316 | }, 317 | }); 318 | 319 | export default config; 320 | -------------------------------------------------------------------------------- /packages/vscode/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'typesafe-path'; 3 | import * as posix from 'typesafe-path/posix'; 4 | import * as fs from './utils/fs'; 5 | import * as preview from '@vue-preview/core'; 6 | import { getLocalHostAvailablePort } from './utils/http'; 7 | import * as html from 'vscode-html-languageservice'; 8 | import { quickPick } from './utils/quickPick'; 9 | 10 | const htmlLs = html.getLanguageService(); 11 | 12 | const enum PreviewType { 13 | Webview = 'vue-preview-webview', 14 | ExternalBrowser = 'vue-preview-start-server', 15 | ExternalBrowser_Component = 'vue-preview-component-preview', 16 | } 17 | 18 | export async function activate(context: vscode.ExtensionContext) { 19 | 20 | let _loadingPanel: vscode.WebviewPanel | undefined; 21 | let avoidUpdateOnDidChangeActiveTextEditor = false; 22 | let updateComponentPreview: Function | undefined; 23 | 24 | const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, -1); 25 | statusBar.command = 'vue-preview.previewMenu'; 26 | statusBar.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); 27 | context.subscriptions.push(statusBar); 28 | 29 | let connection: ReturnType | undefined; 30 | let highlightDomElements = true; 31 | 32 | const previewTerminal = vscode.window.terminals.find(terminal => terminal.name.startsWith('vue-preview:')); 33 | if (previewTerminal) { 34 | connection = preview.createPreviewConnection({ 35 | onGotoCode: handleGoToCode, 36 | getFileHref: (fileName) => { 37 | avoidUpdateOnDidChangeActiveTextEditor = false; 38 | updateComponentPreview?.(); 39 | return 'vscode://files:/' + fileName; 40 | }, 41 | }); 42 | statusBar.text = 'Preview Port: ' + previewTerminal.name.split(':')[2]; 43 | statusBar.show(); 44 | } 45 | vscode.window.onDidOpenTerminal(e => { 46 | if (e.name.startsWith('vue-preview:')) { 47 | connection = preview.createPreviewConnection({ 48 | onGotoCode: handleGoToCode, 49 | getFileHref: (fileName) => { 50 | avoidUpdateOnDidChangeActiveTextEditor = false; 51 | updateComponentPreview?.(); 52 | return 'vscode://files:/' + fileName; 53 | }, 54 | }); 55 | statusBar.text = 'Preview Port: ' + e.name.split(':')[2]; 56 | statusBar.show(); 57 | } 58 | }); 59 | vscode.window.onDidCloseTerminal(e => { 60 | if (e.name.startsWith('vue-preview:')) { 61 | connection?.stop(); 62 | connection = undefined; 63 | statusBar.hide(); 64 | } 65 | }); 66 | 67 | const templateOffsets = new WeakMap(); 68 | 69 | class VueComponentPreview implements vscode.WebviewViewProvider { 70 | 71 | public resolveWebviewView( 72 | webviewView: vscode.WebviewView, 73 | _context: vscode.WebviewViewResolveContext, 74 | _token: vscode.CancellationToken, 75 | ) { 76 | 77 | let lastPreviewDocument: vscode.TextDocument | undefined; 78 | let updating: Promise | undefined; 79 | 80 | webviewView.webview.options = { 81 | enableScripts: true, 82 | }; 83 | updateWebView(true); 84 | updateComponentPreview = updateWebView; 85 | 86 | vscode.window.onDidChangeActiveTextEditor(() => { 87 | if (avoidUpdateOnDidChangeActiveTextEditor) 88 | return; 89 | if (!vscode.window.activeTextEditor || lastPreviewDocument === vscode.window.activeTextEditor.document) 90 | return; 91 | updateWebView(false); 92 | }); 93 | vscode.workspace.onDidChangeTextDocument(() => updateWebView(false)); 94 | vscode.workspace.onDidChangeConfiguration(() => updateWebView(true)); 95 | 96 | webviewView.onDidChangeVisibility(() => updateWebView(false)); 97 | 98 | function updateWebView(refresh: boolean) { 99 | if (updating) { 100 | updating = updating.then(() => updateWebViewWorker(refresh)); 101 | } 102 | else { 103 | updating = updateWebViewWorker(refresh); 104 | } 105 | } 106 | 107 | async function updateWebViewWorker(refresh: boolean) { 108 | 109 | if (!webviewView.visible) 110 | return; 111 | 112 | if (vscode.window.activeTextEditor?.document.languageId === 'vue') 113 | lastPreviewDocument = vscode.window.activeTextEditor.document; 114 | 115 | if (!lastPreviewDocument) 116 | return; 117 | 118 | const fileName = lastPreviewDocument.fileName as path.OsPath; 119 | let terminal = vscode.window.terminals.find(terminal => terminal.name.startsWith('vue-preview:')); 120 | let port: number; 121 | let configFile = await getConfigFile(fileName, 'vite'); 122 | let previewMode: 'vite' | 'nuxt' = 'vite'; 123 | 124 | if (!configFile) { 125 | configFile = await getConfigFile(fileName, 'nuxt'); 126 | previewMode = 'nuxt'; 127 | } 128 | if (!configFile) 129 | return; 130 | 131 | if (terminal) { 132 | port = Number(terminal.name.split(':')[2]); 133 | } 134 | else { 135 | const configDir = path.dirname(configFile); 136 | const server = await startPreviewServer(configDir, previewMode); 137 | terminal = server.terminal; 138 | port = server.port; 139 | } 140 | 141 | const root = vscode.workspace.getConfiguration('vue-preview').get('root')!; 142 | const relativePath = path.relative(path.resolve(path.dirname(configFile), root), fileName).replace(/\\/g, '/') as path.PosixPath; 143 | let url = `http://localhost:${port}/${posix.join('__preview' as path.PosixPath, relativePath)}#`; 144 | 145 | if (lastPreviewDocument.isDirty) { 146 | url += btoa(lastPreviewDocument.getText()); 147 | } 148 | 149 | if (refresh) { 150 | 151 | const bgPath = vscode.Uri.file(path.join(context.extensionPath as path.OsPath, 'images/preview-bg.png' as path.PosixPath)); 152 | const bgSrc = webviewView.webview.asWebviewUri(bgPath); 153 | 154 | webviewView.webview.html = ''; 155 | webviewView.webview.html = getWebviewContent(url, bgSrc.toString()); 156 | } 157 | else { 158 | webviewView.webview.postMessage({ 159 | sender: 'volar', 160 | command: 'updateUrl', 161 | data: url, 162 | }); 163 | } 164 | } 165 | } 166 | } 167 | 168 | vscode.window.registerWebviewViewProvider( 169 | 'vueComponentPreview', 170 | new VueComponentPreview(), 171 | ); 172 | 173 | context.subscriptions.push(vscode.commands.registerCommand('vue-preview.previewMenu', async () => { 174 | 175 | const baseOptions: Record = {}; 176 | const urlOptions: Record = {}; 177 | const highlight: Record = {}; 178 | 179 | baseOptions['kill'] = { label: 'Kill Preview Server' }; 180 | baseOptions['browser'] = { label: 'Open in Browser' }; 181 | highlight['highlight-on'] = { label: (highlightDomElements ? '• ' : '') + 'Highlight DOM Elements' }; 182 | highlight['highlight-off'] = { label: (!highlightDomElements ? '• ' : '') + `Don't Highlight DOM Elements` }; 183 | 184 | const key = await quickPick([baseOptions, urlOptions, highlight]); 185 | 186 | if (key === 'kill') { 187 | for (const terminal of vscode.window.terminals) { 188 | if (terminal.name.startsWith('vue-preview:')) { 189 | terminal.dispose(); 190 | } 191 | } 192 | } 193 | if (key === 'browser') { 194 | vscode.env.openExternal(vscode.Uri.parse('http://localhost:' + statusBar.text.split(':')[1].trim())); 195 | } 196 | if (key === 'highlight-on') { 197 | highlightDomElements = true; 198 | if (vscode.window.activeTextEditor) { 199 | updateSelectionHighlights(vscode.window.activeTextEditor); 200 | } 201 | } 202 | if (key === 'highlight-off') { 203 | highlightDomElements = false; 204 | if (vscode.window.activeTextEditor) { 205 | updateSelectionHighlights(vscode.window.activeTextEditor); 206 | } 207 | } 208 | })); 209 | context.subscriptions.push(vscode.commands.registerCommand('vue-preview.action.vite', async () => { 210 | 211 | const editor = vscode.window.activeTextEditor; 212 | if (!editor) 213 | return; 214 | 215 | const viteConfigFile = await getConfigFile(editor.document.fileName as path.OsPath, 'vite'); 216 | const select = await quickPick({ 217 | [PreviewType.Webview]: { 218 | label: 'Preview Vite App', 219 | detail: vscode.workspace.rootPath && viteConfigFile ? path.relative(vscode.workspace.rootPath as path.OsPath, viteConfigFile) : viteConfigFile, 220 | description: 'Press `Alt` to use go to code feature', 221 | }, 222 | [PreviewType.ExternalBrowser]: { 223 | label: 'Preview Vite App in External Browser', 224 | detail: vscode.workspace.rootPath && viteConfigFile ? path.relative(vscode.workspace.rootPath as path.OsPath, viteConfigFile) : viteConfigFile, 225 | description: 'Press `Alt` to use go to code feature', 226 | }, 227 | [PreviewType.ExternalBrowser_Component]: { 228 | label: `Preview Component in External Browser`, 229 | detail: vscode.workspace.rootPath ? path.relative(vscode.workspace.rootPath as path.OsPath, editor.document.fileName as path.OsPath) : editor.document.fileName, 230 | }, 231 | }); 232 | if (select === undefined) 233 | return; // cancel 234 | 235 | openPreview(select as PreviewType, editor.document.fileName as path.OsPath, 'vite'); 236 | })); 237 | context.subscriptions.push(vscode.commands.registerCommand('vue-preview.action.nuxt', async () => { 238 | 239 | const editor = vscode.window.activeTextEditor; 240 | if (!editor) 241 | return; 242 | 243 | const viteConfigFile = await getConfigFile(editor.document.fileName as path.OsPath, 'nuxt'); 244 | const root = vscode.workspace.getConfiguration('vue-preview').get('root')!; 245 | const select = await quickPick({ 246 | [PreviewType.Webview]: { 247 | label: 'Preview Nuxt App', 248 | detail: vscode.workspace.rootPath && viteConfigFile ? path.relative(vscode.workspace.rootPath as path.OsPath, viteConfigFile) : viteConfigFile, 249 | }, 250 | [PreviewType.ExternalBrowser]: { 251 | label: 'Preview Nuxt App in External Browser', 252 | detail: vscode.workspace.rootPath && viteConfigFile ? path.relative(vscode.workspace.rootPath as path.OsPath, viteConfigFile) : viteConfigFile, 253 | description: 'Press `Alt` to use go to code in Browser', 254 | }, 255 | [PreviewType.ExternalBrowser_Component]: { 256 | label: `Preview Component in External Browser`, 257 | detail: vscode.workspace.rootPath ? path.relative(path.resolve(vscode.workspace.rootPath as path.OsPath, root), editor.document.fileName as path.OsPath) : editor.document.fileName, 258 | }, 259 | }); 260 | if (select === undefined) 261 | return; // cancel 262 | 263 | openPreview(select as PreviewType, editor.document.fileName as path.OsPath, 'nuxt'); 264 | })); 265 | context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(e => { 266 | updateSelectionHighlights(e.textEditor); 267 | })); 268 | context.subscriptions.push(vscode.workspace.onDidChangeTextDocument((e) => { 269 | if (vscode.window.activeTextEditor?.document === e.document) { 270 | updateSelectionHighlights(vscode.window.activeTextEditor); 271 | } 272 | })); 273 | context.subscriptions.push(vscode.workspace.onDidSaveTextDocument((document) => { 274 | if (vscode.window.activeTextEditor?.document === document) { 275 | updateSelectionHighlights(vscode.window.activeTextEditor); 276 | } 277 | })); 278 | 279 | context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(updatePreviewIconStatus)); 280 | 281 | updatePreviewIconStatus(); 282 | 283 | function getTemplateOffset(document: vscode.TextDocument) { 284 | let cache = templateOffsets.get(document); 285 | if (!cache || cache.version !== document.version) { 286 | templateOffsets.delete(document); 287 | const htmlDocument = htmlLs.parseHTMLDocument(html.TextDocument.create(document.uri.toString(), document.languageId, document.version, document.getText())); 288 | const template = htmlDocument.roots.find(node => node.tag === 'template'); 289 | if (template?.startTagEnd !== undefined) { 290 | templateOffsets.set(document, { 291 | version: document.version, 292 | offset: template.startTagEnd, 293 | }); 294 | } 295 | } 296 | return templateOffsets.get(document)?.offset ?? 0; 297 | } 298 | 299 | async function updatePreviewIconStatus() { 300 | if (vscode.window.activeTextEditor?.document.languageId === 'vue') { 301 | 302 | const viteConfigFile = await getConfigFile(vscode.window.activeTextEditor.document.fileName as path.OsPath, 'vite'); 303 | const nuxtConfigFile = await getConfigFile(vscode.window.activeTextEditor.document.fileName as path.OsPath, 'nuxt'); 304 | 305 | vscode.commands.executeCommand('setContext', 'vue-preview.foundViteDir', viteConfigFile !== undefined); 306 | vscode.commands.executeCommand('setContext', 'vue-preview.foundNuxtDir', nuxtConfigFile !== undefined); 307 | } 308 | } 309 | 310 | async function updateSelectionHighlights(textEditor: vscode.TextEditor) { 311 | if (connection && textEditor.document.languageId === 'vue' && highlightDomElements) { 312 | const offset = await getTemplateOffset(textEditor.document); 313 | connection.highlight( 314 | textEditor.document.fileName, 315 | textEditor.selections.map(selection => ({ 316 | start: textEditor.document.offsetAt(selection.start) - offset, 317 | end: textEditor.document.offsetAt(selection.end) - offset, 318 | })), 319 | textEditor.document.isDirty, 320 | ); 321 | } 322 | else { 323 | connection?.unhighlight(); 324 | } 325 | } 326 | 327 | async function openPreview(previewType: PreviewType, fileName: path.OsPath, mode: 'vite' | 'nuxt', _panel?: vscode.WebviewPanel) { 328 | 329 | const configFile = await getConfigFile(fileName, mode); 330 | if (!configFile) 331 | return; 332 | 333 | let terminal = vscode.window.terminals.find(terminal => terminal.name.startsWith('vue-preview:')); 334 | let port: number; 335 | 336 | if (terminal) { 337 | port = Number(terminal.name.split(':')[2]); 338 | } 339 | else { 340 | const configDir = path.dirname(configFile); 341 | const server = await startPreviewServer(configDir, mode); 342 | terminal = server.terminal; 343 | port = server.port; 344 | } 345 | 346 | const loadingPanel = _panel ?? vscode.window.createWebviewPanel( 347 | previewType, 348 | 'Preview ' + path.relative((vscode.workspace.rootPath ?? '') as path.OsPath, configFile), 349 | vscode.ViewColumn.Beside, 350 | { 351 | retainContextWhenHidden: true, 352 | enableScripts: true, 353 | enableFindWidget: true, 354 | }, 355 | ); 356 | 357 | const panelContext: vscode.Disposable[] = []; 358 | 359 | loadingPanel.onDidDispose(() => { 360 | for (const disposable of panelContext) { 361 | disposable.dispose(); 362 | } 363 | }); 364 | 365 | panelContext.push(loadingPanel.webview.onDidReceiveMessage(webviewEventHandler)); 366 | 367 | terminal.show(); 368 | _loadingPanel = loadingPanel; 369 | 370 | if (previewType === PreviewType.ExternalBrowser) { 371 | loadingPanel.webview.html = getWebviewContent(`http://localhost:${port}`, undefined, 'openExternal'); 372 | } 373 | else if (previewType === PreviewType.ExternalBrowser_Component) { 374 | const root = vscode.workspace.getConfiguration('vue-preview').get('root')!; 375 | const relativePath = path.relative(path.resolve(path.dirname(configFile), root), fileName).replace(/\\/g, '/') as path.PosixPath; 376 | loadingPanel.webview.html = getWebviewContent(`http://localhost:${port}/${posix.join('__preview' as path.PosixPath, relativePath)}`, undefined, 'openExternal'); 377 | } 378 | else if (previewType === PreviewType.Webview) { 379 | loadingPanel.webview.html = getWebviewContent(`http://localhost:${port}`, undefined, 'openSimpleBrowser'); 380 | } 381 | 382 | return port; 383 | 384 | async function webviewEventHandler(message: any) { 385 | switch (message.command) { 386 | case 'openUrl': { 387 | const { url, external } = message.data; 388 | if (external) { 389 | vscode.env.openExternal(vscode.Uri.parse(url)); 390 | } 391 | else { 392 | vscode.commands.executeCommand('simpleBrowser.api.open', url, { preserveFocus: true, viewColumn: _loadingPanel?.viewColumn ?? vscode.ViewColumn.Beside }); 393 | } 394 | _loadingPanel?.dispose(); 395 | break; 396 | } 397 | case 'log': { 398 | const text = message.data; 399 | vscode.window.showInformationMessage(text); 400 | break; 401 | } 402 | case 'warn': { 403 | const text = message.data; 404 | vscode.window.showWarningMessage(text); 405 | break; 406 | } 407 | case 'error': { 408 | const text = message.data; 409 | vscode.window.showErrorMessage(text); 410 | break; 411 | } 412 | } 413 | } 414 | } 415 | 416 | async function handleGoToCode(fileName: string, range: [number, number], cancelToken: { readonly isCancelled: boolean; }) { 417 | 418 | avoidUpdateOnDidChangeActiveTextEditor = true; 419 | 420 | const doc = await vscode.workspace.openTextDocument(fileName); 421 | 422 | if (cancelToken.isCancelled) 423 | return; 424 | 425 | const offset = await getTemplateOffset(doc); 426 | const start = doc.positionAt(range[0] + offset); 427 | const end = doc.positionAt(range[1] + offset); 428 | await vscode.window.showTextDocument(doc, vscode.ViewColumn.One); 429 | 430 | if (cancelToken.isCancelled) 431 | return; 432 | 433 | const editor = vscode.window.activeTextEditor; 434 | if (editor) { 435 | editor.selection = new vscode.Selection(start, end); 436 | editor.revealRange(new vscode.Range(start, end)); 437 | } 438 | } 439 | 440 | async function startPreviewServer(viteDir: string, type: 'vite' | 'nuxt') { 441 | 442 | const port = await getLocalHostAvailablePort(vscode.workspace.getConfiguration('vue-preview').get('port')!); 443 | let script = await vscode.workspace.getConfiguration('vue-preview').get('script.' + (type === 'nuxt' ? 'nuxi' : 'vite')) ?? ''; 444 | 445 | if (script.indexOf('{VITE_BIN}') >= 0) { 446 | script = script.replace('{VITE_BIN}', JSON.stringify(require.resolve('./dist/bin/vite', { paths: [context.extensionPath] }))); 447 | } 448 | if (script.indexOf('{NUXI_BIN}') >= 0) { 449 | script = script.replace('{NUXI_BIN}', JSON.stringify(require.resolve('./dist/bin/nuxi', { paths: [context.extensionPath] }))); 450 | } 451 | if (script.indexOf('{PORT}') >= 0) { 452 | script = script.replace('{PORT}', port.toString()); 453 | } 454 | 455 | const terminal = vscode.window.createTerminal({ 456 | name: 'vue-preview:' + type + ':' + port, 457 | isTransient: true, 458 | }); 459 | terminal.sendText(`cd ${JSON.stringify(viteDir)}`); 460 | terminal.sendText(script); 461 | 462 | return { 463 | port, 464 | terminal, 465 | }; 466 | } 467 | 468 | async function getConfigFile(fileName: path.OsPath, mode: 'vite' | 'nuxt') { 469 | let dir = path.dirname(fileName); 470 | let configFile: path.OsPath | undefined; 471 | while (true) { 472 | const configTs = path.join(dir, mode + '.config.ts' as path.PosixPath); 473 | const configJs = path.join(dir, mode + '.config.js' as path.PosixPath); 474 | if (await fs.exists(vscode.Uri.file(configTs))) { 475 | configFile = configTs; 476 | break; 477 | } 478 | if (await fs.exists(vscode.Uri.file(configJs))) { 479 | configFile = configJs; 480 | break; 481 | } 482 | const upperDir = path.dirname(dir); 483 | if (upperDir === dir) { 484 | break; 485 | } 486 | dir = upperDir; 487 | } 488 | return configFile; 489 | } 490 | 491 | function getWebviewContent(url: string, bg?: string, onLoadEvent?: 'openExternal' | 'openSimpleBrowser') { 492 | 493 | const configs = vscode.workspace.getConfiguration('vue-preview'); 494 | 495 | let html = ` 496 | 503 | 504 | 566 | 567 |
568 |
569 | 570 | 571 | 572 | 573 | 597 |
598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 |
622 | `; 623 | 624 | return html; 625 | } 626 | } 627 | --------------------------------------------------------------------------------