master
Brad Cornes 2018-08-25 12:00:03 +01:00
commit 8189593493
2 changed files with 439 additions and 144 deletions

View File

@ -1,7 +1,8 @@
'use strict' 'use strict'
import * as vscode from 'vscode' 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 tailwindClassNames = require('tailwind-class-names')
const dlv = require('dlv') const dlv = require('dlv')
const Color = require('color') const Color = require('color')
@ -15,16 +16,13 @@ const HTML_TYPES = [
'razor', 'razor',
'php', 'php',
'blade', 'blade',
'vue',
'twig', 'twig',
'markdown', 'markdown',
'erb', 'erb',
'handlebars', 'handlebars',
'ejs', 'ejs',
'nunjucks', 'nunjucks',
'haml', 'haml'
// for jsx
...JS_TYPES
] ]
const CSS_TYPES = ['css', 'sass', 'scss', 'less', 'postcss', 'stylus'] const CSS_TYPES = ['css', 'sass', 'scss', 'less', 'postcss', 'stylus']
@ -103,14 +101,25 @@ async function getTailwind() {
export function deactivate() {} export function deactivate() {}
function createCompletionItemProvider( function createCompletionItemProvider({
items, items,
languages: string[], languages,
regex: RegExp, regex,
triggerCharacters: string[], triggerCharacters,
config, config,
prefix = '' prefix = '',
): vscode.Disposable { 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( return vscode.languages.registerCompletionItemProvider(
languages, languages,
{ {
@ -122,21 +131,38 @@ function createCompletionItemProvider(
let str let str
const range: vscode.Range = new vscode.Range( const range: vscode.Range = new vscode.Range(
new vscode.Position(Math.max(position.line - 5, 0), 0), new vscode.Position(0, 0),
position position
) )
const text: string = document.getText(range) 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) { if (matches) {
let parts = matches[matches.length - 1].split(' ') let parts = matches[matches.length - 1].split(' ')
str = parts[parts.length - 1] str = parts[parts.length - 1]
} else if (languages.indexOf('html') !== -1) { } else if (emmet) {
// match emmet style syntax // match emmet style syntax
// e.g. .flex.items-center // e.g. .flex.items-center
let lineText = text.split('\n').pop() let currentLine = lines[lines.length - 1]
matches = lineText.match(/\.([^()#>*^ \[\]=$@{}]*)$/i) 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(
'|'
)}).*?\\.([^.()#>*^ \\[\\]=$@{}]*)$`
)
)
}
if (matches) { if (matches) {
let parts = matches[matches.length - 1].split('.') let parts = matches[matches.length - 1].split('.')
str = parts[parts.length - 1] str = parts[parts.length - 1]
@ -171,6 +197,59 @@ function createCompletionItemProvider(
) )
} }
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) { function prefixItems(items, str, prefix) {
const addPrefix = const addPrefix =
typeof prefix !== 'undefined' && prefix !== '' && str === prefix typeof prefix !== 'undefined' && prefix !== '' && str === prefix
@ -265,7 +344,7 @@ function createItems(classNames, separator, config, parent = '') {
return items return items
} }
function createConfigItems(config) { function createConfigItems(config, prefix = '') {
let items = {} let items = {}
let i = 0 let i = 0
@ -278,7 +357,7 @@ function createConfigItems(config) {
if (depthOf(config[key]) === 0) { if (depthOf(config[key]) === 0) {
if (key === 'plugins') return if (key === 'plugins') return
item.filterText = item.insertText = `.${key}` item.filterText = item.insertText = `${prefix}${key}`
item.sortText = naturalExpand(i.toString()) item.sortText = naturalExpand(i.toString())
if (typeof config[key] === 'string' || typeof config[key] === 'number') { if (typeof config[key] === 'string' || typeof config[key] === 'number') {
item.detail = config[key] item.detail = config[key]
@ -298,7 +377,7 @@ function createConfigItems(config) {
item.filterText = item.insertText = `${key}.` item.filterText = item.insertText = `${key}.`
item.sortText = naturalExpand(i.toString()) item.sortText = naturalExpand(i.toString())
item.command = { title: '', command: 'editor.action.triggerSuggest' } item.command = { title: '', command: 'editor.action.triggerSuggest' }
items[key] = { item, children: createConfigItems(config[key]) } items[key] = { item, children: createConfigItems(config[key], prefix) }
} }
i++ i++
@ -313,6 +392,7 @@ class TailwindIntellisense {
private _tailwind private _tailwind
private _items private _items
private _configItems private _configItems
private _prefixedConfigItems
constructor(tailwind) { constructor(tailwind) {
if (tailwind) { if (tailwind) {
@ -330,162 +410,235 @@ class TailwindIntellisense {
this._items = createItems(tailwind.classNames, separator, tailwind.config) this._items = createItems(tailwind.classNames, separator, tailwind.config)
this._configItems = createConfigItems(tailwind.config) this._configItems = createConfigItems(tailwind.config)
this._prefixedConfigItems = createConfigItems(tailwind.config, '.')
this._providers = [] this._providers = []
this._providers.push( this._providers.push(
createCompletionItemProvider( createCompletionItemProvider({
this._items, items: this._items,
JS_TYPES, languages: JS_TYPES,
/\btw`([^`]*)$/, regex: /\btw`([^`]*)$/,
['`', ' ', separator], triggerCharacters: ['`', ' ', separator],
tailwind.config config: tailwind.config
) })
) )
this._providers.push( this._providers.push(
createCompletionItemProvider( createCompletionItemProvider({
this._items, items: this._items,
CSS_TYPES, languages: CSS_TYPES,
/@apply ([^;}]*)$/, regex: /@apply ([^;}]*)$/,
['.', separator], triggerCharacters: ['.', separator],
tailwind.config, config: tailwind.config,
'.' prefix: '.'
) })
) )
this._providers.push( this._providers.push(
createCompletionItemProvider( createCompletionItemProvider({
this._items, items: this._items,
HTML_TYPES, languages: HTML_TYPES,
/\bclass(Name)?=["']([^"']*)$/, // /\bclass(Name)?=(["'])(?!.*?\2)/ regex: /\bclass=["']([^"']*)$/, // /\bclass(Name)?=(["'])(?!.*?\2)/
["'", '"', ' ', '.', separator], triggerCharacters: ["'", '"', ' ', '.', separator],
tailwind.config config: tailwind.config,
) emmet: true
})
) )
this._providers.push( this._providers.push(
vscode.languages.registerCompletionItemProvider( createCompletionItemProvider({
CSS_TYPES, 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
})
)
// 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
}
})
)
this._providers.push(
createConfigItemProvider({
languages: CSS_TYPES,
items: this._prefixedConfigItems
})
)
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, ...JS_TYPES, 'vue'],
{ {
provideCompletionItems: ( provideHover: (document, position, token) => {
document: vscode.TextDocument, const range1: vscode.Range = new vscode.Range(
position: vscode.Position
): vscode.CompletionItem[] => {
const range: vscode.Range = new vscode.Range(
new vscode.Position(Math.max(position.line - 5, 0), 0), new vscode.Position(Math.max(position.line - 5, 0), 0),
position position
) )
const text: string = document.getText(range) const text1: string = document.getText(range1)
let matches = text.match(/config\(["']([^"']*)$/) if (!/\bclass(Name)?=['"][^'"]*$/.test(text1)) return
if (!matches) return [] const range2: vscode.Range = new vscode.Range(
new vscode.Position(Math.max(position.line - 5, 0), 0),
let objPath = position.with({ line: position.line + 1 })
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
) )
} const text2: string = document.getText(range2)
},
"'",
'"',
'.'
)
)
this._providers.push( let str = text1 + text2.substr(text1.length).match(/^([^"' ]*)/)[0]
vscode.languages.registerHoverProvider(HTML_TYPES, { let matches = str.match(/\bclass(Name)?=["']([^"']*)$/)
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 (matches && matches[2]) {
let className = matches[2].split(' ').pop()
let parts = className.split(':')
const range2: vscode.Range = new vscode.Range( if (typeof dlv(this._tailwind.classNames, parts) === 'string') {
new vscode.Position(Math.max(position.line - 5, 0), 0), let base = parts.pop()
position.with({ line: position.line + 1 }) let selector = `.${escapeClassName(className)}`
)
const text2: string = document.getText(range2)
let str = text1 + text2.substr(text1.length).match(/^([^"' ]*)/)[0] if (parts.indexOf('hover') !== -1) {
let matches = str.match(/\bclass(Name)?=["']([^"']*)$/) selector += ':hover'
} else if (parts.indexOf('focus') !== -1) {
if (matches && matches[2]) { selector += ':focus'
let className = matches[2].split(' ').pop() } else if (parts.indexOf('active') !== -1) {
let parts = className.split(':') selector += ':active'
} else if (parts.indexOf('group-hover') !== -1) {
if (typeof dlv(this._tailwind.classNames, parts) === 'string') { selector = `.group:hover ${selector}`
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}`
}
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', {})
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( let hoverStr = new vscode.MarkdownString()
new vscode.Position( let css = this._tailwind.classNames[base]
position.line, let m = css.match(/^(::?[a-z-]+) {(.*?)}/)
position.character + if (m) {
str.length - selector += m[1]
text1.length - css = m[2].trim()
className.length }
), css = css.replace(/([;{]) /g, '$1\n').replace(/^/gm, ' ')
new vscode.Position( let code = `${selector} {\n${css}\n}`
position.line, let screens = dlv(this._tailwind.config, 'screens', {})
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) this._disposable = vscode.Disposable.from(...this._providers)

142
src/htmlElements.ts 100644
View File

@ -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'
]