tailwind-ctp-intellisense @master -
refs -
log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
Merge branch 'next' into diagnostics
11 changed files, 282 additions(+), 145 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index d3461d15178fbd81895623d655bde63ebf413264..f844afb78a3c31adf7b940746d88f9e90ad99f41 100755
--- a/package-lock.json
+++ b/package-lock.json
@@ -1033,6 +1033,12 @@ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.1.tgz",
"integrity": "sha512-dOrgprHnkDaj1pmrwdcMAf0QRNQzqTB5rxJph+iIQshSmIvtgRqJ0nim8u1vvXU8iOXZrH96+M46JDFTPLingA==",
"dev": true
},
+ "@types/moo": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/@types/moo/-/moo-0.5.3.tgz",
+ "integrity": "sha512-PJJ/jvb5Gor8DWvXN3e75njfQyYNRz0PaFSZ3br9GfHM9N2FxvuJ/E/ytcQePJOLzHlvgFSsIJIvfUMUxWTbnA==",
+ "dev": true
+ },
"@types/node": {
"version": "13.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz",
@@ -4941,6 +4947,12 @@ "mkdirp": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz",
"integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==",
+ "dev": true
+ },
+ "moo": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz",
+ "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==",
"dev": true
},
"ms": {
diff --git a/package.json b/package.json
index 69d62d97f70770c055be6a9d69bc98fb4d3f295a..0b515cadd282395481a1489f142d1b951f894696 100755
--- a/package.json
+++ b/package.json
@@ -44,7 +44,8 @@ "source.css.scss",
"source.css.less",
"source.css.postcss",
"source.vue",
- "source.svelte"
+ "source.svelte",
+ "text.html"
]
}
],
@@ -75,6 +76,7 @@ },
"devDependencies": {
"@ctrl/tinycolor": "^3.1.0",
"@types/mocha": "^5.2.0",
+ "@types/moo": "^0.5.3",
"@types/node": "^13.9.3",
"@types/vscode": "^1.32.0",
"@zeit/ncc": "^0.22.0",
@@ -93,6 +95,7 @@ "jest": "^25.5.4",
"line-column": "^1.0.2",
"mitt": "^1.2.0",
"mkdirp": "^1.0.3",
+ "moo": "^0.5.1",
"pkg-up": "^3.1.0",
"postcss": "^7.0.27",
"postcss-selector-parser": "^6.0.2",
diff --git a/src/lib/languages.ts b/src/lib/languages.ts
index b9238edde46be72d6b7a5db044b063e57666efff..777f5885f033174f1163d970cece03505d7dd0e8 100644
--- a/src/lib/languages.ts
+++ b/src/lib/languages.ts
@@ -31,10 +31,12 @@ 'postcss',
'sass',
'scss',
'stylus',
+ 'sugarss',
// js
'javascript',
'javascriptreact',
'reason',
+ 'typescript',
'typescriptreact',
// mixed
'vue',
diff --git a/src/lsp/providers/completionProvider.ts b/src/lsp/providers/completionProvider.ts
index 8f2c43d8325bc847a7b05a87dc180025a647f2a9..eae7546bffc68d70c05ef1ebbdd0353fac33a42d 100644
--- a/src/lsp/providers/completionProvider.ts
+++ b/src/lsp/providers/completionProvider.ts
@@ -12,7 +12,7 @@ import removeMeta from '../util/removeMeta'
import { getColor, getColorFromValue } from '../util/color'
import { isHtmlContext } from '../util/html'
import { isCssContext } from '../util/css'
-import { findLast, findJsxStrings, arrFindLast } from '../util/find'
+import { findLast } from '../util/find'
import { stringifyConfigValue, stringifyCss } from '../util/stringify'
import { stringifyScreen, Screen } from '../util/screens'
import isObject from '../../util/isObject'
@@ -24,6 +24,10 @@ import { naturalExpand } from '../util/naturalExpand'
import semver from 'semver'
import { docsUrl } from '../util/docsUrl'
import { ensureArray } from '../../util/array'
+import {
+ getClassAttributeLexer,
+ getComputedClassAttributeLexer,
+} from '../util/lexers'
function completionsFromClassList(
state: State,
@@ -122,24 +126,31 @@ start: { line: Math.max(position.line - 10, 0), character: 0 },
end: position,
})
- const match = findLast(/\bclass(?:Name)?=(?<initial>['"`{])/gi, str)
+ const match = findLast(/[\s:]class(?:Name)?=['"`{]/gi, str)
if (match === null) {
return null
}
- const rest = str.substr(match.index + match[0].length)
+ const lexer =
+ match[0][0] === ':'
+ ? getComputedClassAttributeLexer()
+ : getClassAttributeLexer()
+ lexer.reset(str.substr(match.index + match[0].length - 1))
- if (match.groups.initial === '{') {
- const strings = findJsxStrings('{' + rest)
- const lastOpenString = arrFindLast(
- strings,
- (string) => typeof string.end === 'undefined'
- )
- if (lastOpenString) {
- const classList = str.substr(
- str.length - rest.length + lastOpenString.start - 1
- )
+ try {
+ let tokens = Array.from(lexer)
+ let last = tokens[tokens.length - 1]
+ if (last.type.startsWith('start') || last.type === 'classlist') {
+ let classList = ''
+ for (let i = tokens.length - 1; i >= 0; i--) {
+ if (tokens[i].type === 'classlist') {
+ classList = tokens[i].value + classList
+ } else {
+ break
+ }
+ }
+
return completionsFromClassList(state, classList, {
start: {
line: position.line,
@@ -148,20 +159,9 @@ },
end: position,
})
}
- return null
- }
+ } catch (_) {}
- if (rest.indexOf(match.groups.initial) !== -1) {
- return null
- }
-
- return completionsFromClassList(state, rest, {
- start: {
- line: position.line,
- character: position.character - rest.length,
- },
- end: position,
- })
+ return null
}
function provideAtApplyCompletions(
diff --git a/src/lsp/providers/diagnosticsProvider.ts b/src/lsp/providers/diagnosticsProvider.ts
index 66e0f4cdc90d8f09bd9c9c619e8177875cae5ba9..152f3b76a3c0a9d10f37c63f42bdcdc6e5d8a4e1 100644
--- a/src/lsp/providers/diagnosticsProvider.ts
+++ b/src/lsp/providers/diagnosticsProvider.ts
@@ -10,7 +10,7 @@ import { getClassNameParts } from '../util/getClassNameAtPosition'
const dlv = require('dlv')
function provideCssDiagnostics(state: State, document: TextDocument): void {
- const classNames = findClassNamesInRange(document)
+ const classNames = findClassNamesInRange(document, undefined, 'css')
let diagnostics: Diagnostic[] = classNames
.map(({ className, range }) => {
diff --git a/src/lsp/providers/hoverProvider.ts b/src/lsp/providers/hoverProvider.ts
index 7e27d9ac9833ec166a6332607b38562ec0b03f80..a9010a3142cb4e9700f63e3bd06426a144b9afa0 100644
--- a/src/lsp/providers/hoverProvider.ts
+++ b/src/lsp/providers/hoverProvider.ts
@@ -1,16 +1,10 @@
-import { State, DocumentClassName } from '../util/state'
+import { State } from '../util/state'
import { Hover, TextDocumentPositionParams } from 'vscode-languageserver'
-import {
- getClassNameAtPosition,
- getClassNameParts,
-} from '../util/getClassNameAtPosition'
+import { getClassNameParts } from '../util/getClassNameAtPosition'
import { stringifyCss, stringifyConfigValue } from '../util/stringify'
const dlv = require('dlv')
-import { isHtmlContext } from '../util/html'
import { isCssContext } from '../util/css'
-import { isJsContext } from '../util/js'
-import { isWithinRange } from '../util/isWithinRange'
-import { findClassNamesInRange } from '../util/find'
+import { findClassNameAtPosition } from '../util/find'
export function provideHover(
state: State,
@@ -75,68 +69,26 @@ },
}
}
-function provideClassAttributeHover(
+function provideClassNameHover(
state: State,
{ textDocument, position }: TextDocumentPositionParams
): Hover {
let doc = state.editor.documents.get(textDocument.uri)
- if (
- !isHtmlContext(state, doc, position) &&
- !isJsContext(state, doc, position)
- )
- return null
-
- let hovered = getClassNameAtPosition(doc, position)
- if (!hovered) return null
-
- return classNameToHover(state, hovered)
-}
+ let className = findClassNameAtPosition(state, doc, position)
+ if (className === null) return null
-function classNameToHover(
- state: State,
- { className, range }: DocumentClassName
-): Hover {
- const parts = getClassNameParts(state, className)
+ const parts = getClassNameParts(state, className.className)
if (!parts) return null
return {
contents: {
language: 'css',
- value: stringifyCss(className, dlv(state.classNames.classNames, parts)),
+ value: stringifyCss(
+ className.className,
+ dlv(state.classNames.classNames, parts)
+ ),
},
- range,
+ range: className.range,
}
}
-
-function provideAtApplyHover(
- state: State,
- { textDocument, position }: TextDocumentPositionParams
-): Hover {
- let doc = state.editor.documents.get(textDocument.uri)
-
- if (!isCssContext(state, doc, position)) return null
-
- const classNames = findClassNamesInRange(doc, {
- start: { line: Math.max(position.line - 10, 0), character: 0 },
- end: { line: position.line + 10, character: 0 },
- })
-
- const className = classNames.find(({ range }) =>
- isWithinRange(position, range)
- )
-
- if (!className) return null
-
- return classNameToHover(state, className)
-}
-
-function provideClassNameHover(
- state: State,
- params: TextDocumentPositionParams
-): Hover {
- return (
- provideClassAttributeHover(state, params) ||
- provideAtApplyHover(state, params)
- )
-}
diff --git a/src/lsp/util/css.ts b/src/lsp/util/css.ts
index d1acbea2b4e6e166e57d7d604ac2e615d1e94a0b..e6dbd097d4b97c42c4f7f650fc1e1186d33c0047 100644
--- a/src/lsp/util/css.ts
+++ b/src/lsp/util/css.ts
@@ -1,5 +1,5 @@
import { TextDocument, Position } from 'vscode-languageserver'
-import { isInsideTag, isVueDoc, isSvelteDoc } from './html'
+import { isInsideTag, isVueDoc, isSvelteDoc, isHtmlDoc } from './html'
import { State } from './state'
export const CSS_LANGUAGES = [
@@ -9,6 +9,7 @@ 'postcss',
'sass',
'scss',
'stylus',
+ 'sugarss',
]
export function isCssDoc(state: State, doc: TextDocument): boolean {
@@ -28,7 +29,7 @@ if (isCssDoc(state, doc)) {
return true
}
- if (isVueDoc(doc) || isSvelteDoc(doc)) {
+ if (isHtmlDoc(state, doc) || isVueDoc(doc) || isSvelteDoc(doc)) {
let str = doc.getText({
start: { line: 0, character: 0 },
end: position,
diff --git a/src/lsp/util/find.ts b/src/lsp/util/find.ts
index 17b6a127654ae8b748941e828167512078286361..800d0a3ce5dd1d133e5dfb1e246faac7abf1ca1c 100644
--- a/src/lsp/util/find.ts
+++ b/src/lsp/util/find.ts
@@ -1,6 +1,11 @@
import { TextDocument, Range, Position } from 'vscode-languageserver'
-import { DocumentClassName, DocumentClassList } from './state'
+import { DocumentClassName, DocumentClassList, State } from './state'
import lineColumn from 'line-column'
+import { isCssContext } from './css'
+import { isHtmlContext } from './html'
+import { isWithinRange } from './isWithinRange'
+import { isJsContext } from './js'
+import { getClassAttributeLexer } from './lexers'
export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
let match: RegExpMatchArray
@@ -19,62 +24,12 @@ }
return matches[matches.length - 1]
}
-export function arrFindLast<T>(arr: T[], predicate: (item: T) => boolean): T {
- for (let i = arr.length - 1; i >= 0; --i) {
- const x = arr[i]
- if (predicate(x)) {
- return x
- }
- }
- return null
-}
-
-enum Quote {
- SINGLE = "'",
- DOUBLE = '"',
- TICK = '`',
-}
-type StringInfo = {
- start: number
- end?: number
- char: Quote
-}
-
-export function findJsxStrings(str: string): StringInfo[] {
- const chars = str.split('')
- const strings: StringInfo[] = []
- let bracketCount = 0
- for (let i = 0; i < chars.length; i++) {
- const char = chars[i]
- if (char === '{') {
- bracketCount += 1
- } else if (char === '}') {
- bracketCount -= 1
- } else if (
- char === Quote.SINGLE ||
- char === Quote.DOUBLE ||
- char === Quote.TICK
- ) {
- let open = arrFindLast(strings, (string) => string.char === char)
- if (strings.length === 0 || !open || (open && open.end)) {
- strings.push({ start: i + 1, char })
- } else {
- open.end = i
- }
- }
- if (i !== 0 && bracketCount === 0) {
- // end
- break
- }
- }
- return strings
-}
-
export function findClassNamesInRange(
doc: TextDocument,
- range?: Range
+ range?: Range,
+ mode?: 'html' | 'css'
): DocumentClassName[] {
- const classLists = findClassListsInRange(doc, range)
+ const classLists = findClassListsInRange(doc, range, mode)
return [].concat.apply(
[],
classLists.map(({ classList, range }) => {
@@ -109,7 +64,7 @@ })
)
}
-export function findClassListsInRange(
+export function findClassListsInCssRange(
doc: TextDocument,
range?: Range
): DocumentClassList[] {
@@ -139,7 +94,146 @@ }
})
}
+export function findClassListsInHtmlRange(
+ doc: TextDocument,
+ range: Range
+): DocumentClassList[] {
+ const text = doc.getText(range)
+ const matches = findAll(/[\s:]class(?:Name)?=['"`{]/g, text)
+ const result: DocumentClassList[] = []
+
+ matches.forEach((match) => {
+ const subtext = text.substr(match.index + match[0].length - 1, 200)
+
+ let lexer = getClassAttributeLexer()
+ lexer.reset(subtext)
+
+ let classLists: { value: string; offset: number }[] = []
+ let token: moo.Token
+ let currentClassList: { value: string; offset: number }
+
+ try {
+ for (let token of lexer) {
+ if (token.type === 'classlist') {
+ if (currentClassList) {
+ currentClassList.value += token.value
+ } else {
+ currentClassList = {
+ value: token.value,
+ offset: token.offset,
+ }
+ }
+ } else {
+ if (currentClassList) {
+ classLists.push({
+ value: currentClassList.value,
+ offset: currentClassList.offset,
+ })
+ }
+ currentClassList = undefined
+ }
+ }
+ } catch (_) {}
+
+ if (currentClassList) {
+ classLists.push({
+ value: currentClassList.value,
+ offset: currentClassList.offset,
+ })
+ }
+
+ result.push(
+ ...classLists
+ .map(({ value, offset }) => {
+ if (value.trim() === '') {
+ return null
+ }
+
+ const before = value.match(/^\s*/)
+ const beforeOffset = before === null ? 0 : before[0].length
+ const after = value.match(/\s*$/)
+ const afterOffset = after === null ? 0 : -after[0].length
+
+ const start = indexToPosition(
+ text,
+ match.index + match[0].length - 1 + offset + beforeOffset
+ )
+ const end = indexToPosition(
+ text,
+ match.index +
+ match[0].length -
+ 1 +
+ offset +
+ value.length +
+ afterOffset
+ )
+
+ return {
+ classList: value,
+ range: {
+ start: {
+ line: range.start.line + start.line,
+ character: range.start.character + start.character,
+ },
+ end: {
+ line: range.start.line + end.line,
+ character: range.start.character + end.character,
+ },
+ },
+ }
+ })
+ .filter((x) => x !== null)
+ )
+ })
+
+ return result
+}
+
+export function findClassListsInRange(
+ doc: TextDocument,
+ range: Range,
+ mode: 'html' | 'css'
+): DocumentClassList[] {
+ if (mode === 'css') {
+ return findClassListsInCssRange(doc, range)
+ }
+ return findClassListsInHtmlRange(doc, range)
+}
+
function indexToPosition(str: string, index: number): Position {
const { line, col } = lineColumn(str + '\n', index)
return { line: line - 1, character: col - 1 }
}
+
+export function findClassNameAtPosition(
+ state: State,
+ doc: TextDocument,
+ position: Position
+): DocumentClassName {
+ let classNames = []
+ const searchRange = {
+ start: { line: Math.max(position.line - 10, 0), character: 0 },
+ end: { line: position.line + 10, character: 0 },
+ }
+
+ if (isCssContext(state, doc, position)) {
+ classNames = findClassNamesInRange(doc, searchRange, 'css')
+ } else if (
+ isHtmlContext(state, doc, position) ||
+ isJsContext(state, doc, position)
+ ) {
+ classNames = findClassNamesInRange(doc, searchRange, 'html')
+ }
+
+ if (classNames.length === 0) {
+ return null
+ }
+
+ const className = classNames.find(({ range }) =>
+ isWithinRange(position, range)
+ )
+
+ if (!className) return null
+
+ return className
+}
diff --git a/src/lsp/util/js.ts b/src/lsp/util/js.ts
index 8a62a5ff69c8ff4869e8541c6d0eac40a9b1744e..495ec5d828cd123e241f579d31711b541ded73a5 100644
--- a/src/lsp/util/js.ts
+++ b/src/lsp/util/js.ts
@@ -6,6 +6,7 @@ export const JS_LANGUAGES = [
'javascript',
'javascriptreact',
'reason',
+ 'typescript',
'typescriptreact',
]
diff --git a/src/lsp/util/lazy.ts b/src/lsp/util/lazy.ts
new file mode 100644
index 0000000000000000000000000000000000000000..858dac58c1fcc51d0bddc53027a47816db0e942e
--- /dev/null
+++ b/src/lsp/util/lazy.ts
@@ -0,0 +1,19 @@
+// https://www.codementor.io/@agustinchiappeberrini/lazy-evaluation-and-javascript-a5m7g8gs3
+
+export interface Lazy<T> {
+ (): T
+ isLazy: boolean
+}
+
+export const lazy = <T>(getter: () => T): Lazy<T> => {
+ let evaluated: boolean = false
+ let _res: T = null
+ const res = <Lazy<T>>function (): T {
+ if (evaluated) return _res
+ _res = getter.apply(this, arguments)
+ evaluated = true
+ return _res
+ }
+ res.isLazy = true
+ return res
+}
diff --git a/src/lsp/util/lexers.ts b/src/lsp/util/lexers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..65197b943a3ef18cb7b5ab1435c10235bca67bbe
--- /dev/null
+++ b/src/lsp/util/lexers.ts
@@ -0,0 +1,53 @@
+import moo from 'moo'
+import { lazy } from './lazy'
+
+const classAttributeStates: { [x: string]: moo.Rules } = {
+ doubleClassList: {
+ lbrace: { match: /(?<!\\)\{/, push: 'interp' },
+ rbrace: { match: /(?<!\\)\}/, pop: 1 },
+ end: { match: /(?<!\\)"/, pop: 1 },
+ classlist: { match: /[\s\S]/, lineBreaks: true },
+ },
+ singleClassList: {
+ lbrace: { match: /(?<!\\)\{/, push: 'interp' },
+ rbrace: { match: /(?<!\\)\}/, pop: 1 },
+ end: { match: /(?<!\\)'/, pop: 1 },
+ classlist: { match: /[\s\S]/, lineBreaks: true },
+ },
+ tickClassList: {
+ lbrace: { match: /(?<=(?<!\\)\$)\{/, push: 'interp' },
+ rbrace: { match: /(?<!\\)\}/, pop: 1 },
+ end: { match: /(?<!\\)`/, pop: 1 },
+ classlist: { match: /[\s\S]/, lineBreaks: true },
+ },
+ interp: {
+ startSingle: { match: /(?<!\\)'/, push: 'singleClassList' },
+ startDouble: { match: /(?<!\\)"/, push: 'doubleClassList' },
+ startTick: { match: /(?<!\\)`/, push: 'tickClassList' },
+ lbrace: { match: /(?<!\\)\{/, push: 'interp' },
+ rbrace: { match: /(?<!\\)\}/, pop: 1 },
+ text: { match: /[\s\S]/, lineBreaks: true },
+ },
+}
+
+export const getClassAttributeLexer = lazy(() =>
+ moo.states({
+ main: {
+ start1: { match: '"', push: 'doubleClassList' },
+ start2: { match: "'", push: 'singleClassList' },
+ start3: { match: '{', push: 'interp' },
+ },
+ ...classAttributeStates,
+ })
+)
+
+export const getComputedClassAttributeLexer = lazy(() =>
+ moo.states({
+ main: {
+ quote: { match: /['"{]/, push: 'interp' },
+ },
+ // TODO: really this should use a different interp definition that is
+ // terminated correctly based on the initial quote type
+ ...classAttributeStates,
+ })
+)