From e3bbd69369ea2db2ca7fc0bf5add692a583d9630 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Fri, 24 Aug 2018 19:43:35 +0100 Subject: [PATCH 1/3] add .vue @apply support (#4) --- src/extension.ts | 126 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 34 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index fd552df..59d0b29 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,7 +15,6 @@ const HTML_TYPES = [ 'razor', 'php', 'blade', - 'vue', 'twig', 'markdown', 'erb', @@ -97,14 +96,25 @@ async function getTailwind() { 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 +126,28 @@ function createCompletionItemProvider( 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] + matches = currentLine.match(/\.([^()#>*^ \[\]=$@{}]*)$/i) let parts = matches[matches.length - 1].split('.') str = parts[parts.length - 1] } @@ -326,34 +343,75 @@ class TailwindIntellisense { 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(Name)?=["']([^"']*)$/, // /\bclass(Name)?=(["'])(?!.*?\2)/ + triggerCharacters: ["'", '"', ' ', '.', separator], + config: tailwind.config, + emmet: true + }) + ) + + // Vue.js + this._providers.push( + createCompletionItemProvider({ + items: this._items, + languages: ['vue'], + regex: /\bclass(Name)?=["']([^"']*)$/, + enable: text => { + if ( + (text.indexOf('') === -1) || + (text.indexOf('') === -1) + ) { + return true + } + return false + }, + triggerCharacters: ["'", '"', ' ', '.', separator], + config: tailwind.config, + emmet: true + }) + ) + this._providers.push( + createCompletionItemProvider({ + items: this._items, + languages: ['vue'], + regex: /@apply ([^;}]*)$/, + triggerCharacters: ['.', separator], + config: tailwind.config, + enable: text => { + if ( + text.indexOf('') === -1 + ) { + return true + } + return false + } + }) ) this._providers.push( From 3ed7a35646a576ca4619db412f80abdb2092099f Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Sat, 25 Aug 2018 00:03:13 +0100 Subject: [PATCH 2/3] add config helper completions for .vue files --- src/extension.ts | 115 +++++++++++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 39 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 59d0b29..371c76f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -180,6 +180,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) { const addPrefix = typeof prefix !== 'undefined' && prefix !== '' && str === prefix @@ -274,7 +327,7 @@ function createItems(classNames, separator, config, parent = '') { return items } -function createConfigItems(config) { +function createConfigItems(config, prefix = '') { let items = {} let i = 0 @@ -287,7 +340,7 @@ function createConfigItems(config) { 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] @@ -307,7 +360,7 @@ function createConfigItems(config) { 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++ @@ -322,6 +375,7 @@ class TailwindIntellisense { private _tailwind private _items private _configItems + private _prefixedConfigItems constructor(tailwind) { if (tailwind) { @@ -339,6 +393,7 @@ class TailwindIntellisense { this._items = createItems(tailwind.classNames, separator, tailwind.config) this._configItems = createConfigItems(tailwind.config) + this._prefixedConfigItems = createConfigItems(tailwind.config, '.') this._providers = [] @@ -415,44 +470,26 @@ class TailwindIntellisense { ) 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) + createConfigItemProvider({ + languages: CSS_TYPES, + items: this._prefixedConfigItems + }) + ) - 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 - ) + this._providers.push( + createConfigItemProvider({ + languages: ['vue'], + items: this._configItems, + enable: text => { + if ( + text.indexOf('') === -1 + ) { + return true } - }, - "'", - '"', - '.' - ) + return false + } + }) ) this._providers.push( From cc424000813c6338689aba1692a5fe6a808901f5 Mon Sep 17 00:00:00 2001 From: Brad Cornes Date: Sat, 25 Aug 2018 11:57:21 +0100 Subject: [PATCH 3/3] improve emmet syntax handling (#3) --- src/extension.ts | 216 ++++++++++++++++++++++++++++---------------- src/htmlElements.ts | 142 +++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+), 79 deletions(-) create mode 100644 src/htmlElements.ts diff --git a/src/extension.ts b/src/extension.ts index 371c76f..c4598ae 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') @@ -21,9 +22,7 @@ const HTML_TYPES = [ 'handlebars', 'ejs', 'nunjucks', - 'haml', - // for jsx - ...JS_TYPES + 'haml' ] const CSS_TYPES = ['css', 'sass', 'scss', 'less', 'postcss', 'stylus'] @@ -147,7 +146,17 @@ function createCompletionItemProvider({ // match emmet style syntax // e.g. .flex.items-center let currentLine = lines[lines.length - 1] - matches = currentLine.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( + '|' + )}).*?\\.([^.()#>*^ \\[\\]=$@{}]*)$` + ) + ) + } let parts = matches[matches.length - 1].split('.') str = parts[parts.length - 1] } @@ -422,32 +431,78 @@ class TailwindIntellisense { createCompletionItemProvider({ items: this._items, languages: HTML_TYPES, - regex: /\bclass(Name)?=["']([^"']*)$/, // /\bclass(Name)?=(["'])(?!.*?\2)/ + regex: /\bclass=["']([^"']*)$/, // /\bclass(Name)?=(["'])(?!.*?\2)/ triggerCharacters: ["'", '"', ' ', '.', separator], config: tailwind.config, emmet: true }) ) + this._providers.push( + 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 + }) + ) + // Vue.js this._providers.push( createCompletionItemProvider({ items: this._items, languages: ['vue'], - regex: /\bclass(Name)?=["']([^"']*)$/, + regex: /\bclass=["']([^"']*)$/, enable: text => { if ( - (text.indexOf('') === -1) || - (text.indexOf('') === -1) + text.indexOf('') === -1 ) { return true } return false }, - triggerCharacters: ["'", '"', ' ', '.', separator], + triggerCharacters: ["'", '"', ' ', separator] + .concat([ + Object.keys( + vscode.workspace.getConfiguration('emmet.includeLanguages') + ).indexOf('vue-html') !== -1 && '.' + ]) + .filter(Boolean), config: tailwind.config, - emmet: true + 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('') === -1 + ) { + return true + } + return false + }, + triggerCharacters: ["'", '"', ' ', separator], + config: tailwind.config }) ) this._providers.push( @@ -493,86 +548,89 @@ class TailwindIntellisense { ) 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 (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}` - } - - 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 + 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}` } - 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 + 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( + 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 0000000..4bb7b1c --- /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' +]