├── .npmrc ├── src ├── monaco │ ├── languages │ │ └── html │ │ │ ├── interfaces.ts │ │ │ ├── fillers │ │ │ ├── monaco-editor-core.ts │ │ │ ├── monaco-editor-core-amd.ts │ │ │ └── vscode-nls.ts │ │ │ ├── html.worker.ts │ │ │ ├── workerManager.ts │ │ │ ├── htmlWorker.ts │ │ │ ├── htmlMode.ts │ │ │ ├── monaco.contribution.ts │ │ │ └── languageFeatures.ts │ ├── plugins │ │ ├── windicss │ │ │ ├── README.md │ │ │ ├── interfaces.ts │ │ │ ├── utils │ │ │ │ ├── fileTypes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── completions.ts │ │ │ │ └── utilities.ts │ │ │ ├── decorations.ts │ │ │ └── index.ts │ │ ├── editor.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── vue │ │ │ ├── index.ts │ │ │ └── meta.ts │ │ └── utils │ │ │ └── index.ts │ └── index.ts ├── vite-env.d.ts ├── assets │ └── logo.png ├── vue-dev-proxy.ts ├── logic │ ├── dark.ts │ ├── useZip │ │ └── index.ts │ ├── useStyleSheet │ │ └── index.ts │ ├── useMonaco │ │ └── index.ts │ └── PreviewProxy.ts ├── components │ ├── settings │ │ ├── EditorSettings.vue │ │ ├── WindiCSSSettings.vue │ │ ├── PackagesSettings.vue │ │ ├── SettingsTab.vue │ │ ├── InstallSettings.vue │ │ ├── PackageVersion.vue │ │ ├── ManuallyInstallPackage.vue │ │ ├── PackageItem.vue │ │ └── Settings.vue │ ├── playground │ │ ├── Message.vue │ │ ├── Console.vue │ │ ├── Editor.vue │ │ ├── Tab.vue │ │ ├── TabBar.vue │ │ ├── Playground.vue │ │ └── Preview.vue │ ├── ui │ │ ├── SelectItem.vue │ │ ├── Button.vue │ │ ├── Textfield.vue │ │ ├── Select.vue │ │ ├── Container.vue │ │ └── Navigation.vue │ └── template.html ├── shims-vue.d.ts ├── main.ts ├── App.vue ├── data │ └── screen-sizes.json ├── styles │ └── main.css ├── compiler │ ├── windi.ts │ ├── sfcCompiler.ts │ └── moduleCompiler.ts └── orchestrator.ts ├── .eslintignore ├── .gitignore ├── public ├── favicon.ico ├── favicon.svg └── vueuse.svg ├── .eslintrc ├── .vscode └── settings.json ├── README.md ├── netlify.toml ├── demos └── default │ ├── App.vue │ ├── Coordinate.vue │ └── packages.json ├── tsconfig.json ├── plugins └── copy-vue.ts ├── LICENSE ├── windi.config.ts ├── package.json ├── index.html ├── vite.config.ts └── components.d.ts /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /src/monaco/languages/html/interfaces.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | public -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vueuse/playground/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vueuse/playground/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/vue-dev-proxy.ts: -------------------------------------------------------------------------------- 1 | // serve vue to the iframe sandbox during dev. 2 | export * from 'vue' -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu", 3 | "rules": { 4 | "no-console": "off" 5 | } 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": [ 3 | "src/languages" 4 | ] 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Playground 2 | 3 | Experimental Playground for a VueUse sandbox 4 | 5 | > Inspired by Evan You's [Vue SFC Playground](https://sfc.vuejs.org/) -------------------------------------------------------------------------------- /src/logic/dark.ts: -------------------------------------------------------------------------------- 1 | import { useDark, useToggle } from '@vueuse/core' 2 | 3 | export const isDark = useDark() 4 | export const toggleDark = useToggle(isDark) 5 | -------------------------------------------------------------------------------- /src/components/settings/EditorSettings.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /src/components/settings/WindiCSSSettings.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/monaco/plugins/windicss/README.md: -------------------------------------------------------------------------------- 1 | The source code for this was either heavily inspired or directly taken from the windicss-intellisense plugin 2 | 3 | https://github.com/windicss/windicss-intellisense 4 | -------------------------------------------------------------------------------- /src/monaco/plugins/editor.ts: -------------------------------------------------------------------------------- 1 | import { EditorPlugin } from './types' 2 | import { WindiDecoration } from './windicss/decorations' 3 | 4 | export const editorPlugins: EditorPlugin[] = [ 5 | WindiDecoration, 6 | ] 7 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import 'splitpanes/dist/splitpanes.css' 4 | import 'virtual:windi.css' 5 | import './styles/main.css' 6 | 7 | createApp(App).mount('#app') 8 | -------------------------------------------------------------------------------- /src/monaco/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import type { HTMLPlugin } from './types' 2 | import { windicssHTMLPlugin } from './windicss' 3 | import { vueHTMLPlugin } from './vue' 4 | 5 | export const htmlCompletionPlugins: HTMLPlugin[] = [ 6 | windicssHTMLPlugin, 7 | vueHTMLPlugin, 8 | ] 9 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NPM_FLAGS = "--prefix=/dev/null" 3 | NODE_VERSION = "14" 4 | 5 | [build] 6 | publish = "dist" 7 | command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run build" 8 | 9 | [[redirects]] 10 | from = "/*" 11 | to = "/index.html" 12 | status = 200 13 | 14 | [[headers]] 15 | for = "/manifest.webmanifest" 16 | [headers.values] 17 | Content-Type = "application/manifest+json" -------------------------------------------------------------------------------- /src/data/screen-sizes.json: -------------------------------------------------------------------------------- 1 | { 2 | "Default": [0, 0], 3 | "Moto 4G": [360, 640], 4 | "Galaxy S5": [360, 640], 5 | "Pixel 2": [411, 731], 6 | "Pixel 2 XL": [411, 823], 7 | "iPhone 5/SE": [320, 568], 8 | "iPhone 6/7/8": [375, 667], 9 | "iPhone 6/7/8 Plus": [414, 736], 10 | "iPhone X": [375, 812], 11 | "iPad": [768, 1024], 12 | "iPad Pro": [1024, 1366], 13 | "Surface Duo": [540, 720], 14 | "Galaxy Fold": [280, 653] 15 | } 16 | -------------------------------------------------------------------------------- /src/monaco/languages/html/fillers/monaco-editor-core.ts: -------------------------------------------------------------------------------- 1 | /* --------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *-------------------------------------------------------------------------------------------- */ 5 | 6 | export * from 'monaco-editor-core' 7 | -------------------------------------------------------------------------------- /demos/default/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /demos/default/Coordinate.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/vueuse.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "sourceMap": true, 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "lib": ["esnext", "dom"], 12 | "types": [ 13 | "vite-plugin-pwa/client" 14 | ], 15 | "paths": { 16 | "~/*": ["./src/*"] 17 | } 18 | }, 19 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 20 | } 21 | -------------------------------------------------------------------------------- /src/components/playground/Message.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/components/playground/Console.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | -------------------------------------------------------------------------------- /src/monaco/languages/html/fillers/monaco-editor-core-amd.ts: -------------------------------------------------------------------------------- 1 | /* --------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *-------------------------------------------------------------------------------------------- */ 5 | 6 | // Resolves with the global monaco API 7 | 8 | declare let define: any 9 | 10 | define([], () => { 11 | return (self).monaco 12 | }) 13 | -------------------------------------------------------------------------------- /src/monaco/plugins/types.ts: -------------------------------------------------------------------------------- 1 | import type { CompletionItem, TextDocument, HTMLDocument, Position } from 'vscode-html-languageservice' 2 | import type { editor as Editor } from 'monaco-editor' 3 | 4 | export interface HTMLPluginCompletion { 5 | position: Position 6 | document: TextDocument 7 | html: HTMLDocument 8 | } 9 | 10 | export interface HTMLPlugin { 11 | completions(options: HTMLPluginCompletion): CompletionItem[] 12 | } 13 | 14 | export interface EditorPlugin { 15 | language: string 16 | onContentChanged: (editor: Editor.IStandaloneCodeEditor) => void 17 | [key: string]: any 18 | } 19 | -------------------------------------------------------------------------------- /src/components/ui/SelectItem.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/logic/useZip/index.ts: -------------------------------------------------------------------------------- 1 | import Zip, { JSZipFileOptions } from 'jszip' 2 | import { saveAs } from 'file-saver' 3 | 4 | export function useZip() { 5 | const zip = new Zip() 6 | 7 | const createFolder = (name: string) => { 8 | zip.folder(name) 9 | } 10 | 11 | const createFile = (path: string, data: any, options?: JSZipFileOptions) => { 12 | zip.file(path, data, options) 13 | } 14 | 15 | const download = () => { 16 | zip.generateAsync({ type: 'blob' }) 17 | .then((blob) => { 18 | saveAs(blob, 'Playground Project') 19 | }) 20 | } 21 | 22 | return { 23 | createFolder, 24 | createFile, 25 | download, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/playground/Editor.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /demos/default/packages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "vue-demi", 4 | "source": "unpkg", 5 | "description": "Vue Demi (half in French) is a developing utility allows you to write Universal Vue Libraries for Vue 2 & 3", 6 | "url": "https://unpkg.com/vue-demi/lib/index.esm.js" 7 | }, 8 | { 9 | "name": "@vueuse/shared", 10 | "source": "unpkg", 11 | "description": "Shared VueUse utilities.", 12 | "url": "https://unpkg.com/@vueuse/shared/dist/index.esm.js" 13 | }, 14 | { 15 | "name": "@vueuse/core", 16 | "source": "unpkg", 17 | "description": "Collection of essential Vue Composition Utilities", 18 | "url": "https://unpkg.com/@vueuse/core/dist/index.esm.js" 19 | } 20 | ] -------------------------------------------------------------------------------- /src/monaco/languages/html/html.worker.ts: -------------------------------------------------------------------------------- 1 | /* --------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *-------------------------------------------------------------------------------------------- */ 5 | 6 | import * as worker from 'monaco-editor-core/esm/vs/editor/editor.worker' 7 | import { HTMLWorker } from './htmlWorker' 8 | 9 | self.onmessage = () => { 10 | // ignore the first message 11 | worker.initialize((ctx: any, createData: any) => { 12 | return new HTMLWorker(ctx, createData) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/ui/Button.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | -------------------------------------------------------------------------------- /src/components/ui/Textfield.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | -------------------------------------------------------------------------------- /src/components/settings/PackagesSettings.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 28 | -------------------------------------------------------------------------------- /plugins/copy-vue.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'vite' 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | export function copyVuePlugin(): Plugin { 6 | return { 7 | name: 'copy-vue', 8 | generateBundle() { 9 | const filePath = path.resolve( 10 | __dirname, 11 | '../node_modules/vue/dist/vue.runtime.esm-browser.js' 12 | ) 13 | console.log(filePath) 14 | if (!fs.existsSync(filePath)) { 15 | throw new Error( 16 | `vue.runtime.esm-browser.js not built. ` + 17 | `Run "yarn build vue -f esm-browser" first.` 18 | ) 19 | } 20 | this.emitFile({ 21 | type: 'asset', 22 | fileName: 'vue.runtime.esm-browser.js', 23 | source: fs.readFileSync(filePath, 'utf-8') 24 | }) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/components/settings/SettingsTab.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 36 | -------------------------------------------------------------------------------- /src/logic/useStyleSheet/index.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue' 2 | 3 | export interface StyleSheetRules { 4 | [key: string]: string 5 | } 6 | 7 | export function useStyleSheet(name: string) { 8 | const el: HTMLStyleElement = document.createElement('style') 9 | el.title = name 10 | document.body.appendChild(el) 11 | const rules = ref({}) 12 | const stylesheet: CSSStyleSheet | undefined = Object.values(document.styleSheets).find(({ title }) => title === name) 13 | 14 | watch(rules, (styles) => { 15 | if (!stylesheet) 16 | return 17 | 18 | for (let i = 0; i < stylesheet.rules.length; ++i) 19 | stylesheet.deleteRule(i) 20 | 21 | // Determine which styles to add 22 | Object.keys(styles) 23 | .forEach(selector => stylesheet.insertRule(`${selector} { ${styles[selector]} }`)) 24 | }) 25 | 26 | return { 27 | rules, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/monaco/plugins/vue/index.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItemKind } from 'vscode-html-languageservice' 2 | import type { HTMLPlugin } from '../types' 3 | import { Events } from './meta' 4 | 5 | export const vueHTMLPlugin: HTMLPlugin = { 6 | completions({ document, position }) { 7 | const text = document.getText({ 8 | start: { line: 0, character: 0 }, 9 | end: position, 10 | }) 11 | 12 | if (text.match(/(<\w+\s*)[^>]*$/) !== null) { 13 | if (!text.match(/\S+(?=\s*=\s*["']?[^"']*$)/) || text.match(/<\w+\s+$/)) { 14 | return [ 15 | { 16 | label: 'v-if', 17 | sortText: '0', 18 | kind: CompletionItemKind.Function, 19 | insertText: 'v-if=""', 20 | }, 21 | ...Events.map(e => ({ 22 | label: `@${e}`, 23 | insertText: `@${e}=""`, 24 | kind: CompletionItemKind.Event, 25 | })), 26 | ] 27 | } 28 | } 29 | 30 | return [] 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /src/monaco/plugins/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { HTMLPluginCompletion } from '../types' 2 | 3 | export function isCursorInHTMLAttribute({ html, document, position }: HTMLPluginCompletion) { 4 | const currentNode = html.findNodeAt(document.offsetAt(position)) 5 | 6 | if (currentNode.start && currentNode.endTagStart) { 7 | const start = document.positionAt(currentNode.start) 8 | const end = document.positionAt(currentNode.endTagStart) 9 | const tag = document.getText({ start, end }) 10 | 11 | if (!currentNode.attributes) 12 | return false 13 | 14 | const attrs = Object.entries(currentNode.attributes).map(([key, value]) => `${key}=${value}`) 15 | const indexes = attrs.map((attr) => { 16 | return [...tag.matchAll(new RegExp(attr, 'gi'))] 17 | .map(x => ({ 18 | start: x.index, 19 | end: x[0].length + (x.index || 0), 20 | })).flat() 21 | }).flat() 22 | 23 | return indexes.some(({ start, end }) => position.character >= start! && position.character <= end!) 24 | } 25 | 26 | return false 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ui/Select.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 40 | -------------------------------------------------------------------------------- /src/components/ui/Container.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 42 | -------------------------------------------------------------------------------- /src/monaco/plugins/windicss/interfaces.ts: -------------------------------------------------------------------------------- 1 | import type { Processor } from 'windicss/lib' 2 | 3 | export type DictStr = { [key: string]: string | string[] } 4 | 5 | export type DeepNestDictStr = { [key: string]: string | DeepNestDictStr } 6 | 7 | export interface Core { 8 | processor?: Processor 9 | utilities: string[] 10 | variants: any 11 | colors: { 12 | label: string 13 | documentation: string 14 | }[] 15 | dynamics: { 16 | label: string 17 | position: number 18 | }[] 19 | } 20 | 21 | export interface Attr { 22 | static: { 23 | [key: string]: string[] 24 | } 25 | color: { 26 | [key: string]: { 27 | label: string 28 | doc: string 29 | }[] 30 | } 31 | bracket: { 32 | [key: string]: string[] 33 | } 34 | dynamic: { 35 | [key: string]: { 36 | label: string 37 | pos: number 38 | }[] 39 | } 40 | } 41 | 42 | export interface Completion { 43 | static: string[] 44 | color: { 45 | label: string 46 | doc: string 47 | }[] 48 | bracket: string[] 49 | dynamic: { 50 | label: string 51 | pos: number 52 | }[] 53 | attr: Attr 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jacob Clevenger 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 | -------------------------------------------------------------------------------- /src/components/settings/InstallSettings.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 36 | -------------------------------------------------------------------------------- /windi.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite-plugin-windicss' 2 | import colors from 'windicss/colors' 3 | import typography from 'windicss/plugin/typography' 4 | 5 | export default defineConfig({ 6 | darkMode: 'class', 7 | plugins: [ 8 | typography(), 9 | require('windicss/plugin/line-clamp'), 10 | ], 11 | attributify: true, 12 | theme: { 13 | extend: { 14 | typography: { 15 | DEFAULT: { 16 | css: { 17 | maxWidth: '65ch', 18 | color: 'inherit', 19 | a: { 20 | 'color': 'inherit', 21 | 'opacity': 0.75, 22 | 'fontWeight': '500', 23 | 'textDecoration': 'underline', 24 | '&:hover': { 25 | opacity: 1, 26 | color: colors.teal[600], 27 | }, 28 | }, 29 | b: { color: 'inherit' }, 30 | strong: { color: 'inherit' }, 31 | em: { color: 'inherit' }, 32 | h1: { color: 'inherit' }, 33 | h2: { color: 'inherit' }, 34 | h3: { color: 'inherit' }, 35 | h4: { color: 'inherit' }, 36 | code: { color: 'inherit' }, 37 | }, 38 | }, 39 | }, 40 | }, 41 | }, 42 | }) -------------------------------------------------------------------------------- /src/components/playground/Tab.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 46 | -------------------------------------------------------------------------------- /src/monaco/languages/html/fillers/vscode-nls.ts: -------------------------------------------------------------------------------- 1 | /* --------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *-------------------------------------------------------------------------------------------- */ 5 | 6 | export interface Options { 7 | locale?: string 8 | cacheLanguageResolution?: boolean 9 | } 10 | export interface LocalizeInfo { 11 | key: string 12 | comment: string[] 13 | } 14 | export interface LocalizeFunc { 15 | (info: LocalizeInfo, message: string, ...args: any[]): string 16 | (key: string, message: string, ...args: any[]): string 17 | } 18 | export interface LoadFunc { 19 | (file?: string): LocalizeFunc 20 | } 21 | 22 | function format(message: string, args: any[]): string { 23 | let result: string 24 | 25 | if (args.length === 0) { 26 | result = message 27 | } 28 | else { 29 | result = message.replace(/\{(\d+)\}/g, (match, rest) => { 30 | const index = rest[0] 31 | return typeof args[index] !== 'undefined' ? args[index] : match 32 | }) 33 | } 34 | return result 35 | } 36 | 37 | function localize(key: string | LocalizeInfo, message: string, ...args: any[]): string { 38 | return format(message, args) 39 | } 40 | 41 | export function loadMessageBundle(file?: string): LocalizeFunc { 42 | return localize 43 | } 44 | 45 | export function config(opt?: Options | string): LoadFunc { 46 | return loadMessageBundle 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground.vueuse.org", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "serve": "vite preview" 8 | }, 9 | "buildOptions": { 10 | "formats": [ 11 | "global" 12 | ], 13 | "env": "development", 14 | "enableNonBrowserBranches": true 15 | }, 16 | "dependencies": { 17 | "@antfu/utils": "^0.1.6", 18 | "@headlessui/vue": "^1.2.0", 19 | "@types/splitpanes": "^2.2.0", 20 | "@vue/runtime-core": "^3.2.37", 21 | "@vue/runtime-dom": "^3.2.37", 22 | "@vue/shared": "^3.2.37", 23 | "@vueuse/core": "10.1.0", 24 | "@vueuse/integrations": "^10.1.0", 25 | "@vueuse/shared": "10.1.0", 26 | "file-saver": "^2.0.5", 27 | "jszip": "^3.6.0", 28 | "lz-string": "^1.4.4", 29 | "monaco-editor": "0.24.0", 30 | "monaco-editor-core": "^0.24.0", 31 | "splitpanes": "^3.0.4", 32 | "theme-vitesse": "^0.1.11", 33 | "vscode-html-languageservice": "^4.0.3", 34 | "vue": "^3.2.37", 35 | "vue-hako": "^0.0.1" 36 | }, 37 | "devDependencies": { 38 | "@antfu/eslint-config": "^0.25.2", 39 | "@iconify/json": "^1.1.349", 40 | "@types/file-saver": "^2.0.2", 41 | "@types/node": "^15.6.1", 42 | "@vitejs/plugin-vue": "^1.2.2", 43 | "@vue/compiler-sfc": "^3.2.37", 44 | "eslint": "^7.27.0", 45 | "typescript": "^4.1.3", 46 | "vite": "^2.3.4", 47 | "vite-plugin-components": "^0.10.2", 48 | "vite-plugin-icons": "^0.5.1", 49 | "vite-plugin-pwa": "^0.7.3", 50 | "vite-plugin-windicss": "^0.16.7", 51 | "vue-tsc": "^0.0.24", 52 | "windicss": "^3.0.12" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/ui/Navigation.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 67 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | VueUse Playground 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/settings/PackageVersion.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 75 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | font-family: 'Roboto', sans-serif;} 6 | 7 | html { 8 | @apply text-dark-100; 9 | } 10 | 11 | html.dark { 12 | @apply text-light-900; 13 | } 14 | 15 | .dark .mtk1 { 16 | color: white !important; 17 | } 18 | 19 | ::-webkit-scrollbar { 20 | @apply bg-light-300; 21 | } 22 | 23 | ::-webkit-scrollbar-thumb { 24 | @apply bg-light-800; 25 | } 26 | 27 | .dark ::-webkit-scrollbar { 28 | @apply bg-dark-900; 29 | } 30 | 31 | .dark ::-webkit-scrollbar-thumb { 32 | @apply bg-dark-800; 33 | } 34 | 35 | ::-webkit-resizer { 36 | padding: 1rem; 37 | } 38 | 39 | .editor-windicss-color-block { 40 | width: 0.8em; 41 | height: 0.8em; 42 | content: ''; 43 | display: inline-block; 44 | margin: 0.1em 0.2em 0; 45 | border-radius: 0.125em; 46 | } 47 | 48 | .editor-windicss-color-block-light { 49 | border: 1px solid #000000; 50 | } 51 | 52 | .editor-windicss-color-block-dark { 53 | border: 1px solid #eeeeee; 54 | } 55 | 56 | .editors-height { 57 | height: calc(100% - 2rem); 58 | } 59 | 60 | .splitpanes.default-theme .splitpanes__pane { 61 | @apply bg-transparent; 62 | } 63 | .splitpanes.default-theme .splitpanes__splitter { 64 | @apply bg-transparent border-transparent min-w-4 min-h-4; 65 | } 66 | 67 | .splitpanes.default-theme .splitpanes__splitter::before, 68 | .splitpanes.default-theme .splitpanes__splitter::after { 69 | @apply bg-dark-100 bg-opacity-50; 70 | } 71 | 72 | .splitpanes.default-theme .splitpanes__splitter:hover::before, 73 | .splitpanes.default-theme .splitpanes__splitter:hover::after { 74 | @apply bg-light-100 bg-opacity-50; 75 | } 76 | 77 | .dark .splitpanes.default-theme .splitpanes__splitter::before, 78 | .dark .splitpanes.default-theme .splitpanes__splitter::after { 79 | @apply bg-dark-100; 80 | } 81 | 82 | .dark .splitpanes.default-theme .splitpanes__splitter:hover::before, 83 | .dark .splitpanes.default-theme .splitpanes__splitter:hover::after { 84 | @apply bg-dark-100; 85 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import WindiCSS from 'vite-plugin-windicss' 5 | import ViteComponents, { HeadlessUiResolver } from 'vite-plugin-components' 6 | import Icons, { ViteIconsResolver } from 'vite-plugin-icons' 7 | import { copyVuePlugin } from './plugins/copy-vue' 8 | 9 | const prefix = 'monaco-editor/esm/vs' 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | build: { 14 | rollupOptions: { 15 | output: { 16 | manualChunks: { 17 | htmlWorker: ['./src/monaco/languages/html/html.worker'], 18 | tsWorker: [`${prefix}/language/typescript/ts.worker`], 19 | editorWorker: [`${prefix}/editor/editor.worker`], 20 | }, 21 | }, 22 | }, 23 | }, 24 | plugins: [ 25 | vue(), 26 | copyVuePlugin(), 27 | WindiCSS({ 28 | scan: { 29 | include: ['src/**/*.{vue,html,jsx,tsx}', 'index.html'], 30 | }, 31 | }), 32 | ViteComponents({ 33 | globalComponentsDeclaration: true, 34 | customComponentResolvers: [ 35 | ViteIconsResolver({ 36 | componentPrefix: '', 37 | }), 38 | HeadlessUiResolver(), 39 | ], 40 | }), 41 | Icons(), 42 | // VitePWA({ 43 | // base: '/', 44 | // }), 45 | ], 46 | resolve: { 47 | alias: { 48 | '~/': `${path.resolve(__dirname, 'src')}/`, 49 | '@vue/compiler-sfc': '@vue/compiler-sfc/dist/compiler-sfc.esm-browser.js', 50 | }, 51 | }, 52 | optimizeDeps: { 53 | exclude: ['consolidate', 'velocityjs', 'dustjs-linkedin', 'atpl', 'liquor', 'twig', 'ejs', 'eco', 'jazz', 'hamljs', 'hamlet', 'jqtpl', 'whiskers', 'haml-coffee', 'hogan.js', 'templayed', 'handlebars', 'underscore', 'lodash', 'walrus', 'mustache', 'just', 'ect', 'mote', 'toffee', 'dot', 'bracket-template', 'ractive', 'htmling', 'babel-core', 'plates', 'react-dom/server', 'react', 'vash', 'slm', 'marko', 'teacup/lib/express', 'coffee-script', 'squirrelly', 'twing'], 54 | }, 55 | }) 56 | -------------------------------------------------------------------------------- /src/components/playground/TabBar.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 67 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by vite-plugin-components 2 | // read more https://github.com/vuejs/vue-next/pull/3399 3 | 4 | declare module 'vue' { 5 | export interface GlobalComponents { 6 | Console: typeof import('./src/components/playground/Console.vue')['default'] 7 | Editor: typeof import('./src/components/playground/Editor.vue')['default'] 8 | Message: typeof import('./src/components/playground/Message.vue')['default'] 9 | Playground: typeof import('./src/components/playground/Playground.vue')['default'] 10 | Preview: typeof import('./src/components/playground/Preview.vue')['default'] 11 | Tab: typeof import('./src/components/playground/Tab.vue')['default'] 12 | TabBar: typeof import('./src/components/playground/TabBar.vue')['default'] 13 | EditorSettings: typeof import('./src/components/settings/EditorSettings.vue')['default'] 14 | InstallSettings: typeof import('./src/components/settings/InstallSettings.vue')['default'] 15 | ManuallyInstallPackage: typeof import('./src/components/settings/ManuallyInstallPackage.vue')['default'] 16 | PackageItem: typeof import('./src/components/settings/PackageItem.vue')['default'] 17 | PackageVersion: typeof import('./src/components/settings/PackageVersion.vue')['default'] 18 | PackagesSettings: typeof import('./src/components/settings/PackagesSettings.vue')['default'] 19 | Settings: typeof import('./src/components/settings/Settings.vue')['default'] 20 | SettingsTab: typeof import('./src/components/settings/SettingsTab.vue')['default'] 21 | WindiCSSSettings: typeof import('./src/components/settings/WindiCSSSettings.vue')['default'] 22 | Button: typeof import('./src/components/ui/Button.vue')['default'] 23 | Container: typeof import('./src/components/ui/Container.vue')['default'] 24 | Navigation: typeof import('./src/components/ui/Navigation.vue')['default'] 25 | Select: typeof import('./src/components/ui/Select.vue')['default'] 26 | SelectItem: typeof import('./src/components/ui/SelectItem.vue')['default'] 27 | Textfield: typeof import('./src/components/ui/Textfield.vue')['default'] 28 | } 29 | } 30 | 31 | export { } 32 | -------------------------------------------------------------------------------- /src/components/settings/ManuallyInstallPackage.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 82 | -------------------------------------------------------------------------------- /src/compiler/windi.ts: -------------------------------------------------------------------------------- 1 | import { Processor } from 'windicss/lib' 2 | import { HTMLParser } from 'windicss/utils/parser' 3 | 4 | export function generateStyles(html: string) { 5 | // Get windi processor 6 | const whitelist = [ 7 | 'dark:text-light-300', 8 | 'text-dark-100', 9 | ].join(' ') 10 | 11 | // Parse html to get array of class matches with location 12 | const parser = new HTMLParser(html) 13 | 14 | const processor = new Processor({ 15 | darkMode: 'class', 16 | }) 17 | 18 | // Parse all classes and put into one line to simplify operations 19 | const htmlClasses = parser 20 | .parseClasses() 21 | .map(i => i.result) 22 | .join(' ') 23 | 24 | // Generate preflight based on the html we input 25 | const preflightSheet = processor.preflight(html) 26 | 27 | // Process the html classes to an interpreted style sheet 28 | const interpretedSheet = processor.interpret(`${htmlClasses} ${whitelist}`).styleSheet 29 | 30 | // Always returns array 31 | const castArray = (val: any) => (Array.isArray(val) ? val : [val]) 32 | 33 | const attrs: { [key: string]: string | string[] } = parser 34 | .parseAttrs() 35 | .reduceRight((acc: { [key: string]: string | string[] }, curr) => { 36 | // get current match key 37 | const attrKey = curr.key 38 | 39 | // ignore class or className attributes 40 | if (attrKey === 'class' || attrKey === 'className') return acc 41 | 42 | // get current match value as array 43 | const attrValue = castArray(curr.value) 44 | 45 | // if current match key is already in accumulator 46 | if (attrKey in acc) { 47 | // get current attr key value as array 48 | const attrKeyValue = castArray(acc[attrKey]) 49 | 50 | // append current value to accumulator value 51 | acc[attrKey] = [...attrKeyValue, ...attrValue] 52 | } 53 | else { 54 | // else add atrribute value array to accumulator 55 | acc[attrKey] = attrValue 56 | } 57 | 58 | return acc 59 | }, {}) 60 | 61 | const attrsSheet = processor.attributify(attrs) 62 | 63 | // Build styles 64 | const APPEND = false 65 | const MINIFY = false 66 | const styles = attrsSheet.styleSheet 67 | .extend(interpretedSheet) 68 | .extend(preflightSheet, APPEND) 69 | .build(MINIFY) 70 | 71 | return styles 72 | } 73 | -------------------------------------------------------------------------------- /src/components/settings/PackageItem.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 66 | -------------------------------------------------------------------------------- /src/monaco/index.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance, onMounted, watch } from 'vue' 2 | import * as monaco from 'monaco-editor' 3 | import { createSingletonPromise } from '@antfu/utils' 4 | /* __imports__ */ 5 | 6 | import vueuseTypes from '@vueuse/core/index.d.ts?raw' 7 | import vueTypes from '@vue/runtime-core/dist/runtime-core.d.ts?raw' 8 | 9 | import { orchestrator } from '~/orchestrator' 10 | 11 | const setup = createSingletonPromise(async() => { 12 | monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ 13 | ...monaco.languages.typescript.javascriptDefaults.getCompilerOptions(), 14 | noUnusedLocals: false, 15 | noUnusedParameters: false, 16 | allowUnreachableCode: true, 17 | allowUnusedLabels: true, 18 | strict: false, 19 | allowJs: true, 20 | }) 21 | 22 | const registered: string[] = ['vue', '@vueuse/core'] 23 | 24 | monaco.languages.typescript.javascriptDefaults.addExtraLib(` 25 | declare module '@vueuse/core' { ${vueuseTypes} } 26 | `, 'ts:vueuse') 27 | 28 | monaco.languages.typescript.javascriptDefaults.addExtraLib(` 29 | declare module 'vue' { ${vueTypes} } 30 | `, 'ts:vue') 31 | 32 | watch(() => orchestrator.packages, () => { 33 | orchestrator.packages.forEach((pack) => { 34 | if (registered.includes(pack.name)) 35 | return 36 | 37 | registered.push(pack.name) 38 | monaco.languages.typescript.javascriptDefaults.addExtraLib(` 39 | declare module '${pack.name}' { 40 | let x: any; 41 | export = x; 42 | } 43 | `, pack.name) 44 | }) 45 | }, { immediate: true }) 46 | 47 | await Promise.all([ 48 | // load workers 49 | (async() => { 50 | const [ 51 | { default: EditorWorker }, 52 | { default: HtmlWorker }, 53 | { default: TsWorker }, 54 | ] = await Promise.all([ 55 | import('monaco-editor/esm/vs/editor/editor.worker?worker'), 56 | import('./languages/html/html.worker?worker'), 57 | import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'), 58 | ]) 59 | 60 | // @ts-expect-error 61 | window.MonacoEnvironment = { 62 | getWorker(_: any, label: string) { 63 | if (label === 'html' || label === 'handlebars' || label === 'razor') 64 | return new HtmlWorker() 65 | if (label === 'typescript' || label === 'javascript') 66 | return new TsWorker() 67 | return new EditorWorker() 68 | }, 69 | } 70 | })(), 71 | ]) 72 | 73 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 74 | const injection_arg = monaco 75 | 76 | /* __async_injections__ */ 77 | 78 | if (getCurrentInstance()) 79 | await new Promise(resolve => onMounted(resolve)) 80 | 81 | return { monaco } 82 | }) 83 | 84 | export default setup 85 | 86 | setup() 87 | -------------------------------------------------------------------------------- /src/logic/useMonaco/index.ts: -------------------------------------------------------------------------------- 1 | import { watch, Ref, unref, ref } from 'vue' 2 | import { until, createEventHook, tryOnUnmounted } from '@vueuse/core' 3 | 4 | import darktheme from 'theme-vitesse/themes/vitesse-dark.json' 5 | import lightTheme from 'theme-vitesse/themes/vitesse-light.json' 6 | import type { editor as Editor } from 'monaco-editor' 7 | import { editorPlugins } from '~/monaco/plugins/editor' 8 | import setupMonaco from '~/monaco' 9 | import { isDark } from '~/logic/dark' 10 | 11 | export function useMonaco(target: Ref, options: any) { 12 | const changeEventHook = createEventHook() 13 | const isSetup = ref(false) 14 | let editor: Editor.IStandaloneCodeEditor 15 | 16 | const setContent = async(content: string) => { 17 | await until(isSetup).toBeTruthy() 18 | if (editor) 19 | editor.setValue(content) 20 | } 21 | 22 | const init = async() => { 23 | const { monaco } = await setupMonaco() 24 | // @ts-expect-error 25 | monaco.editor.defineTheme('vitesse-dark', darktheme) 26 | // @ts-expect-error 27 | monaco.editor.defineTheme('vitesse-light', lightTheme) 28 | 29 | watch(target, () => { 30 | const el = unref(target) 31 | 32 | if (!el) 33 | return 34 | 35 | const extension = () => { 36 | if (options.language === 'typescript') 37 | return 'ts' 38 | else if (options.language === 'javascript') 39 | return 'js' 40 | else if (options.language === 'html') 41 | return 'html' 42 | } 43 | 44 | const model = monaco.editor.createModel(options.code, options.language, monaco.Uri.parse(`file:///root/${Date.now()}.${extension()}`)) 45 | editor = monaco.editor.create(el, { 46 | model, 47 | tabSize: 2, 48 | insertSpaces: true, 49 | autoClosingQuotes: 'always', 50 | detectIndentation: false, 51 | folding: false, 52 | automaticLayout: true, 53 | theme: 'vitesse-dark', 54 | minimap: { 55 | enabled: false, 56 | }, 57 | }) 58 | 59 | isSetup.value = true 60 | 61 | watch(isDark, () => { 62 | if (isDark.value) 63 | monaco.editor.setTheme('vitesse-dark') 64 | else 65 | monaco.editor.setTheme('vitesse-light') 66 | }, { immediate: true }) 67 | 68 | const plugins = editorPlugins.filter(({ language }) => language === options.language) 69 | editor.getModel()?.onDidChangeContent(() => { 70 | changeEventHook.trigger(editor.getValue()) 71 | plugins.forEach(({ onContentChanged }) => onContentChanged(editor)) 72 | }) 73 | }, { 74 | flush: 'post', 75 | immediate: true, 76 | }) 77 | } 78 | 79 | init() 80 | 81 | tryOnUnmounted(() => stop()) 82 | 83 | return { 84 | onChange: changeEventHook.on, 85 | setContent, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/monaco/plugins/windicss/utils/fileTypes.ts: -------------------------------------------------------------------------------- 1 | export function connect(strings: string|string[]) { 2 | return Array.isArray(strings) ? new RegExp(strings.map(i => `(${i})`).join('|')) : new RegExp(strings) 3 | } 4 | 5 | export function allowAttr(type: string): boolean { 6 | // check if file type allow attributes 7 | return type ? ['html', 'js'].includes(type) : true 8 | } 9 | 10 | const classPattern = String.raw`(\s+class(Name)?\s*=\s*{?\s*["'\`])[^"'\`]*$` 11 | const applyPattern = String.raw`@apply\s+[^;]*$` 12 | const windiPattern = String.raw`\Wwindi\`[^\`]*$` 13 | const htmlPattern = [classPattern, applyPattern, windiPattern] 14 | 15 | export const applyRegex = new RegExp(applyPattern) 16 | 17 | export const patterns: {[key: string]: RegExp} = { 18 | html: connect(htmlPattern), 19 | js: connect(htmlPattern), 20 | css: connect(applyPattern), 21 | } 22 | 23 | export const fileTypes: {[key: string]: {pattern?: RegExp; type: string}} = { 24 | css: { 25 | type: 'css', 26 | }, 27 | scss: { 28 | type: 'css', 29 | }, 30 | sass: { 31 | type: 'css', 32 | }, 33 | less: { 34 | type: 'css', 35 | }, 36 | javascript: { 37 | type: 'js', 38 | }, 39 | javascriptreact: { 40 | type: 'js', 41 | }, 42 | typescriptreact: { 43 | type: 'js', 44 | }, 45 | html: { 46 | type: 'html', 47 | }, 48 | php: { 49 | type: 'html', 50 | }, 51 | vue: { 52 | type: 'html', 53 | pattern: /(\s+(v-bind)?:class\s*=\s*["][{[][^"]*$)|(\s+(v-bind)?:class\s*=\s*['][{[][^']*$)/, 54 | }, 55 | svelte: { 56 | type: 'html', 57 | pattern: /(\s+class:\S*$)|((\s+class\s*=\s*["'`]?\s*{\s*)[^}]*$)/, 58 | }, 59 | } 60 | 61 | // if (getConfig('windicss.includeLanguages')) { 62 | // // "windicss.includeLanguages": { 63 | // // "rust": "html", // css // js 64 | // // "abc": { 65 | // // "type": "html" 66 | // // } 67 | // // "def": { 68 | // // "type": "html", 69 | // // "patterns": ["(class(Name)?\\s*=\\s*\\S?\\s*["'\\`])[^\"'\\`]*$", "..."], 70 | // // }> 71 | // // } 72 | // const config = getConfig<{[key: string]: (string | { type?: string; patterns?: string[] })}>('windicss.includeLanguages') 73 | // if (config) { 74 | // Object.entries(config).map(([key, value]) => { 75 | // if (typeof value === 'string') { 76 | // fileTypes[key] = { type: value in patterns ? value : 'css' } 77 | // } 78 | // else { 79 | // const pattern = (value.patterns === undefined || value.patterns.length === 0) ? undefined : value.patterns 80 | // if (key in fileTypes) 81 | // fileTypes[key] = { type: value.type || fileTypes[key].type, pattern: fileTypes[key].pattern ? pattern ? connect([(fileTypes[key].pattern as RegExp).source, ...pattern]) : fileTypes[key].pattern : undefined } 82 | // } 83 | // }) 84 | // } 85 | // } 86 | -------------------------------------------------------------------------------- /src/monaco/languages/html/workerManager.ts: -------------------------------------------------------------------------------- 1 | /* --------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *-------------------------------------------------------------------------------------------- */ 5 | 6 | import { LanguageServiceDefaults } from './monaco.contribution' 7 | import type { HTMLWorker } from './htmlWorker' 8 | import { Uri, IDisposable, editor } from './fillers/monaco-editor-core' 9 | 10 | const STOP_WHEN_IDLE_FOR = 2 * 60 * 1000 // 2min 11 | 12 | export class WorkerManager { 13 | private _defaults: LanguageServiceDefaults 14 | private _idleCheckInterval: number 15 | private _lastUsedTime: number 16 | private _configChangeListener: IDisposable 17 | 18 | private _worker: editor.MonacoWebWorker 19 | private _client: Promise 20 | 21 | constructor(defaults: LanguageServiceDefaults) { 22 | this._defaults = defaults 23 | this._worker = null 24 | this._idleCheckInterval = setInterval(() => this._checkIfIdle(), 30 * 1000) 25 | this._lastUsedTime = 0 26 | this._configChangeListener = this._defaults.onDidChange(() => this._stopWorker()) 27 | } 28 | 29 | private _stopWorker(): void { 30 | if (this._worker) { 31 | this._worker.dispose() 32 | this._worker = null 33 | } 34 | this._client = null 35 | } 36 | 37 | dispose(): void { 38 | clearInterval(this._idleCheckInterval) 39 | this._configChangeListener.dispose() 40 | this._stopWorker() 41 | } 42 | 43 | private _checkIfIdle(): void { 44 | if (!this._worker) 45 | return 46 | 47 | const timePassedSinceLastUsed = Date.now() - this._lastUsedTime 48 | if (timePassedSinceLastUsed > STOP_WHEN_IDLE_FOR) 49 | this._stopWorker() 50 | } 51 | 52 | private _getClient(): Promise { 53 | this._lastUsedTime = Date.now() 54 | 55 | if (!this._client) { 56 | this._worker = editor.createWebWorker({ 57 | // module that exports the create() method and returns a `HTMLWorker` instance 58 | moduleId: 'vs/language/html/htmlWorker', 59 | 60 | // passed in to the create() method 61 | createData: { 62 | languageSettings: this._defaults.options, 63 | languageId: this._defaults.languageId, 64 | }, 65 | 66 | label: this._defaults.languageId, 67 | }) 68 | 69 | this._client = > this._worker.getProxy() 70 | } 71 | 72 | return this._client 73 | } 74 | 75 | getLanguageServiceWorker(...resources: Uri[]): Promise { 76 | let _client: HTMLWorker 77 | return this._getClient() 78 | .then((client) => { 79 | _client = client 80 | }) 81 | .then((_) => { 82 | return this._worker.withSyncedResources(resources) 83 | }) 84 | .then(_ => _client) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/logic/PreviewProxy.ts: -------------------------------------------------------------------------------- 1 | // ReplProxy and srcdoc implementation from Svelte REPL 2 | // MIT License https://github.com/sveltejs/svelte-repl/blob/master/LICENSE 3 | 4 | let uid = 1 5 | 6 | export class PreviewProxy { 7 | iframe: HTMLIFrameElement 8 | handlers: Record 9 | pending_cmds: Map< 10 | number, 11 | { resolve: (value: unknown) => void; reject: (reason?: any) => void } 12 | > 13 | handle_event: (e: any) => void 14 | 15 | constructor(iframe: HTMLIFrameElement, handlers: Record) { 16 | this.iframe = iframe 17 | this.handlers = handlers 18 | 19 | this.pending_cmds = new Map() 20 | 21 | this.handle_event = e => this.handle_repl_message(e) 22 | window.addEventListener('message', this.handle_event, false) 23 | } 24 | 25 | destroy() { 26 | window.removeEventListener('message', this.handle_event) 27 | } 28 | 29 | iframe_command(action: string, args: any) { 30 | return new Promise((resolve, reject) => { 31 | const cmd_id = uid++ 32 | 33 | this.pending_cmds.set(cmd_id, { resolve, reject }) 34 | 35 | this.iframe.contentWindow!.postMessage({ action, cmd_id, args }, '*') 36 | }) 37 | } 38 | 39 | handle_command_message(cmd_data: any) { 40 | let action = cmd_data.action 41 | let id = cmd_data.cmd_id 42 | let handler = this.pending_cmds.get(id) 43 | 44 | if (handler) { 45 | this.pending_cmds.delete(id) 46 | if (action === 'cmd_error') { 47 | let { message, stack } = cmd_data 48 | let e = new Error(message) 49 | e.stack = stack 50 | handler.reject(e) 51 | } 52 | 53 | if (action === 'cmd_ok') { 54 | handler.resolve(cmd_data.args) 55 | } 56 | } else { 57 | console.error('command not found', id, cmd_data, [ 58 | ...this.pending_cmds.keys() 59 | ]) 60 | } 61 | } 62 | 63 | handle_repl_message(event: any) { 64 | if (event.source !== this.iframe.contentWindow) return 65 | 66 | const { action, args } = event.data 67 | 68 | switch (action) { 69 | case 'cmd_error': 70 | case 'cmd_ok': 71 | return this.handle_command_message(event.data) 72 | case 'fetch_progress': 73 | return this.handlers.on_fetch_progress(args.remaining) 74 | case 'error': 75 | return this.handlers.on_error(event.data) 76 | case 'unhandledrejection': 77 | return this.handlers.on_unhandled_rejection(event.data) 78 | case 'console': 79 | return this.handlers.on_console(event.data) 80 | case 'console_group': 81 | return this.handlers.on_console_group(event.data) 82 | case 'console_group_collapsed': 83 | return this.handlers.on_console_group_collapsed(event.data) 84 | case 'console_group_end': 85 | return this.handlers.on_console_group_end(event.data) 86 | } 87 | } 88 | 89 | eval(script: string | string[]) { 90 | return this.iframe_command('eval', { script }) 91 | } 92 | 93 | handle_links() { 94 | return this.iframe_command('catch_clicks', {}) 95 | } 96 | } -------------------------------------------------------------------------------- /src/monaco/plugins/vue/meta.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItemKind, CompletionItem } from 'vscode-html-languageservice' 2 | 3 | export const EventModifiers = { 4 | stop: 'call event.stopPropagation().', 5 | prevent: 'call event.preventDefault().', 6 | capture: 'add event listener in capture mode.', 7 | self: 'only trigger handler if event was dispatched from this element.', 8 | // {keyAlias}: 'only trigger handler on certain keys.', 9 | once: 'trigger handler at most once.', 10 | left: 'only trigger handler for left button mouse events.', 11 | right: 'only trigger handler for right button mouse events.', 12 | middle: 'only trigger handler for middle button mouse events.', 13 | passive: 'attaches a DOM event with { passive: true }.', 14 | } 15 | 16 | export const CustomTags = [ 17 | 'slot', 18 | ] 19 | 20 | export const Directives: CompletionItem[] = [ 21 | { 22 | label: 'v-if', 23 | insertText: 'v-if=""', 24 | kind: CompletionItemKind.Function, 25 | }, 26 | { 27 | label: 'v-else-if', 28 | insertText: 'v-else-if=""', 29 | kind: CompletionItemKind.Function, 30 | }, 31 | { 32 | label: 'v-else', 33 | kind: CompletionItemKind.Function, 34 | }, 35 | ] 36 | 37 | export const Events: string[] = [ 38 | 'abort', 39 | 'animationend', 40 | 'aimationiteration', 41 | 'animationstart', 42 | 'auxclick', 43 | 'beforeinput', 44 | 'blur', 45 | 'canplay', 46 | 'canplaythrough', 47 | 'change', 48 | 'click', 49 | 'compositionend', 50 | 'compositionstart', 51 | 'compositionupdate', 52 | 'contextmenu', 53 | 'copy', 54 | 'cut', 55 | 'dblclick', 56 | 'drag', 57 | 'dragend', 58 | 'dragenter', 59 | 'dragexit', 60 | 'dragleave', 61 | 'dragover', 62 | 'dragstart', 63 | 'drop', 64 | 'durationchange', 65 | 'emptied', 66 | 'encrypted', 67 | 'ended', 68 | 'error', 69 | 'focus', 70 | 'focusin', 71 | 'focusout', 72 | 'input', 73 | 'invalid', 74 | 'keydown', 75 | 'keypress', 76 | 'keyup', 77 | 'load', 78 | 'loaddata', 79 | 'loadedmetadata', 80 | 'loadstart', 81 | 'mousedown', 82 | 'mouseenter', 83 | 'mouseleave', 84 | 'mousemove', 85 | 'mouseout', 86 | 'mouseover', 87 | 'mouseup', 88 | 'paste', 89 | 'pause', 90 | 'play', 91 | 'playing', 92 | 'pointercancel', 93 | 'pointerdown', 94 | 'pointerenter', 95 | 'pointerleave', 96 | 'pointermove', 97 | 'pointerout', 98 | 'pointerover', 99 | 'pointerup', 100 | 'progress', 101 | 'ratechange', 102 | 'reset', 103 | 'scroll', 104 | 'seeked', 105 | 'seeking', 106 | 'select', 107 | 'stalled', 108 | 'submit', 109 | 'suspend', 110 | 'timeupdate', 111 | 'touchcancel', 112 | 'touchend', 113 | 'touchmove', 114 | 'touchstart', 115 | 'transitionend', 116 | 'transitionstart', 117 | 'vnode-before-mount', 118 | 'vnode-before-unmount', 119 | 'vnode-before-update', 120 | 'vnode-mounted', 121 | 'vnode-unmounted', 122 | 'vnode-updated', 123 | 'volumechange', 124 | 'waiting', 125 | 'wheel', 126 | ] 127 | -------------------------------------------------------------------------------- /src/monaco/plugins/windicss/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { toRGBA } from 'windicss/utils' 2 | 3 | export function flatColors(colors: any, head?: string) { 4 | let flatten: { [ key: string ]: string } = {} 5 | for (const [key, value] of Object.entries(colors)) { 6 | if (typeof value === 'string') 7 | flatten[(head && key === 'DEFAULT') ? head : head ? `${head}-${key}` : key] = value 8 | else if (typeof value === 'function') 9 | flatten[(head && key === 'DEFAULT') ? head : head ? `${head}-${key}` : key] = 'currentColor' 10 | else 11 | flatten = { ...flatten, ...flatColors(value, head ? `${head}-${key}` : key) } 12 | } 13 | return flatten 14 | } 15 | 16 | export function isAttrVariant(word: string, variants: any): boolean { 17 | const lastKey = word.match(/[^:-]+$/)?.[0] || word 18 | return lastKey in variants 19 | } 20 | 21 | export function isAttrUtility(word: string, attrs: any): string | undefined { 22 | if (!word) 23 | return 24 | 25 | const lastKey = word.match(/[^:-]+$/)?.[0] || word 26 | return lastKey in attrs ? lastKey : undefined 27 | } 28 | 29 | export function hex2RGB(hex: string): number[] | undefined { 30 | const RGB_HEX = /^#?(?:([\da-f]{3})[\da-f]?|([\da-f]{6})(?:[\da-f]{2})?)$/i 31 | const [, short, long] = String(hex).match(RGB_HEX) || [] 32 | 33 | if (long) { 34 | const value = Number.parseInt(long, 16) 35 | return [value >> 16, (value >> 8) & 0xFF, value & 0xFF] 36 | } 37 | else if (short) { 38 | return Array.from(short, s => Number.parseInt(s, 16)).map( 39 | n => (n << 4) | n, 40 | ) 41 | } 42 | } 43 | 44 | export function rgb2Hex(r: number, g: number, b: number) { 45 | return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}` 46 | } 47 | 48 | export function isValidColor(utility: string, colors: any, type = 'hex') { 49 | const sep = utility.search('/') 50 | if (sep !== -1) utility = utility.slice(0, sep) 51 | if (/hex-?(?:([\da-f]{3})[\da-f]?|([\da-f]{6})(?:[\da-f]{2})?)$/.test(utility)) { 52 | const hex = utility.replace(/^\S*hex-/, '') 53 | return { color: (type === 'hex' ? hex2RGB : toRGBA)(`#${hex}`), key: `hex-${hex}` } 54 | } 55 | for (const [key, value] of Object.entries(colors)) { 56 | if (utility.endsWith(key)) 57 | return { color: (type === 'hex' ? hex2RGB : toRGBA)(Array.isArray(value) ? value[0] : value), key } 58 | } 59 | return {} 60 | } 61 | 62 | function match(a: [r: number, g: number, b: number], b: [r: number, g: number, b: number]) { 63 | return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] 64 | } 65 | 66 | export function isDarkColor(r: number, g: number, b: number) { 67 | if (match([r, g, b], [20, 184, 166]) || match([r, g, b], [245, 158, 11]) || match([r, g, b], [249, 115, 22]) || match([r, g, b], [217, 70, 239]) || match([r, g, b], [6, 182, 212]) || match([r, g, b], [132, 204, 22])) return true 68 | // special cases: orange-500 yellow-500 teal-500 fuchsia-500 cyan-500 lime-500, With 500 as the dividing line, the view is better 69 | 70 | const hsp = Math.sqrt( 71 | 0.299 * (r * r) 72 | + 0.587 * (g * g) 73 | + 0.114 * (b * b), 74 | ) 75 | 76 | return hsp <= 150 77 | } 78 | -------------------------------------------------------------------------------- /src/components/settings/Settings.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 92 | -------------------------------------------------------------------------------- /src/monaco/plugins/windicss/utils/parser.ts: -------------------------------------------------------------------------------- 1 | export type Attr = { raw: string; key: string; value: { raw: string; start: number }; start: number; end: number } 2 | export type ClassName = { result: string; start: number; end: number } 3 | 4 | export class HTMLParser { 5 | html?: string 6 | 7 | constructor(html?: string) { 8 | this.html = html 9 | } 10 | 11 | removeComments() { 12 | if (!this.html) return [] 13 | const regex = /\/\*[\s\S]*?\*\/|\/\*[\s\S]*$|([^\\:]|^)\/\/.*|$/igm 14 | let match 15 | while ((match = regex.exec(this.html as string))) { 16 | if (match) 17 | this.html = (this.html as string).slice(0, match.index) + ' '.repeat(regex.lastIndex - match.index) + this.html.slice(regex.lastIndex) 18 | } 19 | } 20 | 21 | parseAttrs(): Attr[] { 22 | if (!this.html) return [] 23 | const output: Attr[] = [] 24 | const regex = /\S+\s*=\s*"[^"]+"|\S+\s*=\s*'[^']+'|\S+\s*=\s*[^>\s]+/igm 25 | let match 26 | while ((match = regex.exec(this.html as string))) { 27 | if (match) { 28 | const raw = match[0] 29 | const sep = raw.indexOf('=') 30 | const key = raw.slice(0, sep).trim() 31 | let value: string| string[] = raw.slice(sep + 1).trim() 32 | let vstart = match.index + (sep + 1 + (raw.slice(sep + 1).match(/\S/)?.index || 0)) 33 | if (['"', '\''].includes(value.charAt(0))) { 34 | vstart++ 35 | value = value.slice(1, -1) 36 | } 37 | 38 | output.push({ 39 | raw, 40 | key, 41 | value: { 42 | raw: value, 43 | start: vstart, 44 | }, 45 | start: match.index, 46 | end: regex.lastIndex, 47 | }) 48 | } 49 | } 50 | return output 51 | } 52 | 53 | parseClasses(): ClassName[] { 54 | // Match all class properties 55 | if (!this.html) return [] 56 | const classRegex = /\s(?:class|className|w:dark|w:light|w:active|w:after|w:before|w:checked|w:disabled|w:focus|w:hover|w:tw)=(["'])([^{}]+)\1/ 57 | const quoteRegex = /["']/ 58 | const classNames = [] 59 | let _indexStart = 0 60 | let _htmlLeft = this.html 61 | let propStart = _htmlLeft.search(classRegex) 62 | while (propStart !== -1) { 63 | const afterMatch = _htmlLeft.substring(propStart) 64 | const relativeStart = afterMatch.search(quoteRegex) 65 | const relativeEnd = afterMatch 66 | .substring(relativeStart + 1) 67 | .search(quoteRegex) 68 | const absoluteStart = propStart + relativeStart + 1 69 | const absoluteEnd = absoluteStart + relativeEnd 70 | classNames.push({ 71 | start: _indexStart + absoluteStart, 72 | end: _indexStart + absoluteEnd, 73 | result: _htmlLeft.substring(absoluteStart, absoluteEnd), 74 | }) 75 | _htmlLeft = _htmlLeft.substring(absoluteEnd + 2) 76 | _indexStart += absoluteEnd + 2 77 | propStart = _htmlLeft.search(classRegex) 78 | } 79 | return classNames 80 | } 81 | 82 | parseApplies(): ClassName[] { 83 | if (!this.html) return [] 84 | const output: ClassName[] = [] 85 | const regex = /(?<=@apply\s+)[^;]+(?=\s+!important)|(?<=@apply\s+)[^;]+(?=;)/igm 86 | let match 87 | while ((match = regex.exec(this.html as string))) { 88 | if (match) { 89 | output.push({ 90 | result: this.html.slice(match.index, regex.lastIndex), 91 | start: match.index, 92 | end: regex.lastIndex, 93 | }) 94 | } 95 | } 96 | return output 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/components/playground/Playground.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 106 | -------------------------------------------------------------------------------- /src/monaco/plugins/windicss/decorations.ts: -------------------------------------------------------------------------------- 1 | import { Processor } from 'windicss/lib' 2 | import { ClassParser } from 'windicss/utils/parser' 3 | import type { editor as Editor } from 'monaco-editor' 4 | import * as monaco from 'monaco-editor' 5 | import { hex2RGB } from 'windicss/utils' 6 | import { isString } from '@antfu/utils' 7 | import { flatColors, isAttrUtility, isAttrVariant, isValidColor, isDarkColor, rgb2Hex } from './utils' 8 | import { generateCompletions } from './utils/completions' 9 | import { HTMLParser } from './utils/parser' 10 | import { EditorPlugin } from '~/monaco/plugins/types' 11 | import { useStyleSheet } from '~/logic/useStyleSheet' 12 | 13 | const processor = new Processor() 14 | const windi = { 15 | colors: flatColors(processor.theme('colors', {})), 16 | variants: processor.resolveVariants(), 17 | completions: generateCompletions(processor, null, true), 18 | } 19 | 20 | interface State { 21 | decorations: string[] 22 | } 23 | 24 | interface Provider { 25 | color: string 26 | decoration: Editor.IModelDecoration[] 27 | } 28 | 29 | const { rules } = useStyleSheet('__windicss_styles__') 30 | const state: State = { 31 | decorations: [], 32 | } 33 | 34 | function createColorCube(document: Editor.ITextModel, start: number, offset: number, raw: string, color: string | number[]): Editor.IModelDecoration { 35 | color = isString(color) ? color : rgb2Hex(color[0], color[1], color[2]) 36 | 37 | const { lineNumber: startLine, column: startColumn } = document.getPositionAt(start + offset) 38 | const { lineNumber: endLine, column: endColumn } = document.getPositionAt(start + offset + raw.length) 39 | const [r, g, b] = hex2RGB(color)! 40 | 41 | return { 42 | ownerId: 0, 43 | id: raw, 44 | range: new monaco.Range(startLine, startColumn, endLine, endColumn), 45 | options: { 46 | beforeContentClassName: `editor-windicss-color-block ${isDarkColor(r, g, b) ? 'editor-windicss-color-block-dark' : 'editor-windicss-color-block-light'} color-${color.replace('#', '')})}`, 47 | }, 48 | } 49 | } 50 | 51 | export const WindiDecoration: EditorPlugin = { 52 | language: 'html', 53 | onContentChanged(editor) { 54 | const documentText = editor.getValue() 55 | const parser = new HTMLParser(documentText) 56 | parser.removeComments() 57 | const attrs = parser.parseAttrs() 58 | const model = editor.getModel()! 59 | 60 | // Map Attribute Colors 61 | // @ts-ignore 62 | const attributeProviders: Provider[] = attrs 63 | .filter(attr => isAttrUtility(attr.key, windi.completions.attr.static)) 64 | .map((attr) => { 65 | const matches = attr.value.raw.matchAll(/[^\s/]+/igm) 66 | 67 | return Array.from(matches) 68 | .map((match) => { 69 | const value = match[0].replace('dark:', '').replace('!', '') 70 | 71 | if (value in windi.colors || value.startsWith('hex-')) { 72 | const color = value in windi.colors ? windi.colors[value] : value.replace(/^hex-/, '#') 73 | 74 | return { 75 | color, 76 | decoration: createColorCube(model, attr.value.start, match.index!, match[0], color), 77 | } 78 | } 79 | 80 | return null 81 | }).filter(_ => _) 82 | }) 83 | .flat() 84 | 85 | // @ts-ignore 86 | const classProviders: Provider[] = attrs 87 | .filter(attr => isAttrVariant(attr.key, windi.variants) || ['class', 'className'].includes(attr.key)) 88 | .map((attr) => { 89 | const elements = new ClassParser(attr.value.raw, ':', Object.keys(windi.variants)).parse(false) 90 | const els = [] 91 | 92 | for (const element of elements) { 93 | if (element.type === 'group' && Array.isArray(element.content)) { 94 | for (const e of element.content) { 95 | const color = isValidColor(e.raw, windi.colors) 96 | 97 | if (color && color.color) { 98 | els.push({ 99 | color: color.color, 100 | decoration: createColorCube(model, attr.value.start, e.start, element.raw, color.color), 101 | }) 102 | } 103 | } 104 | } 105 | 106 | const color = element.type === 'utility' && isValidColor(element.raw, windi.colors) 107 | 108 | if (color && color.color) { 109 | els.push({ 110 | color: color.color, 111 | decoration: createColorCube(model, attr.value.start, element.start, element.raw, color.color), 112 | }) 113 | } 114 | } 115 | 116 | return els 117 | }) 118 | .flat() 119 | 120 | rules.value = [...attributeProviders, ...classProviders] 121 | .filter(({ color }) => color) 122 | .reduce((acc, color) => { 123 | const _color = isString(color.color) ? color.color : rgb2Hex(color.color[0], color.color[1], color.color[2]) 124 | // @ts-ignore 125 | acc[`.color-${_color.replace('#', '')}`] = `background-color: ${_color}` 126 | return acc 127 | }, {}) 128 | 129 | // @ts-ignore 130 | state.decorations = editor.deltaDecorations(state.decorations, [ 131 | ...attributeProviders.map(({ decoration }) => decoration), 132 | ...classProviders.map(({ decoration }) => decoration), 133 | ]) 134 | }, 135 | } 136 | -------------------------------------------------------------------------------- /src/monaco/plugins/windicss/index.ts: -------------------------------------------------------------------------------- 1 | import { Processor } from 'windicss/lib' 2 | import { CompletionItemKind, CompletionItem, TextDocument, Position } from 'vscode-html-languageservice' 3 | import type { HTMLPlugin } from '../types' 4 | import { generateCompletions } from './utils/completions' 5 | import { patterns } from './utils/fileTypes' 6 | import { isAttrVariant, isAttrUtility } from './utils' 7 | 8 | const pattern = patterns.html 9 | 10 | const processor = new Processor() 11 | const variants = processor.resolveVariants() 12 | const _completions = generateCompletions(processor, null, true) 13 | 14 | function attrKey(label: string, kind: CompletionItemKind, order: number): CompletionItem { 15 | return { 16 | label, 17 | kind, 18 | sortText: `${order}-${label}`, 19 | insertText: `${label}=""`, 20 | } 21 | } 22 | 23 | const resolveVariants = (document: TextDocument, position: Position) => { 24 | const text = document.getText({ 25 | start: { line: 0, character: 0 }, 26 | end: position, 27 | }) 28 | 29 | if ((!pattern || text.match(pattern) === null) && text.match(patterns.html) === null) { 30 | const key = text.match(/\S+(?=\s*=\s*["']?[^"']*$)/)?.[0] 31 | if (!key || !(isAttrVariant(key, variants))) 32 | return [] 33 | } 34 | 35 | return [ 36 | ..._completions.static.map((classItem, index) => ({ 37 | label: classItem, 38 | kind: CompletionItemKind.Constant, 39 | sortText: `1-${index.toString().padStart(8, '0')}`, 40 | })), 41 | 42 | ...Object.keys(variants).map((variant, index) => ({ 43 | label: variant, 44 | kind: CompletionItemKind.Module, 45 | detail: variant, 46 | sortText: `2-${index.toString().padStart(8, '0')}`, 47 | })), 48 | 49 | ..._completions.color.map(({ label, doc }, index) => ({ 50 | label, 51 | kind: CompletionItemKind.Color, 52 | sortText: `0-${index.toString().padStart(8, '0')}`, 53 | documentation: doc, 54 | })), 55 | 56 | ..._completions.bracket.map((label, index) => ({ 57 | label, 58 | kind: CompletionItemKind.Struct, 59 | sortText: `3-${index.toString().padStart(8, '0')}`, 60 | })), 61 | 62 | ..._completions.dynamic.map(({ label }, index) => ({ 63 | label, 64 | kind: CompletionItemKind.Variable, 65 | sortText: `4-${index.toString().padStart(8, '0')}`, 66 | })), 67 | ] 68 | } 69 | 70 | const resolveAttrKeys = (document: TextDocument, position: Position) => { 71 | const text = document.getText({ 72 | start: { line: 0, character: 0 }, 73 | end: position, 74 | }) 75 | 76 | if (text.match(/(<\w+\s*)[^>]*$/) !== null) { 77 | if (!text.match(/\S+(?=\s*=\s*["']?[^"']*$)/) || text.match(/<\w+\s+$/)) { 78 | return [ 79 | ...Object.keys(_completions.attr.static) 80 | .map(label => attrKey(label, CompletionItemKind.Field, 0)), 81 | 82 | ...Object.keys(variants) 83 | .map(label => attrKey(label, CompletionItemKind.Module, 1)), 84 | ] 85 | } 86 | } 87 | 88 | return [] 89 | } 90 | 91 | const resolveAttrValues = (document: TextDocument, position: Position) => { 92 | const text = document.getText({ 93 | start: { line: 0, character: 0 }, 94 | end: position, 95 | }) 96 | 97 | if (text.match(/(<\w+\s*)[^>]*$/) !== null) { 98 | const key = isAttrUtility(text.match(/\S+(?=\s*=\s*["']?[^"']*$)/)?.[0] as string, _completions.attr.static) 99 | 100 | if (!key) 101 | return [] 102 | 103 | const completions: CompletionItem[] = [] 104 | 105 | completions 106 | .push(..._completions.attr.static[key] 107 | .map((label, index) => ({ 108 | label, 109 | kind: CompletionItemKind.Constant, 110 | detail: key, 111 | sortText: `1-${index.toString().padStart(8, '0')}`, 112 | }))) 113 | 114 | if (key in _completions.attr.color) { 115 | completions 116 | .push(..._completions.attr.color[key] 117 | .map(({ label, doc }, index) => ({ 118 | label, 119 | kind: CompletionItemKind.Color, 120 | detail: key, 121 | sortText: `0-${index.toString().padStart(8, '0')}`, 122 | documentation: doc, 123 | }))) 124 | } 125 | 126 | completions.push(...Object.keys(variants).map((variant, index) => ({ 127 | label: `${variant}:`, 128 | kind: CompletionItemKind.Module, 129 | detail: `${key},${variant}`, 130 | sortText: `2-${index.toString().padStart(8, '0')}`, 131 | }))) 132 | 133 | if (key in _completions.attr.bracket) { 134 | completions.push(..._completions.attr.bracket[key].map((label, index) => ({ 135 | label, 136 | kind: CompletionItemKind.Struct, 137 | detail: key, 138 | sortText: `3-${index.toString().padStart(8, '0')}`, 139 | }))) 140 | } 141 | 142 | if (key in _completions.attr.dynamic) { 143 | completions.push(..._completions.attr.dynamic[key].map(({ label }, index) => ({ 144 | label, 145 | kind: CompletionItemKind.Variable, 146 | detail: key, 147 | sortText: `4-${index.toString().padStart(8, '0')}`, 148 | }))) 149 | } 150 | 151 | return completions 152 | } 153 | 154 | return [] 155 | } 156 | 157 | export const windicssHTMLPlugin: HTMLPlugin = { 158 | completions({ document, position }) { 159 | return [ 160 | ...resolveVariants(document, position), 161 | ...resolveAttrKeys(document, position), 162 | ...resolveAttrValues(document, position), 163 | ] 164 | }, 165 | } 166 | -------------------------------------------------------------------------------- /src/monaco/languages/html/htmlWorker.ts: -------------------------------------------------------------------------------- 1 | /* --------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *-------------------------------------------------------------------------------------------- */ 5 | 6 | import * as htmlService from 'vscode-html-languageservice' 7 | import { worker } from './fillers/monaco-editor-core' 8 | import type { Options } from './monaco.contribution' 9 | import { htmlCompletionPlugins } from '../../plugins/index' 10 | 11 | export interface ICreateData { 12 | languageId: string 13 | languageSettings: Options 14 | } 15 | 16 | export class HTMLWorker { 17 | private _ctx: worker.IWorkerContext 18 | private _languageService: htmlService.LanguageService 19 | private _languageSettings: Options 20 | private _languageId: string 21 | 22 | constructor(ctx: worker.IWorkerContext, createData: ICreateData) { 23 | this._ctx = ctx 24 | this._languageSettings = createData.languageSettings 25 | this._languageId = createData.languageId 26 | this._languageService = htmlService.getLanguageService() 27 | } 28 | 29 | async doValidation(): Promise { 30 | // not yet suported 31 | return Promise.resolve([]) 32 | } 33 | 34 | async doComplete( 35 | uri: string, 36 | position: htmlService.Position, 37 | ): Promise { 38 | const document = this._getTextDocument(uri) 39 | const htmlDocument = this._languageService.parseHTMLDocument(document) 40 | const items = htmlCompletionPlugins.map(plugin => plugin.completions({ document, html: htmlDocument, position })).flat() 41 | const completions = this._languageService.doComplete( 42 | document, 43 | position, 44 | htmlDocument, 45 | this._languageSettings && this._languageSettings.suggest, 46 | ) 47 | 48 | return Promise.resolve({ 49 | isIncomplete: true, 50 | items: [ 51 | ...completions.items, 52 | ...items, 53 | ], 54 | }) 55 | } 56 | 57 | async format( 58 | uri: string, 59 | range: htmlService.Range, 60 | options: htmlService.FormattingOptions, 61 | ): Promise { 62 | const document = this._getTextDocument(uri) 63 | const formattingOptions = { ...this._languageSettings.format, ...options } 64 | const textEdits = this._languageService.format(document, range, formattingOptions) 65 | return Promise.resolve(textEdits) 66 | } 67 | 68 | async doHover(uri: string, position: htmlService.Position): Promise { 69 | const document = this._getTextDocument(uri) 70 | const htmlDocument = this._languageService.parseHTMLDocument(document) 71 | const hover = this._languageService.doHover(document, position, htmlDocument) 72 | return Promise.resolve(hover!) 73 | } 74 | 75 | async findDocumentHighlights( 76 | uri: string, 77 | position: htmlService.Position, 78 | ): Promise { 79 | const document = this._getTextDocument(uri) 80 | const htmlDocument = this._languageService.parseHTMLDocument(document) 81 | const highlights = this._languageService.findDocumentHighlights(document, position, htmlDocument) 82 | return Promise.resolve(highlights) 83 | } 84 | 85 | async findDocumentLinks(uri: string): Promise { 86 | const document = this._getTextDocument(uri) 87 | const links = this._languageService.findDocumentLinks(document, null!) 88 | return Promise.resolve(links) 89 | } 90 | 91 | async findDocumentSymbols(uri: string): Promise { 92 | const document = this._getTextDocument(uri) 93 | const htmlDocument = this._languageService.parseHTMLDocument(document) 94 | const symbols = this._languageService.findDocumentSymbols(document, htmlDocument) 95 | return Promise.resolve(symbols) 96 | } 97 | 98 | async getFoldingRanges( 99 | uri: string, 100 | context?: { rangeLimit?: number }, 101 | ): Promise { 102 | const document = this._getTextDocument(uri) 103 | const ranges = this._languageService.getFoldingRanges(document, context) 104 | return Promise.resolve(ranges) 105 | } 106 | 107 | async getSelectionRanges( 108 | uri: string, 109 | positions: htmlService.Position[], 110 | ): Promise { 111 | const document = this._getTextDocument(uri) 112 | const ranges = this._languageService.getSelectionRanges(document, positions) 113 | return Promise.resolve(ranges) 114 | } 115 | 116 | async doRename( 117 | uri: string, 118 | position: htmlService.Position, 119 | newName: string, 120 | ): Promise { 121 | const document = this._getTextDocument(uri) 122 | const htmlDocument = this._languageService.parseHTMLDocument(document) 123 | const renames = this._languageService.doRename(document, position, newName, htmlDocument) 124 | return Promise.resolve(renames!) 125 | } 126 | 127 | private _getTextDocument(uri: string): htmlService.TextDocument { 128 | const models = this._ctx.getMirrorModels() 129 | for (const model of models) { 130 | if (model.uri.toString() === uri) { 131 | return htmlService.TextDocument.create( 132 | uri, 133 | this._languageId, 134 | model.version, 135 | model.getValue(), 136 | ) 137 | } 138 | } 139 | return null! 140 | } 141 | } 142 | 143 | export function create(ctx: worker.IWorkerContext, createData: ICreateData): HTMLWorker { 144 | return new HTMLWorker(ctx, createData) 145 | } 146 | -------------------------------------------------------------------------------- /src/monaco/languages/html/htmlMode.ts: -------------------------------------------------------------------------------- 1 | /* --------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *-------------------------------------------------------------------------------------------- */ 5 | 6 | import { WorkerManager } from './workerManager' 7 | import type { HTMLWorker } from './htmlWorker' 8 | import { LanguageServiceDefaults } from './monaco.contribution' 9 | import * as languageFeatures from './languageFeatures' 10 | import { Uri, IDisposable, languages } from './fillers/monaco-editor-core' 11 | 12 | export function setupMode1(defaults: LanguageServiceDefaults): void { 13 | const client = new WorkerManager(defaults) 14 | 15 | const worker: languageFeatures.WorkerAccessor = (...uris: Uri[]): Promise => { 16 | return client.getLanguageServiceWorker(...uris) 17 | } 18 | 19 | const languageId = defaults.languageId 20 | 21 | // all modes 22 | languages.registerCompletionItemProvider( 23 | languageId, 24 | new languageFeatures.CompletionAdapter(worker), 25 | ) 26 | languages.registerHoverProvider(languageId, new languageFeatures.HoverAdapter(worker)) 27 | 28 | languages.registerDocumentHighlightProvider( 29 | languageId, 30 | new languageFeatures.DocumentHighlightAdapter(worker), 31 | ) 32 | languages.registerLinkProvider(languageId, new languageFeatures.DocumentLinkAdapter(worker)) 33 | languages.registerFoldingRangeProvider( 34 | languageId, 35 | new languageFeatures.FoldingRangeAdapter(worker), 36 | ) 37 | languages.registerDocumentSymbolProvider( 38 | languageId, 39 | new languageFeatures.DocumentSymbolAdapter(worker), 40 | ) 41 | languages.registerSelectionRangeProvider( 42 | languageId, 43 | new languageFeatures.SelectionRangeAdapter(worker), 44 | ) 45 | languages.registerRenameProvider(languageId, new languageFeatures.RenameAdapter(worker)) 46 | 47 | // only html 48 | if (languageId === 'html') { 49 | languages.registerDocumentFormattingEditProvider( 50 | languageId, 51 | new languageFeatures.DocumentFormattingEditProvider(worker), 52 | ) 53 | languages.registerDocumentRangeFormattingEditProvider( 54 | languageId, 55 | new languageFeatures.DocumentRangeFormattingEditProvider(worker), 56 | ) 57 | new languageFeatures.DiagnosticsAdapter(languageId, worker, defaults) 58 | } 59 | } 60 | 61 | export function setupMode(defaults: LanguageServiceDefaults): IDisposable { 62 | const disposables: IDisposable[] = [] 63 | const providers: IDisposable[] = [] 64 | 65 | const client = new WorkerManager(defaults) 66 | disposables.push(client) 67 | 68 | const worker: languageFeatures.WorkerAccessor = (...uris: Uri[]): Promise => { 69 | return client.getLanguageServiceWorker(...uris) 70 | } 71 | 72 | function registerProviders(): void { 73 | const { languageId, modeConfiguration } = defaults 74 | 75 | disposeAll(providers) 76 | 77 | if (modeConfiguration.completionItems) { 78 | providers.push( 79 | languages.registerCompletionItemProvider( 80 | languageId, 81 | new languageFeatures.CompletionAdapter(worker), 82 | ), 83 | ) 84 | } 85 | if (modeConfiguration.hovers) { 86 | providers.push( 87 | languages.registerHoverProvider(languageId, new languageFeatures.HoverAdapter(worker)), 88 | ) 89 | } 90 | if (modeConfiguration.documentHighlights) { 91 | providers.push( 92 | languages.registerDocumentHighlightProvider( 93 | languageId, 94 | new languageFeatures.DocumentHighlightAdapter(worker), 95 | ), 96 | ) 97 | } 98 | if (modeConfiguration.links) { 99 | providers.push( 100 | languages.registerLinkProvider(languageId, new languageFeatures.DocumentLinkAdapter(worker)), 101 | ) 102 | } 103 | if (modeConfiguration.documentSymbols) { 104 | providers.push( 105 | languages.registerDocumentSymbolProvider( 106 | languageId, 107 | new languageFeatures.DocumentSymbolAdapter(worker), 108 | ), 109 | ) 110 | } 111 | if (modeConfiguration.rename) { 112 | providers.push( 113 | languages.registerRenameProvider(languageId, new languageFeatures.RenameAdapter(worker)), 114 | ) 115 | } 116 | if (modeConfiguration.foldingRanges) { 117 | providers.push( 118 | languages.registerFoldingRangeProvider( 119 | languageId, 120 | new languageFeatures.FoldingRangeAdapter(worker), 121 | ), 122 | ) 123 | } 124 | if (modeConfiguration.selectionRanges) { 125 | providers.push( 126 | languages.registerSelectionRangeProvider( 127 | languageId, 128 | new languageFeatures.SelectionRangeAdapter(worker), 129 | ), 130 | ) 131 | } 132 | if (modeConfiguration.documentFormattingEdits) { 133 | providers.push( 134 | languages.registerDocumentFormattingEditProvider( 135 | languageId, 136 | new languageFeatures.DocumentFormattingEditProvider(worker), 137 | ), 138 | ) 139 | } 140 | if (modeConfiguration.documentRangeFormattingEdits) { 141 | providers.push( 142 | languages.registerDocumentRangeFormattingEditProvider( 143 | languageId, 144 | new languageFeatures.DocumentRangeFormattingEditProvider(worker), 145 | ), 146 | ) 147 | } 148 | if (modeConfiguration.diagnostics) 149 | providers.push(new languageFeatures.DiagnosticsAdapter(languageId, worker, defaults)) 150 | } 151 | 152 | registerProviders() 153 | 154 | disposables.push(asDisposable(providers)) 155 | 156 | return asDisposable(disposables) 157 | } 158 | 159 | function asDisposable(disposables: IDisposable[]): IDisposable { 160 | return { dispose: () => disposeAll(disposables) } 161 | } 162 | 163 | function disposeAll(disposables: IDisposable[]) { 164 | disposables.forEach(disposable => disposable.dispose()) 165 | } 166 | -------------------------------------------------------------------------------- /src/components/playground/Preview.vue: -------------------------------------------------------------------------------- 1 | 178 | 179 | 189 | 190 | 204 | -------------------------------------------------------------------------------- /src/monaco/languages/html/monaco.contribution.ts: -------------------------------------------------------------------------------- 1 | /* --------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *-------------------------------------------------------------------------------------------- */ 5 | 6 | import * as mode from './htmlMode' 7 | import { languages, Emitter, IEvent } from './fillers/monaco-editor-core' 8 | 9 | export interface HTMLFormatConfiguration { 10 | readonly tabSize: number 11 | readonly insertSpaces: boolean 12 | readonly wrapLineLength: number 13 | readonly unformatted: string 14 | readonly contentUnformatted: string 15 | readonly indentInnerHtml: boolean 16 | readonly preserveNewLines: boolean 17 | readonly maxPreserveNewLines: number 18 | readonly indentHandlebars: boolean 19 | readonly endWithNewline: boolean 20 | readonly extraLiners: string 21 | readonly wrapAttributes: 'auto' | 'force' | 'force-aligned' | 'force-expand-multiline' 22 | } 23 | 24 | export interface CompletionConfiguration { 25 | [provider: string]: boolean 26 | } 27 | 28 | export interface Options { 29 | /** 30 | * If set, comments are tolerated. If set to false, syntax errors will be emitted for comments. 31 | */ 32 | readonly format?: HTMLFormatConfiguration 33 | /** 34 | * A list of known schemas and/or associations of schemas to file names. 35 | */ 36 | readonly suggest?: CompletionConfiguration 37 | } 38 | 39 | export interface ModeConfiguration { 40 | /** 41 | * Defines whether the built-in completionItemProvider is enabled. 42 | */ 43 | readonly completionItems?: boolean 44 | 45 | /** 46 | * Defines whether the built-in hoverProvider is enabled. 47 | */ 48 | readonly hovers?: boolean 49 | 50 | /** 51 | * Defines whether the built-in documentSymbolProvider is enabled. 52 | */ 53 | readonly documentSymbols?: boolean 54 | 55 | /** 56 | * Defines whether the built-in definitions provider is enabled. 57 | */ 58 | readonly links?: boolean 59 | 60 | /** 61 | * Defines whether the built-in references provider is enabled. 62 | */ 63 | readonly documentHighlights?: boolean 64 | 65 | /** 66 | * Defines whether the built-in rename provider is enabled. 67 | */ 68 | readonly rename?: boolean 69 | 70 | /** 71 | * Defines whether the built-in color provider is enabled. 72 | */ 73 | readonly colors?: boolean 74 | 75 | /** 76 | * Defines whether the built-in foldingRange provider is enabled. 77 | */ 78 | readonly foldingRanges?: boolean 79 | 80 | /** 81 | * Defines whether the built-in diagnostic provider is enabled. 82 | */ 83 | readonly diagnostics?: boolean 84 | 85 | /** 86 | * Defines whether the built-in selection range provider is enabled. 87 | */ 88 | readonly selectionRanges?: boolean 89 | 90 | /** 91 | * Defines whether the built-in documentFormattingEdit provider is enabled. 92 | */ 93 | readonly documentFormattingEdits?: boolean 94 | 95 | /** 96 | * Defines whether the built-in documentRangeFormattingEdit provider is enabled. 97 | */ 98 | readonly documentRangeFormattingEdits?: boolean 99 | } 100 | 101 | export interface LanguageServiceDefaults { 102 | readonly languageId: string 103 | readonly modeConfiguration: ModeConfiguration 104 | readonly onDidChange: IEvent 105 | readonly options: Options 106 | setOptions(options: Options): void 107 | } 108 | 109 | // --- HTML configuration and defaults --------- 110 | 111 | class LanguageServiceDefaultsImpl implements LanguageServiceDefaults { 112 | private _onDidChange = new Emitter() 113 | private _options: Options 114 | private _modeConfiguration: ModeConfiguration 115 | private _languageId: string 116 | 117 | constructor(languageId: string, options: Options, modeConfiguration: ModeConfiguration) { 118 | this._languageId = languageId 119 | this.setOptions(options) 120 | this.setModeConfiguration(modeConfiguration) 121 | } 122 | 123 | get onDidChange(): IEvent { 124 | return this._onDidChange.event 125 | } 126 | 127 | get languageId(): string { 128 | return this._languageId 129 | } 130 | 131 | get options(): Options { 132 | return this._options 133 | } 134 | 135 | get modeConfiguration(): ModeConfiguration { 136 | return this._modeConfiguration 137 | } 138 | 139 | setOptions(options: Options): void { 140 | this._options = options || Object.create(null) 141 | this._onDidChange.fire(this) 142 | } 143 | 144 | setModeConfiguration(modeConfiguration: ModeConfiguration): void { 145 | this._modeConfiguration = modeConfiguration || Object.create(null) 146 | this._onDidChange.fire(this) 147 | } 148 | } 149 | 150 | const formatDefaults: Required = { 151 | tabSize: 4, 152 | insertSpaces: false, 153 | wrapLineLength: 120, 154 | unformatted: 'default": "a, abbr, acronym, b, bdo, big, br, button, cite, code, dfn, em, i, img, input, kbd, label, map, object, q, samp, select, small, span, strong, sub, sup, textarea, tt, var', 155 | contentUnformatted: 'pre', 156 | indentInnerHtml: false, 157 | preserveNewLines: true, 158 | maxPreserveNewLines: null, 159 | indentHandlebars: false, 160 | endWithNewline: false, 161 | extraLiners: 'head, body, /html', 162 | wrapAttributes: 'auto', 163 | } 164 | 165 | const htmlOptionsDefault: Required = { 166 | format: formatDefaults, 167 | suggest: { html5: true, angular1: true, ionic: true }, 168 | } 169 | 170 | const handlebarOptionsDefault: Required = { 171 | format: formatDefaults, 172 | suggest: { html5: true }, 173 | } 174 | 175 | const razorOptionsDefault: Required = { 176 | format: formatDefaults, 177 | suggest: { html5: true, razor: true }, 178 | } 179 | 180 | function getConfigurationDefault(languageId: string): Required { 181 | return { 182 | completionItems: true, 183 | hovers: true, 184 | documentSymbols: true, 185 | links: true, 186 | documentHighlights: true, 187 | rename: true, 188 | colors: true, 189 | foldingRanges: true, 190 | selectionRanges: true, 191 | diagnostics: languageId === htmlLanguageId, // turned off for Razor and Handlebar 192 | documentFormattingEdits: languageId === htmlLanguageId, // turned off for Razor and Handlebar 193 | documentRangeFormattingEdits: languageId === htmlLanguageId, // turned off for Razor and Handlebar 194 | } 195 | } 196 | 197 | const htmlLanguageId = 'html' 198 | const handlebarsLanguageId = 'handlebars' 199 | const razorLanguageId = 'razor' 200 | 201 | export const htmlDefaults: LanguageServiceDefaults = new LanguageServiceDefaultsImpl( 202 | htmlLanguageId, 203 | htmlOptionsDefault, 204 | getConfigurationDefault(htmlLanguageId), 205 | ) 206 | export const handlebarDefaults: LanguageServiceDefaults = new LanguageServiceDefaultsImpl( 207 | handlebarsLanguageId, 208 | handlebarOptionsDefault, 209 | getConfigurationDefault(handlebarsLanguageId), 210 | ) 211 | export const razorDefaults: LanguageServiceDefaults = new LanguageServiceDefaultsImpl( 212 | razorLanguageId, 213 | razorOptionsDefault, 214 | getConfigurationDefault(razorLanguageId), 215 | ); 216 | 217 | // export to the global based API 218 | (languages).html = { htmlDefaults, razorDefaults, handlebarDefaults } 219 | 220 | // --- Registration to monaco editor --- 221 | 222 | function getMode(): Promise { 223 | return import('./htmlMode') 224 | } 225 | 226 | languages.onLanguage(htmlLanguageId, () => { 227 | getMode().then(mode => mode.setupMode(htmlDefaults)) 228 | }) 229 | languages.onLanguage(handlebarsLanguageId, () => { 230 | getMode().then(mode => mode.setupMode(handlebarDefaults)) 231 | }) 232 | languages.onLanguage(razorLanguageId, () => { 233 | getMode().then(mode => mode.setupMode(razorDefaults)) 234 | }) 235 | -------------------------------------------------------------------------------- /src/orchestrator.ts: -------------------------------------------------------------------------------- 1 | import { reactive, watch, watchEffect } from 'vue' 2 | // import { parse } from '@vue/compiler-sfc' 3 | import { createEventHook } from '@vueuse/core' 4 | import lz from 'lz-string' 5 | import { compileFile } from './compiler/sfcCompiler' 6 | // const demos = import.meta.glob('../demos/**/*.(vue|json)') 7 | 8 | const shouldUpdateContent = createEventHook() 9 | 10 | export interface OrchestratorPackage { 11 | name: string 12 | description?: string 13 | version?: string 14 | url: string 15 | source?: string 16 | } 17 | 18 | export class OrchestratorFile { 19 | filename: string 20 | template: string 21 | script: string 22 | style: string 23 | 24 | compiled = { 25 | js: '', 26 | css: '', 27 | ssr: '', 28 | } 29 | 30 | constructor(filename: string, template: string | undefined, script: string | undefined, style?: string) { 31 | this.filename = filename 32 | this.template = template || '' 33 | this.script = script || '' 34 | this.style = style || '' 35 | } 36 | 37 | get code() { 38 | return ` 39 | 42 | 45 | ` 46 | } 47 | } 48 | 49 | export interface Orchestrator { 50 | files: { 51 | [key: string]: OrchestratorFile 52 | } 53 | packages: OrchestratorPackage[] 54 | activeFilename: string 55 | errors: (string | Error)[] 56 | runtimeErrors: (string | Error)[] 57 | 58 | readonly activeFile: OrchestratorFile | undefined 59 | readonly importMap: string 60 | } 61 | 62 | /** 63 | * Main app orchestrator, handles all the files, import maps, and errors 64 | */ 65 | export const orchestrator: Orchestrator = reactive({ 66 | files: { 67 | 'App.vue': new OrchestratorFile('App.vue', '', ''), 68 | }, 69 | packages: [], 70 | activeFilename: 'App.vue', 71 | errors: [], 72 | runtimeErrors: [], 73 | 74 | get activeFile() { 75 | // @ts-ignore 76 | return orchestrator.files[this.activeFilename] 77 | }, 78 | 79 | get importMap() { 80 | const imports = orchestrator.packages.map(({ name, url }) => `"${name}": "${url}"`) 81 | 82 | return ` 83 | { 84 | "imports": { 85 | ${imports.join(',\n')} 86 | } 87 | } 88 | ` 89 | }, 90 | }) 91 | 92 | /** 93 | * Setup Watchers 94 | */ 95 | 96 | watchEffect(() => { 97 | if (orchestrator.activeFile) 98 | compileFile(orchestrator.activeFile) 99 | }) 100 | 101 | watch(() => orchestrator.activeFilename, () => { 102 | shouldUpdateContent.trigger(null) 103 | }) 104 | 105 | export function exportState() { 106 | const files = Object.entries(orchestrator.files).reduce((acc, [name, { template, script }]) => { 107 | acc[name] = { template, script } 108 | return acc 109 | }, {}) 110 | 111 | return lz.compressToEncodedURIComponent(JSON.stringify({ 112 | packages: orchestrator.packages, 113 | files, 114 | })) 115 | } 116 | 117 | 118 | /** 119 | * Add a file to the orchestrator 120 | * 121 | * @param file File content 122 | */ 123 | export function addFile(file: OrchestratorFile) { 124 | orchestrator.files = { 125 | ...orchestrator.files, 126 | [file.filename]: file, 127 | } 128 | 129 | compileFile(orchestrator.files[file.filename]) 130 | } 131 | 132 | export function setActiveFile(name: string) { 133 | orchestrator.activeFilename = name 134 | } 135 | 136 | /** 137 | * Remove a file from the orchestrator 138 | * 139 | * @param name Name of file to remove 140 | */ 141 | export function removeFile(name: string) { 142 | delete orchestrator.files[name] 143 | setTimeout(() => setActiveFile('App.vue'), 0) 144 | } 145 | 146 | /** 147 | * Remove all files from the orchestrator 148 | */ 149 | export function removeAllFiles() { 150 | orchestrator.files = {} 151 | } 152 | 153 | /** 154 | * Load a demo folder 155 | * 156 | * @param name Name of demo to open 157 | */ 158 | // export async function openDemo(name: string) { 159 | // // Get all modules from demo 160 | // const modules = (await Promise.all(Object.entries(demos) 161 | // .filter(([path]) => path.split('demos/')[1].split('/')[0] === name) 162 | // .filter(([path]) => path.includes('.vue') || path.includes('.json')) 163 | // .map(async([path]) => ([path, (await import(`${path}?raw`)).default])))) 164 | 165 | // console.log(modules) 166 | 167 | // const packages = (await Promise.all(Object.entries(demos) 168 | // .filter(([path]) => path.split('demos/')[1].split('/')[0] === name) 169 | // .filter(([path]) => path.includes('.json')) 170 | // .map(async([path, imp]) => ([path, (await imp()).default])))) 171 | // .find(([path]) => path.includes('packages.json')) 172 | 173 | // if (packages) 174 | // orchestrator.packages = packages[1] 175 | 176 | // removeAllFiles() 177 | 178 | // // Load Vue Files 179 | // modules 180 | // .filter(([path]) => path.includes('.vue')) 181 | // .map(([path, content]) => { 182 | // const { descriptor: { template, scriptSetup } } = parse(content) 183 | // return { 184 | // filename: path.split(`${name}/`)[1], 185 | // script: scriptSetup?.content.trim(), 186 | // template: template?.content.trim(), 187 | // } 188 | // }) 189 | // .forEach(({ filename, script, template }) => { 190 | // addFile(new OrchestratorFile(filename, template, script)) 191 | // }) 192 | 193 | // setActiveFile('App.vue') 194 | // shouldUpdateContent.trigger(null) 195 | // } 196 | 197 | export const onShouldUpdateContent = shouldUpdateContent.on 198 | 199 | // openDemo('default') 200 | 201 | // App.vue 202 | const appTemplate = ` 203 |
209 | 210 | 211 |
212 | ` 213 | const appScript = ` 214 | import { useMouse } from '@vueuse/core' 215 | import Coordinate from './Coordinate.vue' 216 | 217 | const { x, y } = useMouse() 218 | ` 219 | 220 | // Coordinate.vue 221 | const coordinateTemplate = ` 222 |
230 | {{ value }} 231 | Mouse {{ label }} 232 |
233 | ` 234 | 235 | const coordinateScript = ` 236 | defineProps({ 237 | label: String, 238 | value: Number, 239 | }) 240 | ` 241 | 242 | const initialPackages = [ 243 | { 244 | name: 'vue-demi', 245 | source: 'unpkg', 246 | description: 'Vue Demi (half in French) is a developing utility allows you to write Universal Vue Libraries for Vue 2 & 3', 247 | url: 'https://unpkg.com/vue-demi/lib/index.mjs', 248 | }, 249 | { 250 | name: '@vueuse/shared', 251 | source: 'unpkg', 252 | description: 'Shared VueUse utilities.', 253 | url: 'https://unpkg.com/@vueuse/shared@10.1.0/index.mjs', 254 | }, 255 | { 256 | name: '@vueuse/core', 257 | source: 'unpkg', 258 | description: 'Collection of essential Vue Composition Utilities', 259 | url: 'https://unpkg.com/@vueuse/core@10.1.0/index.mjs', 260 | }, 261 | ] 262 | 263 | function loadInitialState() { 264 | removeAllFiles() 265 | 266 | if (location.hash.slice(1)) { 267 | const { files, packages } = JSON.parse(lz.decompressFromEncodedURIComponent(location.hash.slice(1))) 268 | 269 | console.log(files, packages) 270 | 271 | if (files && packages) { 272 | orchestrator.packages = packages 273 | 274 | for (const f in files) { 275 | console.log(f) 276 | addFile(new OrchestratorFile(f, files[f].template, files[f].script)) 277 | } 278 | setActiveFile('App.vue') 279 | shouldUpdateContent.trigger(null) 280 | } 281 | } 282 | else { 283 | orchestrator.packages = initialPackages 284 | addFile(new OrchestratorFile('App.vue', appTemplate.trim(), appScript.trim())) 285 | addFile(new OrchestratorFile('Coordinate.vue', coordinateTemplate.trim(), coordinateScript.trim())) 286 | setActiveFile('App.vue') 287 | shouldUpdateContent.trigger(null) 288 | } 289 | } 290 | 291 | setTimeout(() => { 292 | loadInitialState() 293 | }, 0) 294 | -------------------------------------------------------------------------------- /src/compiler/sfcCompiler.ts: -------------------------------------------------------------------------------- 1 | import { SFCDescriptor, BindingMetadata } from '@vue/compiler-sfc' 2 | import * as defaultCompiler from '@vue/compiler-sfc' 3 | import { ref } from 'vue' 4 | import { orchestrator as store, OrchestratorFile as File } from '../orchestrator' 5 | import { generateStyles } from './windi' 6 | 7 | export const MAIN_FILE = 'App.vue' 8 | export const COMP_IDENTIFIER = '__sfc__' 9 | 10 | /** 11 | * The default SFC compiler we are using is built from each commit 12 | * but we may swap it out with a version fetched from CDNs 13 | */ 14 | let SFCCompiler: typeof defaultCompiler = defaultCompiler 15 | 16 | // @ts-ignore 17 | const defaultVueUrl = import.meta.env.PROD 18 | ? `${location.origin}/vue.runtime.esm-browser.js` // to be copied on build 19 | : `${location.origin}/src/vue-dev-proxy` 20 | 21 | export const vueRuntimeUrl = ref(defaultVueUrl) 22 | 23 | export async function setVersion(version: string) { 24 | const compilerUrl = `https://unpkg.com/@vue/compiler-sfc@${version}/dist/compiler-sfc.esm-browser.js` 25 | const runtimeUrl = `https://unpkg.com/@vue/runtime-dom@${version}/dist/runtime-dom.esm-browser.js` 26 | const [compiler] = await Promise.all([ 27 | import(/* @vite-ignore */ compilerUrl), 28 | import(/* @vite-ignore */ runtimeUrl), 29 | ]) 30 | SFCCompiler = compiler 31 | vueRuntimeUrl.value = runtimeUrl 32 | // eslint-disable-next-line no-console 33 | console.info(`Now using Vue version: ${version}`) 34 | } 35 | 36 | export function resetVersion() { 37 | SFCCompiler = defaultCompiler 38 | vueRuntimeUrl.value = defaultVueUrl 39 | } 40 | 41 | export async function compileFile({ filename, code, compiled }: File) { 42 | if (!code.trim()) { 43 | store.errors = [] 44 | return 45 | } 46 | 47 | if (!filename.endsWith('.vue')) { 48 | compiled.js = compiled.ssr = code 49 | store.errors = [] 50 | return 51 | } 52 | 53 | const id = await hashId(filename) 54 | const { errors, descriptor } = SFCCompiler.parse(code, { 55 | filename, 56 | sourceMap: true, 57 | }) 58 | if (errors.length) { 59 | store.errors = errors 60 | return 61 | } 62 | 63 | if ( 64 | (descriptor.script && descriptor.script.lang) 65 | || (descriptor.scriptSetup && descriptor.scriptSetup.lang) 66 | || descriptor.styles.some(s => s.lang) 67 | || (descriptor.template && descriptor.template.lang) 68 | ) { 69 | store.errors = [ 70 | 'lang="x" pre-processors are not supported in the in-browser playground.', 71 | ] 72 | return 73 | } 74 | 75 | const hasScoped = descriptor.styles.some(s => s.scoped) 76 | let clientCode = '' 77 | let ssrCode = '' 78 | 79 | const appendSharedCode = (code: string) => { 80 | clientCode += code 81 | ssrCode += code 82 | } 83 | 84 | const clientScriptResult = doCompileScript(descriptor, id, false) 85 | if (!clientScriptResult) 86 | return 87 | 88 | const [clientScript, bindings] = clientScriptResult 89 | clientCode += clientScript 90 | 91 | // script ssr only needs to be performed if using 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 |
265 | 266 | 267 | -------------------------------------------------------------------------------- /src/compiler/moduleCompiler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | babelParse, 3 | MagicString, 4 | walk, 5 | walkIdentifiers, 6 | } from '@vue/compiler-sfc' 7 | import { ExportSpecifier, Identifier, Node, ObjectProperty } from '@babel/types' 8 | import { orchestrator as store, OrchestratorFile as File } from '../orchestrator' 9 | import { MAIN_FILE } from './sfcCompiler' 10 | 11 | export function compileModulesForPreview() { 12 | return processFile(store.files[MAIN_FILE]).reverse() 13 | } 14 | 15 | const modulesKey = '__modules__' 16 | const exportKey = '__export__' 17 | const dynamicImportKey = '__dynamic_import__' 18 | const moduleKey = '__module__' 19 | 20 | // similar logic with Vite's SSR transform, except this is targeting the browser 21 | function processFile(file: File, seen = new Set()) { 22 | if (seen.has(file)) 23 | return [] 24 | 25 | seen.add(file) 26 | 27 | const { js, css } = file.compiled 28 | 29 | const s = new MagicString(js) 30 | 31 | const ast = babelParse(js, { 32 | sourceFilename: file.filename, 33 | sourceType: 'module', 34 | }).program.body 35 | 36 | const idToImportMap = new Map() 37 | const declaredConst = new Set() 38 | const importedFiles = new Set() 39 | const importToIdMap = new Map() 40 | 41 | function defineImport(node: Node, source: string) { 42 | const filename = source.replace(/^\.\/+/, '') 43 | if (!(filename in store.files)) 44 | throw new Error(`File "${filename}" does not exist.`) 45 | 46 | if (importedFiles.has(filename)) 47 | return importToIdMap.get(filename)! 48 | 49 | importedFiles.add(filename) 50 | const id = `__import_${importedFiles.size}__` 51 | importToIdMap.set(filename, id) 52 | s.appendLeft( 53 | node.start!, 54 | `const ${id} = ${modulesKey}[${JSON.stringify(filename)}]\n`, 55 | ) 56 | return id 57 | } 58 | 59 | function defineExport(name: string, local = name) { 60 | s.append(`\n${exportKey}(${moduleKey}, "${name}", () => ${local})`) 61 | } 62 | 63 | // 0. instantiate module 64 | s.prepend( 65 | `const ${moduleKey} = __modules__[${JSON.stringify( 66 | file.filename, 67 | )}] = { [Symbol.toStringTag]: "Module" }\n\n`, 68 | ) 69 | 70 | // 1. check all import statements and record id -> importName map 71 | for (const node of ast) { 72 | // import foo from 'foo' --> foo -> __import_foo__.default 73 | // import { baz } from 'foo' --> baz -> __import_foo__.baz 74 | // import * as ok from 'foo' --> ok -> __import_foo__ 75 | if (node.type === 'ImportDeclaration') { 76 | const source = node.source.value 77 | if (source.startsWith('./')) { 78 | const importId = defineImport(node, node.source.value) 79 | for (const spec of node.specifiers) { 80 | if (spec.type === 'ImportSpecifier') { 81 | idToImportMap.set( 82 | spec.local.name, 83 | `${importId}.${(spec.imported as Identifier).name}`, 84 | ) 85 | } 86 | else if (spec.type === 'ImportDefaultSpecifier') { 87 | idToImportMap.set(spec.local.name, `${importId}.default`) 88 | } 89 | else { 90 | // namespace specifier 91 | idToImportMap.set(spec.local.name, importId) 92 | } 93 | } 94 | s.remove(node.start!, node.end!) 95 | } 96 | } 97 | } 98 | 99 | // 2. check all export statements and define exports 100 | for (const node of ast) { 101 | // named exports 102 | if (node.type === 'ExportNamedDeclaration') { 103 | if (node.declaration) { 104 | if ( 105 | node.declaration.type === 'FunctionDeclaration' 106 | || node.declaration.type === 'ClassDeclaration' 107 | ) { 108 | // export function foo() {} 109 | defineExport(node.declaration.id!.name) 110 | } 111 | else if (node.declaration.type === 'VariableDeclaration') { 112 | // export const foo = 1, bar = 2 113 | for (const decl of node.declaration.declarations) { 114 | const names = extractNames(decl.id as any) 115 | for (const name of names) 116 | defineExport(name) 117 | } 118 | } 119 | s.remove(node.start!, node.declaration.start!) 120 | } 121 | else if (node.source) { 122 | // export { foo, bar } from './foo' 123 | const importId = defineImport(node, node.source.value) 124 | for (const spec of node.specifiers) { 125 | defineExport( 126 | (spec.exported as Identifier).name, 127 | `${importId}.${(spec as ExportSpecifier).local.name}`, 128 | ) 129 | } 130 | s.remove(node.start!, node.end!) 131 | } 132 | else { 133 | // export { foo, bar } 134 | for (const spec of node.specifiers) { 135 | const local = (spec as ExportSpecifier).local.name 136 | const binding = idToImportMap.get(local) 137 | defineExport((spec.exported as Identifier).name, binding || local) 138 | } 139 | s.remove(node.start!, node.end!) 140 | } 141 | } 142 | 143 | // default export 144 | if (node.type === 'ExportDefaultDeclaration') 145 | s.overwrite(node.start!, node.start! + 14, `${moduleKey}.default =`) 146 | 147 | // export * from './foo' 148 | if (node.type === 'ExportAllDeclaration') { 149 | const importId = defineImport(node, node.source.value) 150 | s.remove(node.start!, node.end!) 151 | s.append(`\nfor (const key in ${importId}) { 152 | if (key !== 'default') { 153 | ${exportKey}(${moduleKey}, key, () => ${importId}[key]) 154 | } 155 | }`) 156 | } 157 | } 158 | 159 | // 3. convert references to import bindings 160 | for (const node of ast) { 161 | if (node.type === 'ImportDeclaration') continue 162 | walkIdentifiers(node, (id, parent, parentStack) => { 163 | const binding = idToImportMap.get(id.name) 164 | if (!binding) 165 | return 166 | 167 | if (isStaticProperty(parent) && parent.shorthand) { 168 | // let binding used in a property shorthand 169 | // { foo } -> { foo: __import_x__.foo } 170 | // skip for destructure patterns 171 | if ( 172 | !(parent as any).inPattern 173 | || isInDestructureAssignment(parent, parentStack) 174 | ) 175 | s.appendLeft(id.end!, `: ${binding}`) 176 | } 177 | else if ( 178 | parent.type === 'ClassDeclaration' 179 | && id === parent.superClass 180 | ) { 181 | if (!declaredConst.has(id.name)) { 182 | declaredConst.add(id.name) 183 | // locate the top-most node containing the class declaration 184 | const topNode = parentStack[1] 185 | s.prependRight(topNode.start!, `const ${id.name} = ${binding};\n`) 186 | } 187 | } 188 | else { 189 | s.overwrite(id.start!, id.end!, binding) 190 | } 191 | }) 192 | } 193 | 194 | // 4. convert dynamic imports 195 | (walk as any)(ast, { 196 | enter(node: Node, parent: Node) { 197 | if (node.type === 'Import' && parent.type === 'CallExpression') { 198 | const arg = parent.arguments[0] 199 | if (arg.type === 'StringLiteral' && arg.value.startsWith('./')) { 200 | s.overwrite(node.start!, node.start! + 6, dynamicImportKey) 201 | s.overwrite( 202 | arg.start!, 203 | arg.end!, 204 | JSON.stringify(arg.value.replace(/^\.\/+/, '')), 205 | ) 206 | } 207 | } 208 | }, 209 | }) 210 | 211 | // append CSS injection code 212 | if (css) 213 | s.append(`\nwindow.__css__ += ${JSON.stringify(css)}`) 214 | 215 | const processed = [s.toString()] 216 | if (importedFiles.size) { 217 | for (const imported of importedFiles) 218 | processed.push(...processFile(store.files[imported], seen)) 219 | } 220 | 221 | // return a list of files to further process 222 | return processed 223 | } 224 | 225 | const isStaticProperty = (node: Node): node is ObjectProperty => 226 | node.type === 'ObjectProperty' && !node.computed 227 | 228 | function extractNames(param: Node): string[] { 229 | return extractIdentifiers(param).map(id => id.name) 230 | } 231 | 232 | function extractIdentifiers( 233 | param: Node, 234 | nodes: Identifier[] = [], 235 | ): Identifier[] { 236 | switch (param.type) { 237 | case 'Identifier': 238 | nodes.push(param) 239 | break 240 | 241 | case 'MemberExpression': 242 | let object: any = param 243 | while (object.type === 'MemberExpression') 244 | object = object.object 245 | 246 | nodes.push(object) 247 | break 248 | 249 | case 'ObjectPattern': 250 | param.properties.forEach((prop) => { 251 | if (prop.type === 'RestElement') 252 | extractIdentifiers(prop.argument, nodes) 253 | else 254 | extractIdentifiers(prop.value, nodes) 255 | }) 256 | break 257 | 258 | case 'ArrayPattern': 259 | param.elements.forEach((element) => { 260 | if (element) extractIdentifiers(element, nodes) 261 | }) 262 | break 263 | 264 | case 'RestElement': 265 | extractIdentifiers(param.argument, nodes) 266 | break 267 | 268 | case 'AssignmentPattern': 269 | extractIdentifiers(param.left, nodes) 270 | break 271 | } 272 | 273 | return nodes 274 | } 275 | 276 | function isInDestructureAssignment(parent: Node, parentStack: Node[]): boolean { 277 | if ( 278 | parent 279 | && (parent.type === 'ObjectProperty' || parent.type === 'ArrayPattern') 280 | ) { 281 | let i = parentStack.length 282 | while (i--) { 283 | const p = parentStack[i] 284 | if (p.type === 'AssignmentExpression') 285 | return true 286 | else if (p.type !== 'ObjectProperty' && !p.type.endsWith('Pattern')) 287 | break 288 | } 289 | } 290 | return false 291 | } 292 | -------------------------------------------------------------------------------- /src/monaco/plugins/windicss/utils/completions.ts: -------------------------------------------------------------------------------- 1 | import type { Processor } from 'windicss/lib' 2 | import type { Attr, Completion } from '../interfaces' 3 | import { utilities, negative } from './utilities' 4 | import { flatColors } from './index' 5 | 6 | export function generateCompletions(processor: Processor, colors: any, attributify = true, prefix = '') { 7 | const completions: Completion = { 8 | static: [], 9 | color: [], 10 | bracket: [], 11 | dynamic: [], 12 | attr: { 13 | static: {}, 14 | color: {}, 15 | bracket: {}, 16 | dynamic: {}, 17 | }, 18 | } 19 | const staticUtilities = processor.resolveStaticUtilities(true) 20 | // generate normal utilities completions 21 | for (const [config, list] of Object.entries({ ...utilities, ...processor._plugin.completions })) { 22 | for (const utility of list) { 23 | const bracket = utility.indexOf('[') 24 | if (bracket !== -1) { 25 | completions.bracket.push(utility) 26 | continue 27 | } 28 | const mark = utility.indexOf('{') 29 | if (mark === -1) { 30 | completions.static.push(utility) 31 | } 32 | else { 33 | const key = prefix + utility.slice(0, mark - 1) 34 | const suffix = utility.slice(mark) 35 | switch (suffix) { 36 | case '{static}': 37 | for (const i of Object.keys(processor.theme(config, {}) as any)) 38 | completions.static.push(i === 'DEFAULT' ? key : i.charAt(0) === '-' ? `-${key}${i}` : `${key}-${i}`) 39 | 40 | break 41 | case '{color}': 42 | for (const [k, v] of Object.entries(flatColors(processor.theme(config, colors)))) { 43 | if (k === 'DEFAULT') continue 44 | completions.color.push({ 45 | label: `${key}-${k}`, 46 | doc: v, 47 | }) 48 | } 49 | break 50 | default: 51 | completions.dynamic.push({ 52 | label: prefix + utility, 53 | pos: utility.length - mark, 54 | }) 55 | if (config in negative) { 56 | completions.dynamic.push({ 57 | label: `${prefix}-${utility}`, 58 | pos: utility.length + 1 - mark, 59 | }) 60 | } 61 | break 62 | } 63 | } 64 | } 65 | } 66 | 67 | // generate attributify completions 68 | const attr: Attr = { static: {}, color: {}, bracket: {}, dynamic: {} } 69 | 70 | if (attributify) { 71 | const attrDisable = processor.config('attributify.disable') as string[] | undefined 72 | const addAttr = (key: string, value: any, type: 'static' | 'color' | 'bracket' | 'dynamic' = 'static') => { 73 | if (attrDisable && attrDisable.includes(key)) return 74 | key in attr[type] ? attr[type][key].push(value) : attr[type][key] = [value] 75 | } 76 | 77 | addAttr('flex', '~') 78 | addAttr('flex', 'inline') 79 | addAttr('grid', '~') 80 | addAttr('grid', 'inline') 81 | addAttr('gradient', 'none') 82 | addAttr('underline', '~') 83 | addAttr('underline', 'line-through') 84 | addAttr('underline', 'none') 85 | addAttr('filter', '~') 86 | addAttr('filter', 'none') 87 | addAttr('backdrop', '~') 88 | addAttr('backdrop', 'none') 89 | 90 | for (const [key, style] of Object.entries(staticUtilities)) { 91 | if (!style[0]) continue 92 | switch (style[0].meta.group) { 93 | case 'fontStyle': 94 | case 'fontSmoothing': 95 | case 'fontVariantNumeric': 96 | addAttr('font', key) 97 | break 98 | case 'textAlign': 99 | addAttr('text', key.slice(5)) // text- 100 | break 101 | case 'verticalAlign': 102 | addAttr('text', key.slice(6)) // align- 103 | break 104 | case 'textDecoration': 105 | addAttr('text', key) 106 | break 107 | case 'textTransform': 108 | case 'textOverflow': 109 | case 'wordBreak': 110 | case 'writingMode': 111 | case 'writingOrientation': 112 | case 'hyphens': 113 | addAttr('text', key) 114 | break 115 | case 'whitespace': 116 | addAttr('text', key.slice(5)) // whitespace -> space 117 | break 118 | case 'listStylePosition': 119 | addAttr('list', key.slice(5)) // list- 120 | break 121 | case 'backgroundAttachment': 122 | case 'backgroundRepeat': 123 | case 'backgroundClip': 124 | case 'backgroundOrigin': 125 | case 'backgroundBlendMode': 126 | addAttr('bg', key.slice(3)) // bg- 127 | break 128 | case 'borderStyle': 129 | addAttr('border', key.slice(7)) // border- 130 | addAttr('divide', key.slice(7)) // border- 131 | break 132 | case 'borderCollapse': 133 | addAttr('border', key.slice(7)) // border- 134 | break 135 | case 'strokeDashArray': 136 | case 'strokeDashOffset': 137 | case 'stroke': 138 | addAttr('icon', key) 139 | break 140 | case 'flexWrap': 141 | case 'flexDirection': 142 | addAttr('flex', key.slice(5)) // flex- 143 | break 144 | case 'gridAutoFlow': 145 | addAttr('grid', key.slice(5)) // grid- 146 | break 147 | case 'display': 148 | if (key.startsWith('table') || key === 'inline-table') 149 | addAttr('table', key.replace(/-?table-?/, '') || '~') 150 | else 151 | addAttr('display', key) 152 | 153 | break 154 | case 'position': 155 | case 'float': 156 | case 'clear': 157 | addAttr('pos', key) 158 | break 159 | case 'isolation': 160 | addAttr('pos', key) 161 | addAttr('isolation', key.replace('isolation-', '')) 162 | break 163 | case 'visibility': 164 | case 'backfaceVisibility': 165 | addAttr('display', key) 166 | break 167 | case 'tableLayout': 168 | addAttr('table', key.slice(6)) // table- 169 | break 170 | case 'captionSide': 171 | case 'emptyCells': 172 | addAttr('table', key) 173 | break 174 | case 'alignContent': 175 | case 'alignItems': 176 | case 'alignSelf': 177 | addAttr('align', key) 178 | break 179 | case 'justifyContent': 180 | case 'justifyItems': 181 | case 'justifySelf': 182 | case 'placeContent': 183 | case 'placeItems': 184 | case 'placeSelf': 185 | case 'userSelect': 186 | case 'resize': 187 | case 'overflow': 188 | case 'appearance': 189 | case 'textDecorationStyle': 190 | case 'overscrollBehavior': 191 | const splits = split(key) 192 | if (!splits.key) break 193 | addAttr(splits.key, splits.body) 194 | break 195 | case 'boxDecorationBreak': 196 | addAttr('box', key) 197 | break 198 | case 'boxSizing': 199 | addAttr('box', key.slice(4)) // box- 200 | break 201 | case 'objectFit': 202 | addAttr('object', key.slice(7)) // object- 203 | break 204 | case 'transform': 205 | if (key.startsWith('preserve')) 206 | addAttr('transform', key) 207 | else 208 | addAttr('transform', key.slice(10) || '~') // transform- 209 | 210 | break 211 | case 'perspectOrigin': 212 | addAttr('transform', key) 213 | break 214 | case 'pointerEvents': 215 | addAttr('pointer', key.slice(15)) // pointer-events- 216 | break 217 | case 'mixBlendMode': 218 | addAttr('blend', key.slice(10)) // mix-blend- 219 | break 220 | case 'accessibility': 221 | addAttr('sr', key.replace(/sr-/, '')) 222 | break 223 | } 224 | } 225 | 226 | for (const utility of completions.static) { 227 | const { key, body } = split(utility) 228 | if (key) { 229 | if (key === 'underline') addAttr('text', utility) 230 | addAttr(key, body) 231 | } 232 | } 233 | 234 | for (const { label, doc } of completions.color) { 235 | const { key, body } = split(label) 236 | if (key) { 237 | addAttr(key, { label: body, doc }, 'color') 238 | if (key === 'underline') addAttr('text', { label, doc }, 'color') 239 | } 240 | } 241 | 242 | for (const utility of completions.bracket) { 243 | const { key, body } = split(utility) 244 | if (key) addAttr(key, body, 'bracket') 245 | } 246 | 247 | for (const { label, pos } of completions.dynamic) { 248 | const { key, body } = split(label) 249 | if (key) addAttr(key, { label: body, pos }, 'dynamic') 250 | } 251 | } 252 | 253 | completions.static.push(...Object.keys(staticUtilities)) 254 | completions.attr = attr 255 | return completions 256 | } 257 | 258 | function split(utility: string) { 259 | if (utility.startsWith('bg-gradient')) return { key: 'gradient', body: utility.replace(/^bg-gradient-/, '') } 260 | if (utility === 'w-min') return { key: 'w', body: 'min-content' } 261 | if (utility === 'w-max') return { key: 'w', body: 'max-content' } 262 | if (utility === 'h-min') return { key: 'h', body: 'min-content' } 263 | if (utility === 'h-max') return { key: 'h', body: 'max-content' } 264 | if (utility.startsWith('min-w')) return { key: 'w', body: utility.replace(/^min-w-/, 'min-') } 265 | if (utility.startsWith('max-w')) return { key: 'w', body: utility.replace(/^max-w-/, 'max-') } 266 | if (utility.startsWith('min-h')) return { key: 'h', body: utility.replace(/^min-h-/, 'min-') } 267 | if (utility.startsWith('max-h')) return { key: 'h', body: utility.replace(/^max-h-/, 'max-') } 268 | 269 | const key = utility.match(/[^-]+/)?.[0] 270 | if (key) { 271 | if (['duration', 'ease', 'delay'].includes(key)) return { key: 'transition', body: utility } 272 | if (['scale', 'rotate', 'translate', 'skew', 'origin', 'perspect'].includes(key)) return { key: 'transform', body: utility } 273 | if (['blur', 'brightness', 'contrast', 'drop', 'grayscale', 'hue', 'invert', 'saturate', 'sepia'].includes(key)) return { key: 'filter', body: utility } 274 | if (['inset', 'top', 'left', 'bottom', 'right'].includes(key)) return { key: 'pos', body: utility } 275 | if (['py', 'px', 'pt', 'pl', 'pb', 'pr'].includes(key)) return { key: 'p', body: utility.slice(1) } 276 | if (['my', 'mx', 'mt', 'ml', 'mb', 'mr'].includes(key)) return { key: 'm', body: utility.charAt(0) === '-' ? `-${utility.slice(2)}` : utility.slice(1) } 277 | if (['stroke', 'fill'].includes(key)) return { key: 'icon', body: utility } 278 | if (['from', 'via', 'to'].includes(key)) return { key: 'gradient', body: utility } 279 | if (['tracking', 'leading'].includes(key)) return { key: 'font', body: utility } 280 | if (['tab', 'indent'].includes(key)) return { key: 'text', body: utility } 281 | if (['col', 'row', 'auto', 'gap'].includes(key)) return { key: 'grid', body: utility } 282 | if (key === 'placeholder') return { key: 'text', body: utility } 283 | if (key === 'rounded') return { key: 'border', body: utility } 284 | } 285 | const negative = utility.charAt(0) === '-' 286 | const body = (negative ? utility.slice(1) : utility).match(/-.+/)?.[0].slice(1) || '~' 287 | return { key, body: negative ? `-${body}` : body } 288 | } 289 | -------------------------------------------------------------------------------- /src/monaco/plugins/windicss/utils/utilities.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-template-curly-in-string */ 2 | 3 | const utilities: { [key: string]: string[]} = { 4 | animation: [ 5 | 'animate-{static}', 6 | ], 7 | backdropBlur: [ 8 | 'backdrop-blur-{static}', 9 | 'backdrop-blur-[11px]', 10 | ], 11 | backdropBrightness: [ 12 | 'backdrop-brightness-{static}', 13 | 'backdrop-brightness-[1.23]', 14 | ], 15 | backdropContrast: [ 16 | 'backdrop-contrast-{static}', 17 | 'backdrop-contrast-[0.87]', 18 | ], 19 | backdropGrayscale: [ 20 | 'backdrop-grayscale-{static}', 21 | 'backdrop-grayscale-[0.42]', 22 | ], 23 | backdropHueRotate: [ 24 | 'backdrop-hue-rotate-{static}', 25 | 'backdrop-hue-rotate-[1.57rad]', 26 | ], 27 | backdropInvert: [ 28 | 'backdrop-invert-{static}', 29 | 'backdrop-invert-[0.66]', 30 | ], 31 | backdropOpacity: [ 32 | 'backdrop-opacity-{static}', 33 | 'backdrop-opacity-[22%]', 34 | ], 35 | backdropSaturate: [ 36 | 'backdrop-saturate-{static}', 37 | 'backdrop-saturate-[144%]', 38 | ], 39 | backdropSepia: [ 40 | 'backdrop-sepia-{static}', 41 | 'backdrop-sepia-[0.38]', 42 | ], 43 | backgroundColor: [ 44 | 'bg-{color}', 45 | 'bg-${var}', 46 | 'bg-[#0f0]', 47 | 'bg-[#ff0000]', 48 | 'bg-[#0000ffcc]', 49 | 'bg-[rgb(123,123,123)]', 50 | 'bg-[rgba(123,123,123,0.5)]', 51 | 'bg-[hsl(0,100%,50%)]', 52 | 'bg-[hsla(0,100%,50%,0.3)]', 53 | ], 54 | backgroundImage: [ 55 | 'bg-{static}', 56 | ], 57 | backgroundOpacity: [ 58 | 'bg-opacity-{static}', 59 | 'bg-opacity-{int}', 60 | 'bg-opacity-${var}', 61 | 'bg-opacity-[0.11]', 62 | 'bg-opacity-[var(--value)]', 63 | ], 64 | backgroundPosition: [ 65 | 'bg-{static}', 66 | ], 67 | backgroundSize: [ 68 | 'bg-{static}', 69 | ], 70 | blur: [ 71 | 'blur-{static}', 72 | 'blur-{int}', 73 | 'blur-{size}', 74 | 'blur-[15px]', 75 | ], 76 | borderColor: [ 77 | 'border-{color}', 78 | 'border-${var}', 79 | 'border-[#f00]', 80 | ], 81 | borderOpacity: [ 82 | 'border-opacity-{static}', 83 | 'border-opacity-{int}', 84 | 'border-opacity-${var}', 85 | ], 86 | borderRadius: [ 87 | 'rounded-{static}', 88 | 'rounded-t-{static}', 89 | 'rounded-l-{static}', 90 | 'rounded-r-{static}', 91 | 'rounded-b-{static}', 92 | 93 | 'rounded-${var}', 94 | 'rounded-t-${var}', 95 | 'rounded-l-${var}', 96 | 'rounded-r-${var}', 97 | 'rounded-b-${var}', 98 | 99 | 'rounded-[11px]', 100 | 'rounded-t-[var(--radius)]', 101 | 'rounded-r-[var(--radius)]', 102 | 'rounded-b-[var(--radius)]', 103 | 'rounded-l-[var(--radius)]', 104 | 'rounded-tr-[var(--radius)]', 105 | 'rounded-br-[var(--radius)]', 106 | 'rounded-bl-[var(--radius)]', 107 | 'rounded-tl-[var(--radius)]', 108 | 109 | 'rounded-tl-{static}', 110 | 'rounded-tr-{static}', 111 | 'rounded-br-{static}', 112 | 'rounded-bl-{static}', 113 | 114 | 'rounded-{nxl}', 115 | 'rounded-{size}', 116 | 'rounded-t-{nxl}', 117 | 'rounded-t-{size}', 118 | 'rounded-l-{nxl}', 119 | 'rounded-l-{size}', 120 | 'rounded-r-{nxl}', 121 | 'rounded-r-{size}', 122 | 'rounded-b-{nxl}', 123 | 'rounded-b-{size}', 124 | 'rounded-tl-{nxl}', 125 | 'rounded-tl-{size}', 126 | 'rounded-tr-{nxl}', 127 | 'rounded-tr-{size}', 128 | 'rounded-br-{nxl}', 129 | 'rounded-br-{size}', 130 | 'rounded-bl-{nxl}', 131 | 'rounded-bl-{size}', 132 | ], 133 | borderWidth: [ 134 | 'border-{static}', 135 | 'border-{int}', 136 | 'border-{size}', 137 | 'border-[2.5px]', 138 | 'border-t-{static}', 139 | 'border-t-{int}', 140 | 'border-t-{size}', 141 | 'border-r-{static}', 142 | 'border-r-{int}', 143 | 'border-r-{size}', 144 | 'border-b-{static}', 145 | 'border-b-{int}', 146 | 'border-b-{size}', 147 | 'border-l-{static}', 148 | 'border-l-{int}', 149 | 'border-l-{size}', 150 | 'border-width-${var}', 151 | 'border-t-width-${var}', 152 | 'border-l-width-${var}', 153 | 'border-r-width-${var}', 154 | 'border-b-width-${var}', 155 | ], 156 | boxShadow: [ 157 | 'shadow-{static}', 158 | ], 159 | boxShadowColor: [ 160 | 'shadow-{color}', 161 | 'shadow-${var}', 162 | ], 163 | brightness: [ 164 | 'brightness-{static}', 165 | 'brightness-[300%]', 166 | ], 167 | caretColor: [ 168 | 'caret-{color}', 169 | 'caret-${var}', 170 | ], 171 | caretOpacity: [ 172 | 'caret-opacity-{static}', 173 | 'caret-opacity-${var}', 174 | ], 175 | content: [ 176 | 'content-{static}', 177 | 'content-{string}', 178 | 'content-${var}', 179 | 'content-["👍"]', 180 | 'content-[attr(data-content)]', 181 | ], 182 | container: [ 183 | 'container', 184 | ], 185 | contrast: [ 186 | 'contrast-{static}', 187 | 'contrast-[2.4]', 188 | ], 189 | cursor: [ 190 | 'cursor-{static}', 191 | ], 192 | divideColor: [ 193 | 'divide-{color}', 194 | 'divide-${var}', 195 | ], 196 | divideOpacity: [ 197 | 'divide-opacity-{static}', 198 | 'divide-opacity-{int}', 199 | 'divide-opacity-${var}', 200 | ], 201 | divideWidth: [ 202 | 'divide-y-{static}', 203 | 'divide-x-{static}', 204 | 'divide-y-reverse', 205 | 'divide-x-reverse', 206 | 'divide-y-{int}', 207 | 'divide-x-{int}', 208 | 'divide-y-${var}', 209 | 'divide-x-${var}', 210 | ], 211 | dropShadow: [ 212 | 'drop-shadow-{static}', 213 | ], 214 | fill: [ 215 | 'fill-{color}', 216 | 'fill-${var}', 217 | 'fill-[#1c1c1e]', 218 | 'fill-[var(--color)]', 219 | ], 220 | flex: [ 221 | 'flex-{static}', 222 | 'flex-[var(--flex)]', 223 | ], 224 | flexGrow: [ 225 | 'flex-grow-{static}', 226 | 'flex-grow-[var(--grow)]', 227 | ], 228 | flexShrink: [ 229 | 'flex-shrink-{static}', 230 | 'flex-shrink-[var(--shrink)]', 231 | ], 232 | fontFamily: [ 233 | 'font-{static}', 234 | 'font-${var}', 235 | ], 236 | fontSize: [ 237 | 'text-{static}', 238 | 'text-{nxl}', 239 | 'text-size-${var}', 240 | 'text-[2.23rem]', 241 | ], 242 | fontWeight: [ 243 | 'font-{static}', 244 | 'font-{int}', 245 | ], 246 | gap: [ 247 | 'gap-{static}', 248 | 'gap-x-{static}', 249 | 'gap-y-{static}', 250 | 251 | 'gap-{float}', 252 | 'gap-x-{float}', 253 | 'gap-y-{float}', 254 | 255 | 'gap-{size}', 256 | 'gap-x-{size}', 257 | 'gap-y-{size}', 258 | 259 | 'gap-${var}', 260 | 'gap-x-${var}', 261 | 'gap-y-${var}', 262 | ], 263 | gradientColorStops: [ 264 | 'from-{color}', 265 | 'from-${var}', 266 | 'from-[#da5b66]', 267 | 'from-[var(--color)]', 268 | 'via-{color}', 269 | 'via-${var}', 270 | 'via-[#da5b66]', 271 | 'via-[var(--color)]', 272 | 'to-{color}', 273 | 'to-${var}', 274 | 'to-[#da5b66]', 275 | 'to-[var(--color)]', 276 | ], 277 | grayscale: [ 278 | 'grayscale-{static}', 279 | 'grayscale-[0.55]', 280 | ], 281 | gridAutoColumns: [ 282 | 'auto-cols-{static}', 283 | ], 284 | gridAutoRows: [ 285 | 'auto-rows-{static}', 286 | ], 287 | gridColumn: [ 288 | 'col-{static}', 289 | 'col-span-{int}', 290 | 'col-${var}', 291 | 'col-[7]', 292 | ], 293 | gridColumnEnd: [ 294 | 'col-end-{static}', 295 | 'col-end-{int}', 296 | 'col-end-${var}', 297 | 'col-end-[7]', 298 | ], 299 | gridColumnStart: [ 300 | 'col-start-{static}', 301 | 'col-start-{int}', 302 | 'col-start-${var}', 303 | 'col-start-[7]', 304 | ], 305 | gridRow: [ 306 | 'row-{static}', 307 | 'row-span-{int}', 308 | 'row-${var}', 309 | 'row-[7]', 310 | ], 311 | gridRowEnd: [ 312 | 'row-end-{static}', 313 | 'row-end-{int}', 314 | 'row-end-${var}', 315 | 'row-end-[7]', 316 | ], 317 | gridRowStart: [ 318 | 'row-start-{static}', 319 | 'row-start-{int}', 320 | 'row-start-${var}', 321 | 'row-start-[7]', 322 | ], 323 | gridTemplateColumns: [ 324 | 'grid-cols-{static}', 325 | 'grid-cols-{int}', 326 | 'grid-cols-${var}', 327 | 'grid-cols-[200px,repeat(auto-fill,minmax(15%,100px)),300px]', 328 | ], 329 | gridTemplateRows: [ 330 | 'grid-rows-{static}', 331 | 'grid-rows-{int}', 332 | 'grid-rows-${var}', 333 | 'grid-rows-[200px,repeat(auto-fill,minmax(15%,100px)),300px]', 334 | ], 335 | height: [ 336 | 'h-{static}', 337 | 'h-{float}', 338 | 'h-{fraction}', 339 | 'h-{nxl}', 340 | 'h-{size}', 341 | 'h-${var}', 342 | 'h-[3.23rem]', 343 | 'h-[calc(100%+1rem)]', 344 | 'h-[var(--width)]', 345 | ], 346 | hueRotate: [ 347 | 'hue-rotate-{static}', 348 | 'hue-rotate-[0.8turn]', 349 | ], 350 | inset: [ 351 | 'inset-{static}', 352 | 'inset-{float}', 353 | 'inset-{fraction}', 354 | 'inset-{size}', 355 | 'inset-${var}', 356 | 'inset-[11px]', 357 | 358 | 'inset-y-{static}', 359 | 'inset-y-{float}', 360 | 'inset-y-{fraction}', 361 | 'inset-y-{size}', 362 | 'inset-y-${var}', 363 | 364 | 'inset-x-{static}', 365 | 'inset-x-{float}', 366 | 'inset-x-{fraction}', 367 | 'inset-x-{size}', 368 | 'inset-x-${var}', 369 | 370 | 'top-{static}', 371 | 'top-{float}', 372 | 'top-{fraction}', 373 | 'top-{size}', 374 | 'top-${var}', 375 | 376 | 'right-{static}', 377 | 'right-{float}', 378 | 'right-{fraction}', 379 | 'right-{size}', 380 | 'right-${var}', 381 | 382 | 'bottom-{static}', 383 | 'bottom-{float}', 384 | 'bottom-{fraction}', 385 | 'bottom-{size}', 386 | 'bottom-${var}', 387 | 388 | 'left-{static}', 389 | 'left-{float}', 390 | 'left-{fraction}', 391 | 'left-{size}', 392 | 'left-${var}', 393 | ], 394 | invert: [ 395 | 'invert-{static}', 396 | 'invert-[0.75]', 397 | ], 398 | letterSpacing: [ 399 | 'tracking-{static}', 400 | 'tracking-{size}', 401 | 'tracking-${var}', 402 | 'tracking-[var(--tracking)]', 403 | ], 404 | lineHeight: [ 405 | 'leading-{static}', 406 | 'leading-{int}', 407 | 'leading-{size}', 408 | 'leading-${var}', 409 | 'leading-[var(--leading)]', 410 | ], 411 | listStyleType: [ 412 | 'list-{static}', 413 | ], 414 | margin: [ 415 | 'm-{static}', 416 | 'my-{static}', 417 | 'mx-{static}', 418 | 'mt-{static}', 419 | 'mr-{static}', 420 | 'mb-{static}', 421 | 'ml-{static}', 422 | 423 | 'm-{float}', 424 | 'my-{float}', 425 | 'mx-{float}', 426 | 'mt-{float}', 427 | 'mr-{float}', 428 | 'mb-{float}', 429 | 'ml-{float}', 430 | 431 | 'm-{size}', 432 | 'my-{size}', 433 | 'mx-{size}', 434 | 'mt-{size}', 435 | 'mr-{size}', 436 | 'mb-{size}', 437 | 'ml-{size}', 438 | 439 | 'm-${var}', 440 | 'my-${var}', 441 | 'mx-${var}', 442 | 'mt-${var}', 443 | 'mr-${var}', 444 | 'mb-${var}', 445 | 'ml-${var}', 446 | 447 | 'm-[7px]', 448 | 'my-[7px]', 449 | 'mx-[7px]', 450 | 'mt-[7px]', 451 | 'mr-[7px]', 452 | 'mb-[7px]', 453 | 'ml-[7px]', 454 | 'mt-[clamp(30px,100px)]', 455 | ], 456 | maxHeight: [ 457 | 'max-h-{static}', 458 | 'max-h-{float}', 459 | 'max-h-{fraction}', 460 | 'max-h-{nxl}', 461 | 'max-h-{size}', 462 | 'max-h-${var}', 463 | 'max-h-[3.23rem]', 464 | 'max-h-[calc(100%+1rem)]', 465 | 'max-h-[var(--width)]', 466 | ], 467 | maxWidth: [ 468 | 'max-w-{static}', 469 | 'max-w-{float}', 470 | 'max-w-{fraction}', 471 | 'max-w-{nxl}', 472 | 'max-w-{size}', 473 | 'max-w-${var}', 474 | 'max-w-[3.23rem]', 475 | 'max-w-[calc(100%+1rem)]', 476 | 'max-w-[var(--width)]', 477 | ], 478 | minHeight: [ 479 | 'min-h-{static}', 480 | 'min-h-{float}', 481 | 'min-h-{fraction}', 482 | 'min-h-{nxl}', 483 | 'min-h-{size}', 484 | 'min-h-${var}', 485 | 'min-h-[3.23rem]', 486 | 'min-h-[calc(100%+1rem)]', 487 | 'min-h-[var(--width)]', 488 | ], 489 | minWidth: [ 490 | 'min-w-{static}', 491 | 'min-w-{float}', 492 | 'min-w-{fraction}', 493 | 'min-w-{nxl}', 494 | 'min-w-{size}', 495 | 'min-w-${var}', 496 | 'min-w-[3.23rem]', 497 | 'min-w-[calc(100%+1rem)]', 498 | 'min-w-[var(--width)]', 499 | ], 500 | objectPosition: [ 501 | 'object-{static}', 502 | ], 503 | opacity: [ 504 | 'opacity-{static}', 505 | 'opacity-{int}', 506 | 'opacity-${var}', 507 | 'opacity-[var(--opacity)]', 508 | ], 509 | order: [ 510 | 'order-{static}', 511 | 'order-{int}', 512 | 'order-${var}', 513 | ], 514 | outline: [ 515 | 'outline-{static}', 516 | 'outline-[var(--outline)]', 517 | ], 518 | outlineColor: [ 519 | 'outline-{color}', 520 | 'outline-${var}', 521 | 'outline-solid-{color}', 522 | 'outline-dotted-{color}', 523 | ], 524 | padding: [ 525 | 'p-{static}', 526 | 'p-[var(--app-padding)]', 527 | 528 | 'py-{static}', 529 | 'px-{static}', 530 | 'pt-{static}', 531 | 'pr-{static}', 532 | 'pb-{static}', 533 | 'pl-{static}', 534 | 535 | 'p-{float}', 536 | 'py-{float}', 537 | 'px-{float}', 538 | 'pt-{float}', 539 | 'pr-{float}', 540 | 'pb-{float}', 541 | 'pl-{float}', 542 | 543 | 'p-{size}', 544 | 'py-{size}', 545 | 'px-{size}', 546 | 'pt-{size}', 547 | 'pr-{size}', 548 | 'pb-{size}', 549 | 'pl-{size}', 550 | 551 | 'p-${var}', 552 | 'py-${var}', 553 | 'px-${var}', 554 | 'pt-${var}', 555 | 'pr-${var}', 556 | 'pb-${var}', 557 | 'pl-${var}', 558 | ], 559 | perspective: [ 560 | 'perspect-{static}', 561 | ], 562 | perspectiveOrigin: [ 563 | 'perspect-origin-{static}', 564 | ], 565 | placeholderColor: [ 566 | 'placeholder-{color}', 567 | 'placeholder-${var}', 568 | 'placeholder-[var(--placeholder)]', 569 | ], 570 | placeholderOpacity: [ 571 | 'placeholder-opacity-{static}', 572 | 'placeholder-opacity-{int}', 573 | 'placeholder-opacity-${var}', 574 | 'placeholder-opacity-[var(--placeholder)]', 575 | ], 576 | ringColor: [ 577 | 'ring-{color}', 578 | 'ring-${var}', 579 | 'ring-[#76ad65]', 580 | ], 581 | ringOffsetColor: [ 582 | 'ring-offset-{color}', 583 | 'ring-offset-${var}', 584 | 'ring-offset-[#76ad65]', 585 | ], 586 | ringOffsetWidth: [ 587 | 'ring-offset-{static}', 588 | 'ring-offset-{int}', 589 | 'ring-offset-${var}', 590 | 'ring-offset-[10px]', 591 | ], 592 | ringOpacity: [ 593 | 'ring-opacity-{static}', 594 | 'ring-opacity-{int}', 595 | 'ring-opacity-${var}', 596 | 'ring-opacity-[var(--ring-opacity)]', 597 | ], 598 | ringWidth: [ 599 | 'ring-inset', 600 | 'ring-{static}', 601 | 'ring-{int}', 602 | 'ring-width-${var}', 603 | 'ring-[10px]', 604 | ], 605 | rotate: [ 606 | 'rotate-{static}', 607 | 'rotate-{float}', 608 | 'rotate-${var}', 609 | 'rotate-x-{static}', 610 | 'rotate-x-{float}', 611 | 'rotate-x-${var}', 612 | 'rotate-y-{static}', 613 | 'rotate-y-{float}', 614 | 'rotate-y-${var}', 615 | 'rotate-z-{static}', 616 | 'rotate-z-{float}', 617 | 'rotate-z-${var}', 618 | 'rotate-[23deg]', 619 | 'rotate-[2.3rad]', 620 | 'rotate-[401grad]', 621 | 'rotate-[1.5turn]', 622 | ], 623 | saturate: [ 624 | 'saturate-{static}', 625 | 'saturate-[180%]', 626 | ], 627 | scale: [ 628 | 'scale-{static}', 629 | 'scale-{int}', 630 | 'scale-${var}', 631 | 'scale-x-{static}', 632 | 'scale-x-{int}', 633 | 'scale-x-${var}', 634 | 'scale-y-{static}', 635 | 'scale-y-{int}', 636 | 'scale-y-${var}', 637 | 'scale-z-{static}', 638 | 'scale-z-{int}', 639 | 'scale-z-${var}', 640 | ], 641 | sepia: [ 642 | 'sepia-{static}', 643 | 'sepia-[0.2]', 644 | ], 645 | skew: [ 646 | 'skew-x-{static}', 647 | 'skew-x-{float}', 648 | 'skew-x-${var}', 649 | 'skew-x-[3px]', 650 | 651 | 'skew-y-{static}', 652 | 'skew-y-{float}', 653 | 'skew-y-${var}', 654 | 'skew-y-[3px]', 655 | ], 656 | space: [ 657 | 'space-y-{static}', 658 | 'space-y-reverse', 659 | 'space-x-{static}', 660 | 'space-x-reverse', 661 | 'space-y-{float}', 662 | 'space-x-{float}', 663 | 'space-y-${var}', 664 | 'space-x-${var}', 665 | 'space-x-[20cm]', 666 | 'space-x-[calc(20%-1cm)]', 667 | ], 668 | stroke: [ 669 | 'stroke-{color}', 670 | 'stroke-${var}', 671 | 'stroke-[#da5b66]', 672 | ], 673 | strokeWidth: [ 674 | 'stroke-{static}', 675 | 'stroke-{int}', 676 | 'stroke-width-${var}', 677 | ], 678 | strokeDashArray: [ 679 | 'stroke-dash-{static}', 680 | 'stroke-dash-{int}', 681 | ], 682 | strokeDashOffset: [ 683 | 'stroke-offset-{static}', 684 | 'stroke-offset-{int}', 685 | ], 686 | tabSize: [ 687 | 'tab-{static}', 688 | ], 689 | textColor: [ 690 | 'text-{color}', 691 | 'text-${var}', 692 | ], 693 | textOpacity: [ 694 | 'text-opacity-{static}', 695 | 'text-opacity-{int}', 696 | 'text-opacity-${var}', 697 | ], 698 | textShadow: [ 699 | 'text-shadow-{static}', 700 | ], 701 | textDecorationColor: [ 702 | 'underline-{color}', 703 | 'underline-${var}', 704 | ], 705 | textDecorationOpacity: [ 706 | 'underline-opacity-{static}', 707 | 'underline-opacity-{int}', 708 | 'underline-opacity-${var}', 709 | ], 710 | textDecorationLength: [ 711 | 'underline-{static}', 712 | 'underline-{int}', 713 | 'underline-{size}', 714 | ], 715 | textDecorationOffset: [ 716 | 'underline-offset-{static}', 717 | 'underline-offset-{int}', 718 | 'underline-offset-{size}', 719 | ], 720 | textIndent: [ 721 | 'indent-{static}', 722 | 'indent-{size}', 723 | ], 724 | textStrokeColor: [ 725 | 'text-stroke-{color}', 726 | 'text-stroke-${var}', 727 | ], 728 | textStrokeWidth: [ 729 | 'text-stroke-{static}', 730 | 'text-stroke-{size}', 731 | ], 732 | transformOrigin: [ 733 | 'origin-{static}', 734 | ], 735 | transitionDuration: [ 736 | 'duration-{static}', 737 | 'duration-{int}', 738 | 'duration-${var}', 739 | 'duration-[2s]', 740 | 'duration-[var(--app-duration)]', 741 | ], 742 | transitionDelay: [ 743 | 'delay-{static}', 744 | 'delay-{int}', 745 | 'delay-${var}', 746 | 'delay-[var(--delay)]', 747 | ], 748 | transitionProperty: [ 749 | 'transition-{static}', 750 | ], 751 | transitionTimingFunction: [ 752 | 'ease-{static}', 753 | ], 754 | translate: [ 755 | 'translate-{static}', 756 | 'translate-{float}', 757 | 'translate-{fraction}', 758 | 'translate-{size}', 759 | 'translate-${var}', 760 | 761 | 'translate-x-{static}', 762 | 'translate-x-{float}', 763 | 'translate-x-{fraction}', 764 | 'translate-x-{size}', 765 | 'translate-x-${var}', 766 | 767 | 'translate-y-{static}', 768 | 'translate-y-{float}', 769 | 'translate-y-{fraction}', 770 | 'translate-y-{size}', 771 | 'translate-y-${var}', 772 | 773 | 'translate-z-{static}', 774 | 'translate-z-{float}', 775 | 'translate-z-{fraction}', 776 | 'translate-z-{size}', 777 | 'translate-z-${var}', 778 | ], 779 | width: [ 780 | 'w-{static}', 781 | 'w-{float}', 782 | 'w-{fraction}', 783 | 'w-{nxl}', 784 | 'w-{size}', 785 | 'w-${var}', 786 | 'w-[3.23rem]', 787 | 'w-[calc(100%+1rem)]', 788 | 'w-[calc(var(--10-10px,calc(-20px-(-30px--40px)))-50px)]', 789 | 'w-[var(--width)]', 790 | 'w-[var(--width,calc(100%+1rem))]', 791 | 'w-[calc(100%/3-1rem*2)]', 792 | ], 793 | zIndex: [ 794 | 'z-{static}', 795 | 'z-{int}', 796 | 'z-${var}', 797 | ], 798 | } 799 | 800 | const negative: { [key: string]: true} = { 801 | inset: true, 802 | zIndex: true, 803 | order: true, 804 | margin: true, 805 | space: true, 806 | letterSpacing: true, 807 | rotate: true, 808 | translate: true, 809 | skew: true, 810 | } 811 | 812 | export { utilities, negative } 813 | -------------------------------------------------------------------------------- /src/monaco/languages/html/languageFeatures.ts: -------------------------------------------------------------------------------- 1 | /* --------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *-------------------------------------------------------------------------------------------- */ 5 | 6 | import * as htmlService from 'vscode-html-languageservice' 7 | import { InsertReplaceEdit, TextEdit } from 'vscode-html-languageservice' 8 | import { LanguageServiceDefaults } from './monaco.contribution' 9 | import type { HTMLWorker } from './htmlWorker' 10 | import { 11 | languages, 12 | editor, 13 | Uri, 14 | Position, 15 | Range, 16 | CancellationToken, 17 | IDisposable, 18 | MarkerSeverity, 19 | IMarkdownString, 20 | } from './fillers/monaco-editor-core' 21 | 22 | export interface WorkerAccessor { 23 | (...more: Uri[]): Promise 24 | } 25 | 26 | // --- diagnostics --- --- 27 | 28 | export class DiagnosticsAdapter { 29 | private _disposables: IDisposable[] = [] 30 | private _listener: { [uri: string]: IDisposable } = Object.create(null) 31 | 32 | constructor( 33 | private _languageId: string, 34 | private _worker: WorkerAccessor, 35 | defaults: LanguageServiceDefaults, 36 | ) { 37 | const onModelAdd = (model: editor.IModel): void => { 38 | const modeId = model.getModeId() 39 | if (modeId !== this._languageId) 40 | return 41 | 42 | let handle: number 43 | this._listener[model.uri.toString()] = model.onDidChangeContent(() => { 44 | clearTimeout(handle) 45 | handle = setTimeout(() => this._doValidate(model.uri, modeId), 500) 46 | }) 47 | 48 | this._doValidate(model.uri, modeId) 49 | } 50 | 51 | const onModelRemoved = (model: editor.IModel): void => { 52 | editor.setModelMarkers(model, this._languageId, []) 53 | const uriStr = model.uri.toString() 54 | const listener = this._listener[uriStr] 55 | if (listener) { 56 | listener.dispose() 57 | delete this._listener[uriStr] 58 | } 59 | } 60 | 61 | this._disposables.push(editor.onDidCreateModel(onModelAdd)) 62 | this._disposables.push( 63 | editor.onWillDisposeModel((model) => { 64 | onModelRemoved(model) 65 | }), 66 | ) 67 | this._disposables.push( 68 | editor.onDidChangeModelLanguage((event) => { 69 | onModelRemoved(event.model) 70 | onModelAdd(event.model) 71 | }), 72 | ) 73 | 74 | this._disposables.push( 75 | defaults.onDidChange((_) => { 76 | editor.getModels().forEach((model) => { 77 | if (model.getModeId() === this._languageId) { 78 | onModelRemoved(model) 79 | onModelAdd(model) 80 | } 81 | }) 82 | }), 83 | ) 84 | 85 | this._disposables.push({ 86 | dispose: () => { 87 | for (const key in this._listener) 88 | this._listener[key].dispose() 89 | }, 90 | }) 91 | 92 | editor.getModels().forEach(onModelAdd) 93 | } 94 | 95 | public dispose(): void { 96 | this._disposables.forEach(d => d && d.dispose()) 97 | this._disposables = [] 98 | } 99 | 100 | private _doValidate(resource: Uri, languageId: string): void { 101 | this._worker(resource) 102 | .then((worker) => { 103 | return worker.doValidation(resource.toString()).then((diagnostics) => { 104 | const markers = diagnostics.map(d => toDiagnostics(resource, d)) 105 | const model = editor.getModel(resource) 106 | if (model && model.getModeId() === languageId) 107 | editor.setModelMarkers(model, languageId, markers) 108 | }) 109 | }) 110 | .then(undefined, (err) => { 111 | console.error(err) 112 | }) 113 | } 114 | } 115 | 116 | function toSeverity(lsSeverity: number): MarkerSeverity { 117 | switch (lsSeverity) { 118 | case htmlService.DiagnosticSeverity.Error: 119 | return MarkerSeverity.Error 120 | case htmlService.DiagnosticSeverity.Warning: 121 | return MarkerSeverity.Warning 122 | case htmlService.DiagnosticSeverity.Information: 123 | return MarkerSeverity.Info 124 | case htmlService.DiagnosticSeverity.Hint: 125 | return MarkerSeverity.Hint 126 | default: 127 | return MarkerSeverity.Info 128 | } 129 | } 130 | 131 | function toDiagnostics(resource: Uri, diag: htmlService.Diagnostic): editor.IMarkerData { 132 | const code = typeof diag.code === 'number' ? String(diag.code) : diag.code 133 | 134 | return { 135 | severity: toSeverity(diag.severity), 136 | startLineNumber: diag.range.start.line + 1, 137 | startColumn: diag.range.start.character + 1, 138 | endLineNumber: diag.range.end.line + 1, 139 | endColumn: diag.range.end.character + 1, 140 | message: diag.message, 141 | code, 142 | source: diag.source, 143 | } 144 | } 145 | 146 | // --- completion ------ 147 | 148 | function fromPosition(position: Position): htmlService.Position { 149 | if (!position) 150 | return void 0 151 | 152 | return { character: position.column - 1, line: position.lineNumber - 1 } 153 | } 154 | 155 | function fromRange(range: Range): htmlService.Range { 156 | if (!range) 157 | return void 0 158 | 159 | return { 160 | start: fromPosition(range.getStartPosition()), 161 | end: fromPosition(range.getEndPosition()), 162 | } 163 | } 164 | 165 | function toRange(range: htmlService.Range): Range { 166 | if (!range) 167 | return void 0 168 | 169 | return new Range( 170 | range.start.line + 1, 171 | range.start.character + 1, 172 | range.end.line + 1, 173 | range.end.character + 1, 174 | ) 175 | } 176 | 177 | function isInsertReplaceEdit(edit: TextEdit | InsertReplaceEdit): edit is InsertReplaceEdit { 178 | return ( 179 | typeof (edit).insert !== 'undefined' 180 | && typeof (edit).replace !== 'undefined' 181 | ) 182 | } 183 | 184 | function toCompletionItemKind(kind: number): languages.CompletionItemKind { 185 | const mItemKind = languages.CompletionItemKind 186 | 187 | switch (kind) { 188 | case htmlService.CompletionItemKind.Text: 189 | return mItemKind.Text 190 | case htmlService.CompletionItemKind.Method: 191 | return mItemKind.Method 192 | case htmlService.CompletionItemKind.Function: 193 | return mItemKind.Function 194 | case htmlService.CompletionItemKind.Constructor: 195 | return mItemKind.Constructor 196 | case htmlService.CompletionItemKind.Field: 197 | return mItemKind.Field 198 | case htmlService.CompletionItemKind.Variable: 199 | return mItemKind.Variable 200 | case htmlService.CompletionItemKind.Class: 201 | return mItemKind.Class 202 | case htmlService.CompletionItemKind.Interface: 203 | return mItemKind.Interface 204 | case htmlService.CompletionItemKind.Module: 205 | return mItemKind.Module 206 | case htmlService.CompletionItemKind.Property: 207 | return mItemKind.Property 208 | case htmlService.CompletionItemKind.Unit: 209 | return mItemKind.Unit 210 | case htmlService.CompletionItemKind.Value: 211 | return mItemKind.Value 212 | case htmlService.CompletionItemKind.Enum: 213 | return mItemKind.Enum 214 | case htmlService.CompletionItemKind.Keyword: 215 | return mItemKind.Keyword 216 | case htmlService.CompletionItemKind.Snippet: 217 | return mItemKind.Snippet 218 | case htmlService.CompletionItemKind.Color: 219 | return mItemKind.Color 220 | case htmlService.CompletionItemKind.File: 221 | return mItemKind.File 222 | case htmlService.CompletionItemKind.Reference: 223 | return mItemKind.Reference 224 | } 225 | return mItemKind.Property 226 | } 227 | 228 | function fromCompletionItemKind( 229 | kind: languages.CompletionItemKind, 230 | ): htmlService.CompletionItemKind { 231 | const mItemKind = languages.CompletionItemKind 232 | 233 | switch (kind) { 234 | case mItemKind.Text: 235 | return htmlService.CompletionItemKind.Text 236 | case mItemKind.Method: 237 | return htmlService.CompletionItemKind.Method 238 | case mItemKind.Function: 239 | return htmlService.CompletionItemKind.Function 240 | case mItemKind.Constructor: 241 | return htmlService.CompletionItemKind.Constructor 242 | case mItemKind.Field: 243 | return htmlService.CompletionItemKind.Field 244 | case mItemKind.Variable: 245 | return htmlService.CompletionItemKind.Variable 246 | case mItemKind.Class: 247 | return htmlService.CompletionItemKind.Class 248 | case mItemKind.Interface: 249 | return htmlService.CompletionItemKind.Interface 250 | case mItemKind.Module: 251 | return htmlService.CompletionItemKind.Module 252 | case mItemKind.Property: 253 | return htmlService.CompletionItemKind.Property 254 | case mItemKind.Unit: 255 | return htmlService.CompletionItemKind.Unit 256 | case mItemKind.Value: 257 | return htmlService.CompletionItemKind.Value 258 | case mItemKind.Enum: 259 | return htmlService.CompletionItemKind.Enum 260 | case mItemKind.Keyword: 261 | return htmlService.CompletionItemKind.Keyword 262 | case mItemKind.Snippet: 263 | return htmlService.CompletionItemKind.Snippet 264 | case mItemKind.Color: 265 | return htmlService.CompletionItemKind.Color 266 | case mItemKind.File: 267 | return htmlService.CompletionItemKind.File 268 | case mItemKind.Reference: 269 | return htmlService.CompletionItemKind.Reference 270 | } 271 | return htmlService.CompletionItemKind.Property 272 | } 273 | 274 | function toTextEdit(textEdit: htmlService.TextEdit): editor.ISingleEditOperation { 275 | if (!textEdit) 276 | return void 0 277 | 278 | return { 279 | range: toRange(textEdit.range), 280 | text: textEdit.newText, 281 | } 282 | } 283 | 284 | export class CompletionAdapter implements languages.CompletionItemProvider { 285 | constructor(private _worker: WorkerAccessor) {} 286 | 287 | public get triggerCharacters(): string[] { 288 | return ['.', ':', '<', '"', '=', '/', '@'] 289 | } 290 | 291 | provideCompletionItems( 292 | model: editor.IReadOnlyModel, 293 | position: Position, 294 | context: languages.CompletionContext, 295 | token: CancellationToken, 296 | ): Promise { 297 | const resource = model.uri 298 | 299 | return this._worker(resource) 300 | .then((worker) => { 301 | return worker.doComplete(resource.toString(), fromPosition(position)) 302 | }) 303 | .then((info) => { 304 | if (!info) 305 | return 306 | 307 | const wordInfo = model.getWordUntilPosition(position) 308 | const wordRange = new Range( 309 | position.lineNumber, 310 | wordInfo.startColumn, 311 | position.lineNumber, 312 | wordInfo.endColumn, 313 | ) 314 | 315 | const items: languages.CompletionItem[] = info.items.map((entry) => { 316 | const item: languages.CompletionItem = { 317 | label: entry.label, 318 | insertText: entry.insertText || entry.label, 319 | sortText: entry.sortText, 320 | filterText: entry.filterText, 321 | documentation: entry.documentation, 322 | detail: entry.detail, 323 | range: wordRange, 324 | kind: toCompletionItemKind(entry.kind), 325 | } 326 | if (entry.textEdit) { 327 | if (isInsertReplaceEdit(entry.textEdit)) { 328 | item.range = { 329 | insert: toRange(entry.textEdit.insert), 330 | replace: toRange(entry.textEdit.replace), 331 | } 332 | } 333 | else { 334 | item.range = toRange(entry.textEdit.range) 335 | } 336 | item.insertText = entry.textEdit.newText 337 | } 338 | if (entry.additionalTextEdits) 339 | item.additionalTextEdits = entry.additionalTextEdits.map(toTextEdit) 340 | 341 | if (entry.insertTextFormat === htmlService.InsertTextFormat.Snippet) 342 | item.insertTextRules = languages.CompletionItemInsertTextRule.InsertAsSnippet 343 | 344 | return item 345 | }) 346 | 347 | return { 348 | isIncomplete: info.isIncomplete, 349 | suggestions: items, 350 | } 351 | }) 352 | } 353 | } 354 | 355 | // --- hover ------ 356 | 357 | function isMarkupContent(thing: any): thing is htmlService.MarkupContent { 358 | return ( 359 | thing 360 | && typeof thing === 'object' 361 | && typeof (thing).kind === 'string' 362 | ) 363 | } 364 | 365 | function toMarkdownString( 366 | entry: htmlService.MarkupContent | htmlService.MarkedString, 367 | ): IMarkdownString { 368 | if (typeof entry === 'string') { 369 | return { 370 | value: entry, 371 | } 372 | } 373 | if (isMarkupContent(entry)) { 374 | if (entry.kind === 'plaintext') { 375 | return { 376 | value: entry.value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'), 377 | } 378 | } 379 | return { 380 | value: entry.value, 381 | } 382 | } 383 | 384 | return { value: `\`\`\`${entry.language}\n${entry.value}\n\`\`\`\n` } 385 | } 386 | 387 | function toMarkedStringArray( 388 | contents: htmlService.MarkupContent | htmlService.MarkedString | htmlService.MarkedString[], 389 | ): IMarkdownString[] { 390 | if (!contents) 391 | return void 0 392 | 393 | if (Array.isArray(contents)) 394 | return contents.map(toMarkdownString) 395 | 396 | return [toMarkdownString(contents)] 397 | } 398 | 399 | export class HoverAdapter implements languages.HoverProvider { 400 | constructor(private _worker: WorkerAccessor) {} 401 | 402 | provideHover( 403 | model: editor.IReadOnlyModel, 404 | position: Position, 405 | token: CancellationToken, 406 | ): Promise { 407 | const resource = model.uri 408 | 409 | return this._worker(resource) 410 | .then((worker) => { 411 | return worker.doHover(resource.toString(), fromPosition(position)) 412 | }) 413 | .then((info) => { 414 | if (!info) 415 | return 416 | 417 | return { 418 | range: toRange(info.range), 419 | contents: toMarkedStringArray(info.contents), 420 | } 421 | }) 422 | } 423 | } 424 | 425 | // --- document highlights ------ 426 | 427 | function toHighlighKind(kind: htmlService.DocumentHighlightKind): languages.DocumentHighlightKind { 428 | const mKind = languages.DocumentHighlightKind 429 | 430 | switch (kind) { 431 | case htmlService.DocumentHighlightKind.Read: 432 | return mKind.Read 433 | case htmlService.DocumentHighlightKind.Write: 434 | return mKind.Write 435 | case htmlService.DocumentHighlightKind.Text: 436 | return mKind.Text 437 | } 438 | return mKind.Text 439 | } 440 | 441 | export class DocumentHighlightAdapter implements languages.DocumentHighlightProvider { 442 | constructor(private _worker: WorkerAccessor) {} 443 | 444 | public provideDocumentHighlights( 445 | model: editor.IReadOnlyModel, 446 | position: Position, 447 | token: CancellationToken, 448 | ): Promise { 449 | const resource = model.uri 450 | 451 | return this._worker(resource) 452 | .then(worker => worker.findDocumentHighlights(resource.toString(), fromPosition(position))) 453 | .then((items) => { 454 | if (!items) 455 | return 456 | 457 | return items.map(item => ({ 458 | range: toRange(item.range), 459 | kind: toHighlighKind(item.kind), 460 | })) 461 | }) 462 | } 463 | } 464 | 465 | // --- document symbols ------ 466 | 467 | function toSymbolKind(kind: htmlService.SymbolKind): languages.SymbolKind { 468 | const mKind = languages.SymbolKind 469 | 470 | switch (kind) { 471 | case htmlService.SymbolKind.File: 472 | return mKind.Array 473 | case htmlService.SymbolKind.Module: 474 | return mKind.Module 475 | case htmlService.SymbolKind.Namespace: 476 | return mKind.Namespace 477 | case htmlService.SymbolKind.Package: 478 | return mKind.Package 479 | case htmlService.SymbolKind.Class: 480 | return mKind.Class 481 | case htmlService.SymbolKind.Method: 482 | return mKind.Method 483 | case htmlService.SymbolKind.Property: 484 | return mKind.Property 485 | case htmlService.SymbolKind.Field: 486 | return mKind.Field 487 | case htmlService.SymbolKind.Constructor: 488 | return mKind.Constructor 489 | case htmlService.SymbolKind.Enum: 490 | return mKind.Enum 491 | case htmlService.SymbolKind.Interface: 492 | return mKind.Interface 493 | case htmlService.SymbolKind.Function: 494 | return mKind.Function 495 | case htmlService.SymbolKind.Variable: 496 | return mKind.Variable 497 | case htmlService.SymbolKind.Constant: 498 | return mKind.Constant 499 | case htmlService.SymbolKind.String: 500 | return mKind.String 501 | case htmlService.SymbolKind.Number: 502 | return mKind.Number 503 | case htmlService.SymbolKind.Boolean: 504 | return mKind.Boolean 505 | case htmlService.SymbolKind.Array: 506 | return mKind.Array 507 | } 508 | return mKind.Function 509 | } 510 | 511 | export class DocumentSymbolAdapter implements languages.DocumentSymbolProvider { 512 | constructor(private _worker: WorkerAccessor) {} 513 | 514 | public provideDocumentSymbols( 515 | model: editor.IReadOnlyModel, 516 | token: CancellationToken, 517 | ): Promise { 518 | const resource = model.uri 519 | 520 | return this._worker(resource) 521 | .then(worker => worker.findDocumentSymbols(resource.toString())) 522 | .then((items) => { 523 | if (!items) 524 | return 525 | 526 | return items.map(item => ({ 527 | name: item.name, 528 | detail: '', 529 | containerName: item.containerName, 530 | kind: toSymbolKind(item.kind), 531 | tags: [], 532 | range: toRange(item.location.range), 533 | selectionRange: toRange(item.location.range), 534 | })) 535 | }) 536 | } 537 | } 538 | 539 | export class DocumentLinkAdapter implements languages.LinkProvider { 540 | constructor(private _worker: WorkerAccessor) {} 541 | 542 | public provideLinks( 543 | model: editor.IReadOnlyModel, 544 | token: CancellationToken, 545 | ): Promise { 546 | const resource = model.uri 547 | 548 | return this._worker(resource) 549 | .then(worker => worker.findDocumentLinks(resource.toString())) 550 | .then((items) => { 551 | if (!items) 552 | return 553 | 554 | return { 555 | links: items.map(item => ({ 556 | range: toRange(item.range), 557 | url: item.target, 558 | })), 559 | } 560 | }) 561 | } 562 | } 563 | 564 | function fromFormattingOptions( 565 | options: languages.FormattingOptions, 566 | ): htmlService.FormattingOptions { 567 | return { 568 | tabSize: options.tabSize, 569 | insertSpaces: options.insertSpaces, 570 | } 571 | } 572 | 573 | export class DocumentFormattingEditProvider implements languages.DocumentFormattingEditProvider { 574 | constructor(private _worker: WorkerAccessor) {} 575 | 576 | public provideDocumentFormattingEdits( 577 | model: editor.IReadOnlyModel, 578 | options: languages.FormattingOptions, 579 | token: CancellationToken, 580 | ): Promise { 581 | const resource = model.uri 582 | 583 | return this._worker(resource).then((worker) => { 584 | return worker 585 | .format(resource.toString(), null, fromFormattingOptions(options)) 586 | .then((edits) => { 587 | if (!edits || edits.length === 0) 588 | return 589 | 590 | return edits.map(toTextEdit) 591 | }) 592 | }) 593 | } 594 | } 595 | 596 | export class DocumentRangeFormattingEditProvider 597 | implements languages.DocumentRangeFormattingEditProvider { 598 | constructor(private _worker: WorkerAccessor) {} 599 | 600 | public provideDocumentRangeFormattingEdits( 601 | model: editor.IReadOnlyModel, 602 | range: Range, 603 | options: languages.FormattingOptions, 604 | token: CancellationToken, 605 | ): Promise { 606 | const resource = model.uri 607 | 608 | return this._worker(resource).then((worker) => { 609 | return worker 610 | .format(resource.toString(), fromRange(range), fromFormattingOptions(options)) 611 | .then((edits) => { 612 | if (!edits || edits.length === 0) 613 | return 614 | 615 | return edits.map(toTextEdit) 616 | }) 617 | }) 618 | } 619 | } 620 | 621 | export class RenameAdapter implements languages.RenameProvider { 622 | constructor(private _worker: WorkerAccessor) {} 623 | 624 | provideRenameEdits( 625 | model: editor.IReadOnlyModel, 626 | position: Position, 627 | newName: string, 628 | token: CancellationToken, 629 | ): Promise { 630 | const resource = model.uri 631 | 632 | return this._worker(resource) 633 | .then((worker) => { 634 | return worker.doRename(resource.toString(), fromPosition(position), newName) 635 | }) 636 | .then((edit) => { 637 | return toWorkspaceEdit(edit) 638 | }) 639 | } 640 | } 641 | 642 | function toWorkspaceEdit(edit: htmlService.WorkspaceEdit): languages.WorkspaceEdit { 643 | if (!edit || !edit.changes) 644 | return void 0 645 | 646 | const resourceEdits: languages.WorkspaceTextEdit[] = [] 647 | for (const uri in edit.changes) { 648 | const _uri = Uri.parse(uri) 649 | for (const e of edit.changes[uri]) { 650 | resourceEdits.push({ 651 | resource: _uri, 652 | edit: { 653 | range: toRange(e.range), 654 | text: e.newText, 655 | }, 656 | }) 657 | } 658 | } 659 | return { 660 | edits: resourceEdits, 661 | } 662 | } 663 | 664 | export class FoldingRangeAdapter implements languages.FoldingRangeProvider { 665 | constructor(private _worker: WorkerAccessor) {} 666 | 667 | public provideFoldingRanges( 668 | model: editor.IReadOnlyModel, 669 | context: languages.FoldingContext, 670 | token: CancellationToken, 671 | ): Promise { 672 | const resource = model.uri 673 | 674 | return this._worker(resource) 675 | .then(worker => worker.getFoldingRanges(resource.toString(), context)) 676 | .then((ranges) => { 677 | if (!ranges) 678 | return 679 | 680 | return ranges.map((range) => { 681 | const result: languages.FoldingRange = { 682 | start: range.startLine + 1, 683 | end: range.endLine + 1, 684 | } 685 | if (typeof range.kind !== 'undefined') 686 | result.kind = toFoldingRangeKind(range.kind) 687 | 688 | return result 689 | }) 690 | }) 691 | } 692 | } 693 | 694 | function toFoldingRangeKind(kind: htmlService.FoldingRangeKind): languages.FoldingRangeKind { 695 | switch (kind) { 696 | case htmlService.FoldingRangeKind.Comment: 697 | return languages.FoldingRangeKind.Comment 698 | case htmlService.FoldingRangeKind.Imports: 699 | return languages.FoldingRangeKind.Imports 700 | case htmlService.FoldingRangeKind.Region: 701 | return languages.FoldingRangeKind.Region 702 | } 703 | } 704 | 705 | export class SelectionRangeAdapter implements languages.SelectionRangeProvider { 706 | constructor(private _worker: WorkerAccessor) {} 707 | 708 | public provideSelectionRanges( 709 | model: editor.IReadOnlyModel, 710 | positions: Position[], 711 | token: CancellationToken, 712 | ): Promise { 713 | const resource = model.uri 714 | 715 | return this._worker(resource) 716 | .then(worker => worker.getSelectionRanges(resource.toString(), positions.map(fromPosition))) 717 | .then((selectionRanges) => { 718 | if (!selectionRanges) 719 | return 720 | 721 | return selectionRanges.map((selectionRange) => { 722 | const result: languages.SelectionRange[] = [] 723 | while (selectionRange) { 724 | result.push({ range: toRange(selectionRange.range) }) 725 | selectionRange = selectionRange.parent 726 | } 727 | return result 728 | }) 729 | }) 730 | } 731 | } 732 | --------------------------------------------------------------------------------