diff --git a/src/extension.ts b/src/extension.ts
index fd552df0330b3610650c27f8cfbc485a0fb6f18f..c4598ae87dd8ca51c151bfc2d884b4b6a6913040 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -1,7 +1,8 @@
'use strict'
import * as vscode from 'vscode'
-import { join, dirname } from 'path'
+import { dirname } from 'path'
+const htmlElements = require('./htmlElements.js')
const tailwindClassNames = require('tailwind-class-names')
const dlv = require('dlv')
const Color = require('color')
@@ -15,16 +16,13 @@ 'jade',
'razor',
'php',
'blade',
- 'vue',
'twig',
'markdown',
'erb',
'handlebars',
'ejs',
'nunjucks',
- 'haml',
- // for jsx
- ...JS_TYPES
+ 'haml'
]
const CSS_TYPES = ['css', 'sass', 'scss', 'less', 'postcss', 'stylus']
@@ -97,14 +95,25 @@ }
export function deactivate() {}
-function createCompletionItemProvider(
+function createCompletionItemProvider({
items,
- languages: string[],
- regex: RegExp,
- triggerCharacters: string[],
+ languages,
+ regex,
+ triggerCharacters,
config,
- prefix = ''
-): vscode.Disposable {
+ prefix = '',
+ enable = () => true,
+ emmet = false
+}: {
+ items?
+ languages?: string[]
+ regex?: RegExp
+ triggerCharacters?: string[]
+ config?
+ prefix?: string
+ enable?: (text: string) => boolean
+ emmet?: boolean
+} = {}): vscode.Disposable {
return vscode.languages.registerCompletionItemProvider(
languages,
{
@@ -116,21 +125,38 @@ const separator = config.options.separator || ':'
let str
const range: vscode.Range = new vscode.Range(
- new vscode.Position(Math.max(position.line - 5, 0), 0),
+ new vscode.Position(0, 0),
position
)
const text: string = document.getText(range)
- let matches = text.match(regex)
+ if (!enable(text)) return []
+
+ let lines = text.split(/[\n\r]/)
+
+ let matches = lines
+ .slice(-5)
+ .join('\n')
+ .match(regex)
if (matches) {
let parts = matches[matches.length - 1].split(' ')
str = parts[parts.length - 1]
- } else if (languages.indexOf('html') !== -1) {
+ } else if (emmet) {
// match emmet style syntax
// e.g. .flex.items-center
- let lineText = text.split('\n').pop()
- matches = lineText.match(/\.([^()#>*^ \[\]=$@{}]*)$/i)
+ let currentLine = lines[lines.length - 1]
+ let currentWord = currentLine.split(' ').pop()
+ matches = currentWord.match(/^\.([^.()#>*^ \[\]=$@{}]*)$/)
+ if (!matches) {
+ matches = currentWord.match(
+ new RegExp(
+ `^([A-Z][a-zA-Z0-9]*|[a-z][a-z0-9]*-[a-z0-9-]+|${htmlElements.join(
+ '|'
+ )}).*?\\.([^.()#>*^ \\[\\]=$@{}]*)$`
+ )
+ )
+ }
let parts = matches[matches.length - 1].split('.')
str = parts[parts.length - 1]
}
@@ -163,6 +189,59 @@ ...triggerCharacters
)
}
+function createConfigItemProvider({
+ languages,
+ items,
+ enable = () => true
+}: {
+ languages?: string[]
+ items?: vscode.CompletionItem[]
+ enable?: (text: string) => boolean
+} = {}) {
+ return vscode.languages.registerCompletionItemProvider(
+ languages,
+ {
+ provideCompletionItems: (
+ document: vscode.TextDocument,
+ position: vscode.Position
+ ): vscode.CompletionItem[] => {
+ const range: vscode.Range = new vscode.Range(
+ new vscode.Position(0, 0),
+ position
+ )
+ const text: string = document.getText(range)
+
+ if (!enable(text)) return []
+
+ let lines = text.split(/[\n\r]/)
+
+ let matches = lines
+ .slice(-5)
+ .join('\n')
+ .match(/config\(["']([^"']*)$/)
+
+ if (!matches) return []
+
+ let objPath =
+ matches[1]
+ .replace(/\.[^.]*$/, '')
+ .replace('.', '.children.')
+ .trim() + '.children'
+ let foo = dlv(items, objPath)
+
+ if (foo) {
+ return Object.keys(foo).map(x => foo[x].item)
+ }
+
+ return Object.keys(items).map(x => items[x].item)
+ }
+ },
+ "'",
+ '"',
+ '.'
+ )
+}
+
function prefixItems(items, str, prefix) {
const addPrefix =
typeof prefix !== 'undefined' && prefix !== '' && str === prefix
@@ -257,7 +336,7 @@
return items
}
-function createConfigItems(config) {
+function createConfigItems(config, prefix = '') {
let items = {}
let i = 0
@@ -270,7 +349,7 @@
if (depthOf(config[key]) === 0) {
if (key === 'plugins') return
- item.filterText = item.insertText = `.${key}`
+ item.filterText = item.insertText = `${prefix}${key}`
item.sortText = naturalExpand(i.toString())
if (typeof config[key] === 'string' || typeof config[key] === 'number') {
item.detail = config[key]
@@ -290,7 +369,7 @@
item.filterText = item.insertText = `${key}.`
item.sortText = naturalExpand(i.toString())
item.command = { title: '', command: 'editor.action.triggerSuggest' }
- items[key] = { item, children: createConfigItems(config[key]) }
+ items[key] = { item, children: createConfigItems(config[key], prefix) }
}
i++
@@ -305,6 +384,7 @@ private _disposable: vscode.Disposable
private _tailwind
private _items
private _configItems
+ private _prefixedConfigItems
constructor(tailwind) {
if (tailwind) {
@@ -322,162 +402,235 @@ if (separator !== ':') return
this._items = createItems(tailwind.classNames, separator, tailwind.config)
this._configItems = createConfigItems(tailwind.config)
+ this._prefixedConfigItems = createConfigItems(tailwind.config, '.')
this._providers = []
this._providers.push(
- createCompletionItemProvider(
- this._items,
- JS_TYPES,
- /\btw`([^`]*)$/,
- ['`', ' ', separator],
- tailwind.config
- )
+ createCompletionItemProvider({
+ items: this._items,
+ languages: JS_TYPES,
+ regex: /\btw`([^`]*)$/,
+ triggerCharacters: ['`', ' ', separator],
+ config: tailwind.config
+ })
)
this._providers.push(
- createCompletionItemProvider(
- this._items,
- CSS_TYPES,
- /@apply ([^;}]*)$/,
- ['.', separator],
- tailwind.config,
- '.'
- )
+ createCompletionItemProvider({
+ items: this._items,
+ languages: CSS_TYPES,
+ regex: /@apply ([^;}]*)$/,
+ triggerCharacters: ['.', separator],
+ config: tailwind.config,
+ prefix: '.'
+ })
)
this._providers.push(
- createCompletionItemProvider(
- this._items,
- HTML_TYPES,
- /\bclass(Name)?=["']([^"']*)$/, // /\bclass(Name)?=(["'])(?!.*?\2)/
- ["'", '"', ' ', '.', separator],
- tailwind.config
- )
+ createCompletionItemProvider({
+ items: this._items,
+ languages: HTML_TYPES,
+ regex: /\bclass=["']([^"']*)$/, // /\bclass(Name)?=(["'])(?!.*?\2)/
+ triggerCharacters: ["'", '"', ' ', '.', separator],
+ config: tailwind.config,
+ emmet: true
+ })
)
this._providers.push(
- vscode.languages.registerCompletionItemProvider(
- CSS_TYPES,
- {
- provideCompletionItems: (
- document: vscode.TextDocument,
- position: vscode.Position
- ): vscode.CompletionItem[] => {
- const range: vscode.Range = new vscode.Range(
- new vscode.Position(Math.max(position.line - 5, 0), 0),
- position
- )
- const text: string = document.getText(range)
+ createCompletionItemProvider({
+ items: this._items,
+ languages: JS_TYPES,
+ regex: /\bclass(Name)?=["']([^"']*)$/, // /\bclass(Name)?=(["'])(?!.*?\2)/
+ triggerCharacters: ["'", '"', ' ', separator]
+ .concat([
+ Object.keys(
+ vscode.workspace.getConfiguration('emmet.includeLanguages')
+ ).indexOf('javascript') !== -1 && '.'
+ ])
+ .filter(Boolean),
+ config: tailwind.config,
+ emmet:
+ Object.keys(
+ vscode.workspace.getConfiguration('emmet.includeLanguages')
+ ).indexOf('javascript') !== -1
+ })
+ )
- let matches = text.match(/config\(["']([^"']*)$/)
+ // Vue.js
+ this._providers.push(
+ createCompletionItemProvider({
+ items: this._items,
+ languages: ['vue'],
+ regex: /\bclass=["']([^"']*)$/,
+ enable: text => {
+ if (
+ text.indexOf('<template') !== -1 &&
+ text.indexOf('</template>') === -1
+ ) {
+ return true
+ }
+ return false
+ },
+ triggerCharacters: ["'", '"', ' ', separator]
+ .concat([
+ Object.keys(
+ vscode.workspace.getConfiguration('emmet.includeLanguages')
+ ).indexOf('vue-html') !== -1 && '.'
+ ])
+ .filter(Boolean),
+ config: tailwind.config,
+ emmet:
+ Object.keys(
+ vscode.workspace.getConfiguration('emmet.includeLanguages')
+ ).indexOf('vue-html') !== -1
+ })
+ )
+ this._providers.push(
+ createCompletionItemProvider({
+ items: this._items,
+ languages: ['vue'],
+ regex: /\bclass=["']([^"']*)$/,
+ enable: text => {
+ if (
+ text.indexOf('<script') !== -1 &&
+ text.indexOf('</script>') === -1
+ ) {
+ return true
+ }
+ return false
+ },
+ triggerCharacters: ["'", '"', ' ', separator],
+ config: tailwind.config
+ })
+ )
+ this._providers.push(
+ createCompletionItemProvider({
+ items: this._items,
+ languages: ['vue'],
+ regex: /@apply ([^;}]*)$/,
+ triggerCharacters: ['.', separator],
+ config: tailwind.config,
+ enable: text => {
+ if (
+ text.indexOf('<style') !== -1 &&
+ text.indexOf('</style>') === -1
+ ) {
+ return true
+ }
+ return false
+ }
+ })
+ )
- if (!matches) return []
+ this._providers.push(
+ createConfigItemProvider({
+ languages: CSS_TYPES,
+ items: this._prefixedConfigItems
+ })
+ )
- let objPath =
- matches[1]
- .replace(/\.[^.]*$/, '')
- .replace('.', '.children.')
- .trim() + '.children'
- let foo = dlv(this._configItems, objPath)
-
- if (foo) {
- console.log(Object.keys(foo).map(x => foo[x].item))
- return Object.keys(foo).map(x => foo[x].item)
- }
-
- return Object.keys(this._configItems).map(
- x => this._configItems[x].item
- )
+ this._providers.push(
+ createConfigItemProvider({
+ languages: ['vue'],
+ items: this._configItems,
+ enable: text => {
+ if (
+ text.indexOf('<style') !== -1 &&
+ text.indexOf('</style>') === -1
+ ) {
+ return true
}
- },
- "'",
- '"',
- '.'
- )
+ return false
+ }
+ })
)
this._providers.push(
- vscode.languages.registerHoverProvider(HTML_TYPES, {
- provideHover: (document, position, token) => {
- const range1: vscode.Range = new vscode.Range(
- new vscode.Position(Math.max(position.line - 5, 0), 0),
- position
- )
- const text1: string = document.getText(range1)
+ vscode.languages.registerHoverProvider(
+ [...HTML_TYPES, ...JS_TYPES, 'vue'],
+ {
+ provideHover: (document, position, token) => {
+ const range1: vscode.Range = new vscode.Range(
+ new vscode.Position(Math.max(position.line - 5, 0), 0),
+ position
+ )
+ const text1: string = document.getText(range1)
- if (!/\bclass(Name)?=['"][^'"]*$/.test(text1)) return
+ if (!/\bclass(Name)?=['"][^'"]*$/.test(text1)) return
- const range2: vscode.Range = new vscode.Range(
- new vscode.Position(Math.max(position.line - 5, 0), 0),
- position.with({ line: position.line + 1 })
- )
- const text2: string = document.getText(range2)
+ const range2: vscode.Range = new vscode.Range(
+ new vscode.Position(Math.max(position.line - 5, 0), 0),
+ position.with({ line: position.line + 1 })
+ )
+ const text2: string = document.getText(range2)
- let str = text1 + text2.substr(text1.length).match(/^([^"' ]*)/)[0]
- let matches = str.match(/\bclass(Name)?=["']([^"']*)$/)
+ let str = text1 + text2.substr(text1.length).match(/^([^"' ]*)/)[0]
+ let matches = str.match(/\bclass(Name)?=["']([^"']*)$/)
- if (matches && matches[2]) {
- let className = matches[2].split(' ').pop()
- let parts = className.split(':')
+ if (matches && matches[2]) {
+ let className = matches[2].split(' ').pop()
+ let parts = className.split(':')
- if (typeof dlv(this._tailwind.classNames, parts) === 'string') {
- let base = parts.pop()
- let selector = `.${escapeClassName(className)}`
-
- if (parts.indexOf('hover') !== -1) {
- selector += ':hover'
- } else if (parts.indexOf('focus') !== -1) {
- selector += ':focus'
- } else if (parts.indexOf('active') !== -1) {
- selector += ':active'
- } else if (parts.indexOf('group-hover') !== -1) {
- selector = `.group:hover ${selector}`
- }
+ if (typeof dlv(this._tailwind.classNames, parts) === 'string') {
+ let base = parts.pop()
+ let selector = `.${escapeClassName(className)}`
- let hoverStr = new vscode.MarkdownString()
- let css = this._tailwind.classNames[base]
- let m = css.match(/^(::?[a-z-]+) {(.*?)}/)
- if (m) {
- selector += m[1]
- css = m[2].trim()
- }
- css = css.replace(/([;{]) /g, '$1\n').replace(/^/gm, ' ')
- let code = `${selector} {\n${css}\n}`
- let screens = dlv(this._tailwind.config, 'screens', {})
+ if (parts.indexOf('hover') !== -1) {
+ selector += ':hover'
+ } else if (parts.indexOf('focus') !== -1) {
+ selector += ':focus'
+ } else if (parts.indexOf('active') !== -1) {
+ selector += ':active'
+ } else if (parts.indexOf('group-hover') !== -1) {
+ selector = `.group:hover ${selector}`
+ }
- Object.keys(screens).some(screen => {
- if (parts.indexOf(screen) !== -1) {
- code = `@media (min-width: ${
- screens[screen]
- }) {\n${code.replace(/^/gm, ' ')}\n}`
- return true
+ let hoverStr = new vscode.MarkdownString()
+ let css = this._tailwind.classNames[base]
+ let m = css.match(/^(::?[a-z-]+) {(.*?)}/)
+ if (m) {
+ selector += m[1]
+ css = m[2].trim()
}
- return false
- })
- hoverStr.appendCodeblock(code, 'css')
+ css = css.replace(/([;{]) /g, '$1\n').replace(/^/gm, ' ')
+ let code = `${selector} {\n${css}\n}`
+ let screens = dlv(this._tailwind.config, 'screens', {})
- let hoverRange = new vscode.Range(
- new vscode.Position(
- position.line,
- position.character +
- str.length -
- text1.length -
- className.length
- ),
- new vscode.Position(
- position.line,
- position.character + str.length - text1.length
+ Object.keys(screens).some(screen => {
+ if (parts.indexOf(screen) !== -1) {
+ code = `@media (min-width: ${
+ screens[screen]
+ }) {\n${code.replace(/^/gm, ' ')}\n}`
+ return true
+ }
+ return false
+ })
+ hoverStr.appendCodeblock(code, 'css')
+
+ let hoverRange = new vscode.Range(
+ new vscode.Position(
+ position.line,
+ position.character +
+ str.length -
+ text1.length -
+ className.length
+ ),
+ new vscode.Position(
+ position.line,
+ position.character + str.length - text1.length
+ )
)
- )
- return new vscode.Hover(hoverStr, hoverRange)
+ return new vscode.Hover(hoverStr, hoverRange)
+ }
}
- }
- return null
+ return null
+ }
}
- })
+ )
)
this._disposable = vscode.Disposable.from(...this._providers)
diff --git a/src/htmlElements.ts b/src/htmlElements.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4bb7b1c1220b17799c08d5b923dd5e8ac0e4f280
--- /dev/null
+++ b/src/htmlElements.ts
@@ -0,0 +1,142 @@
+module.exports = [
+ 'a',
+ 'abbr',
+ 'acronym',
+ 'address',
+ 'applet',
+ 'area',
+ 'article',
+ 'aside',
+ 'audio',
+ 'b',
+ 'base',
+ 'basefont',
+ 'bdi',
+ 'bdo',
+ 'bgsound',
+ 'big',
+ 'blink',
+ 'blockquote',
+ 'body',
+ 'br',
+ 'button',
+ 'canvas',
+ 'caption',
+ 'center',
+ 'cite',
+ 'code',
+ 'col',
+ 'colgroup',
+ 'command',
+ 'content',
+ 'data',
+ 'datalist',
+ 'dd',
+ 'del',
+ 'details',
+ 'dfn',
+ 'dialog',
+ 'dir',
+ 'div',
+ 'dl',
+ 'dt',
+ 'element',
+ 'em',
+ 'embed',
+ 'fieldset',
+ 'figcaption',
+ 'figure',
+ 'font',
+ 'footer',
+ 'form',
+ 'frame',
+ 'frameset',
+ 'h1',
+ 'head',
+ 'header',
+ 'hgroup',
+ 'hr',
+ 'html',
+ 'i',
+ 'iframe',
+ 'image',
+ 'img',
+ 'input',
+ 'ins',
+ 'isindex',
+ 'kbd',
+ 'keygen',
+ 'label',
+ 'legend',
+ 'li',
+ 'link',
+ 'listing',
+ 'main',
+ 'map',
+ 'mark',
+ 'marquee',
+ 'menu',
+ 'menuitem',
+ 'meta',
+ 'meter',
+ 'multicol',
+ 'nav',
+ 'nextid',
+ 'nobr',
+ 'noembed',
+ 'noframes',
+ 'noscript',
+ 'object',
+ 'ol',
+ 'optgroup',
+ 'option',
+ 'output',
+ 'p',
+ 'param',
+ 'picture',
+ 'plaintext',
+ 'pre',
+ 'progress',
+ 'q',
+ 'rb',
+ 'rp',
+ 'rt',
+ 'rtc',
+ 'ruby',
+ 's',
+ 'samp',
+ 'script',
+ 'section',
+ 'select',
+ 'shadow',
+ 'slot',
+ 'small',
+ 'source',
+ 'spacer',
+ 'span',
+ 'strike',
+ 'strong',
+ 'style',
+ 'sub',
+ 'summary',
+ 'sup',
+ 'table',
+ 'tbody',
+ 'td',
+ 'template',
+ 'textarea',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'time',
+ 'title',
+ 'tr',
+ 'track',
+ 'tt',
+ 'u',
+ 'ul',
+ 'var',
+ 'video',
+ 'wbr',
+ 'xmp'
+]