tailwind-ctp-intellisense/src/extension.ts

389 lines
9.1 KiB
TypeScript
Raw Normal View History

2018-04-19 20:18:23 +00:00
'use strict'
import * as vscode from 'vscode'
import { join } from 'path'
const tailwindClassNames = require('tailwind-class-names')
// const tailwindClassNames = require('/Users/brad/Code/tailwind-class-names/dist')
const dlv = require('dlv')
2018-04-20 00:42:31 +00:00
const CONFIG_GLOB = '{tailwind,tailwind.config,.tailwindrc}.js'
2018-04-19 20:18:23 +00:00
export async function activate(context: vscode.ExtensionContext) {
2018-04-20 00:42:31 +00:00
let tw
try {
tw = await getTailwind()
} catch (err) {}
let intellisense = new TailwindIntellisense(tw)
context.subscriptions.push(intellisense)
let watcher = vscode.workspace.createFileSystemWatcher(`**/${CONFIG_GLOB}`)
watcher.onDidChange(onFileChange)
watcher.onDidCreate(onFileChange)
watcher.onDidDelete(onFileChange)
async function onFileChange(event) {
try {
tw = await getTailwind()
} catch (err) {
intellisense.dispose()
return
}
intellisense.reload(tw)
}
}
async function getTailwind() {
2018-04-19 20:18:23 +00:00
if (!vscode.workspace.name) return
2018-04-20 00:42:31 +00:00
let files = await vscode.workspace.findFiles(
CONFIG_GLOB,
2018-04-19 20:18:23 +00:00
'**/node_modules/**',
1
)
2018-04-20 00:42:31 +00:00
if (!files) return null
let configPath = files[0].fsPath
2018-04-19 20:18:23 +00:00
const plugin = join(
vscode.workspace.workspaceFolders[0].uri.fsPath,
'node_modules',
'tailwindcss'
)
let tw
try {
tw = await tailwindClassNames(
2018-04-20 00:42:31 +00:00
configPath,
2018-04-19 20:18:23 +00:00
{
tree: true,
strings: true
},
plugin
)
} catch (err) {
2018-04-20 00:42:31 +00:00
return null
2018-04-19 20:18:23 +00:00
}
2018-04-20 00:42:31 +00:00
return tw
2018-04-19 20:18:23 +00:00
}
export function deactivate() {}
function createCompletionItemProvider(
items,
languages: string[],
regex: RegExp,
triggerCharacters: string[],
config,
prefix = ''
): vscode.Disposable {
return vscode.languages.registerCompletionItemProvider(
languages,
{
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)
let p = prefix
const separator = config.options.separator || ':'
const matches = text.match(regex)
if (matches) {
const parts = matches[matches.length - 1].split(' ')
const str = parts[parts.length - 1]
const pth = str
.replace(new RegExp(`${separator}`, 'g'), '.')
.replace(/\.$/, '')
.replace(/^\./, '')
.replace(/\./g, '.children.')
if (pth !== '') {
const itms = dlv(items, pth)
if (itms) {
return prefixItems(itms.children, str, prefix)
}
}
return prefixItems(items, str, prefix)
}
return []
}
},
...triggerCharacters
)
}
function prefixItems(items, str, prefix) {
const addPrefix =
typeof prefix !== 'undefined' && prefix !== '' && str === prefix
return Object.keys(items).map(x => {
const item = items[x].item
if (addPrefix) {
item.filterText = item.insertText = `${prefix}${item.label}`
} else {
item.filterText = item.insertText = item.label
}
return item
})
}
function depthOf(obj) {
2018-05-07 17:15:29 +00:00
if (typeof obj !== 'object' || Array.isArray(obj)) return 0
2018-04-19 20:18:23 +00:00
let level = 1
for (let key in obj) {
if (!obj.hasOwnProperty(key)) continue
if (typeof obj[key] === 'object') {
const depth = depthOf(obj[key]) + 1
level = Math.max(depth, level)
}
}
return level
}
function createItems(classNames, separator, config, parent = '') {
let items = {}
Object.keys(classNames).forEach(key => {
if (depthOf(classNames[key]) === 0) {
const item = new vscode.CompletionItem(
key,
vscode.CompletionItemKind.Constant
)
if (key !== 'container' && key !== 'group') {
if (parent) {
item.detail = classNames[key].replace(
new RegExp(`:${parent} \{(.*?)\}`),
'$1'
)
} else {
item.detail = classNames[key]
}
}
items[key] = {
item
}
} else {
const item = new vscode.CompletionItem(
`${key}${separator}`,
vscode.CompletionItemKind.Constant
)
item.command = { title: '', command: 'editor.action.triggerSuggest' }
if (key === 'hover' || key === 'focus' || key === 'active') {
item.detail = `:${key}`
} else if (key === 'group-hover') {
item.detail = '.group:hover &'
} else if (
config.screens &&
Object.keys(config.screens).indexOf(key) !== -1
) {
item.detail = `@media (min-width: ${config.screens[key]})`
}
items[key] = {
item,
children: createItems(classNames[key], separator, config, key)
}
}
})
return items
}
2018-04-20 00:42:31 +00:00
2018-05-07 17:15:29 +00:00
function createConfigItems(config) {
let items = {}
let i = 0
Object.keys(config).forEach(key => {
let item = new vscode.CompletionItem(
key,
vscode.CompletionItemKind.Constant
)
if (depthOf(config[key]) === 0) {
if (key === 'plugins') return
item.filterText = item.insertText = `.${key}`
item.sortText = naturalExpand(i.toString())
if (typeof config[key] === 'string' || typeof config[key] === 'number') {
item.detail = config[key]
} else if (Array.isArray(config[key])) {
item.detail = stringifyArray(config[key])
}
items[key] = { item }
} else {
if (key === 'modules' || key === 'options') return
item.filterText = item.insertText = `${key}.`
item.sortText = naturalExpand(i.toString())
item.command = { title: '', command: 'editor.action.triggerSuggest' }
items[key] = { item, children: createConfigItems(config[key]) }
}
i++
})
return items
}
2018-04-20 00:42:31 +00:00
class TailwindIntellisense {
private _completionProviders: vscode.Disposable[]
private _disposable: vscode.Disposable
private _items
2018-05-07 17:15:29 +00:00
private _configItems
2018-04-20 00:42:31 +00:00
constructor(tailwind) {
if (tailwind) {
this.reload(tailwind)
}
}
public reload(tailwind) {
this.dispose()
const separator = dlv(tailwind.config, 'options.separator', ':')
if (separator !== ':') return
this._items = createItems(tailwind.classNames, separator, tailwind.config)
2018-05-07 17:15:29 +00:00
this._configItems = createConfigItems(tailwind.config)
2018-04-20 00:42:31 +00:00
this._completionProviders = []
this._completionProviders.push(
createCompletionItemProvider(
this._items,
['typescriptreact', 'javascript', 'javascriptreact'],
/\btw`([^`]*)$/,
['`', ' ', separator],
tailwind.config
)
)
this._completionProviders.push(
createCompletionItemProvider(
this._items,
['css', 'sass', 'scss'],
/@apply ([^;}]*)$/,
['.', separator],
tailwind.config,
'.'
)
)
this._completionProviders.push(
createCompletionItemProvider(
this._items,
[
'html',
'jade',
'razor',
'php',
'blade',
'vue',
'twig',
'markdown',
'erb',
'handlebars',
'ejs',
// for jsx
'typescriptreact',
'javascript',
'javascriptreact'
],
/\bclass(Name)?=["']([^"']*)/, // /\bclass(Name)?=(["'])(?!.*?\2)/
["'", '"', ' ', separator],
tailwind.config
)
)
2018-05-07 17:15:29 +00:00
this._completionProviders.push(
vscode.languages.registerCompletionItemProvider(
['css', 'sass', 'scss'],
{
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)
let matches = text.match(/config\(["']([^"']*)$/)
if (!matches) return []
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
)
}
},
"'",
'"',
'.'
)
)
2018-04-20 00:42:31 +00:00
this._disposable = vscode.Disposable.from(...this._completionProviders)
}
dispose() {
if (this._disposable) {
this._disposable.dispose()
}
}
}
2018-05-07 17:15:29 +00:00
function pad(n) {
return ('00000000' + n).substr(-8)
}
function naturalExpand(a: string) {
return a.replace(/\d+/g, pad)
}
function stringifyArray(arr: Array<any>): string {
return arr
.reduce((acc, curr) => {
let str = curr.toString()
if (str.includes(' ')) {
acc.push(`"${str.replace(/\s\s+/g, ' ')}"`)
} else {
acc.push(str)
}
return acc
}, [])
.join(', ')
}