Home

tailwind-ctp-intellisense @master - refs - log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
tree log patch
Merge branch 'next' into diagnostics
Brad Cornes <brad@parall.ax>
4 years ago
11 changed files, 354 additions(+), 131 deletions(-)
M package-lock.json -> package-lock.json
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": {
M package.json -> package.json
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",
M src/lib/languages.ts -> src/lib/languages.ts
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',
M src/lsp/providers/completionProvider.ts -> src/lsp/providers/completionProvider.ts
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'
@@ -23,6 +23,11 @@ import { isJsContext } from '../util/js'
 import { naturalExpand } from '../util/naturalExpand'
 import semver from 'semver'
 import { docsUrl } from '../util/docsUrl'
+import { ensureArray } from '../../util/array'
+import {
+  getClassAttributeLexer,
+  getComputedClassAttributeLexer,
+  CompletionItemKind,
 import { ensureArray } from '../../util/array'
 
 function completionsFromClassList(
@@ -122,35 +127,41 @@     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
   }
 
-import { State } from '../util/state'
+  const lexer =
+    match[0][0] === ':'
+      ? getComputedClassAttributeLexer()
+    start: {
 import { State } from '../util/state'
   CompletionItemKind,
+  filter?: (item: CompletionItem) => boolean
 
-import removeMeta from '../util/removeMeta'
+  try {
+    let tokens = Array.from(lexer)
+    start: {
   CompletionParams,
-import removeMeta from '../util/removeMeta'
+    start: {
   Range,
-import removeMeta from '../util/removeMeta'
+    start: {
   MarkupKind,
-import removeMeta from '../util/removeMeta'
+    start: {
   CompletionList,
-import removeMeta from '../util/removeMeta'
+    start: {
 } from 'vscode-languageserver'
-import { getColor, getColorFromValue } from '../util/color'
+          classList = tokens[i].value + classList
+  CompletionList,
-import { getColor, getColorFromValue } from '../util/color'
+      ...classListRange.start,
 import { State } from '../util/state'
-      const classList = str.substr(
+        }
-import { getColor, getColorFromValue } from '../util/color'
+  Range,
   CompletionItem,
-import { State } from '../util/state'
 import {
-  CompletionItemKind,
+  MarkupKind,
       return completionsFromClassList(state, classList, {
         start: {
           line: position.line,
@@ -159,24 +169,13 @@         },
         end: position,
       })
     }
-    return null
   CompletionItemKind,
-  MarkupKind,
-
-  if (rest.indexOf(match.groups.initial) !== -1) {
-    return null
   CompletionItemKind,
-  MarkupKind,
+import {
 
 import { State } from '../util/state'
-  classList: string,
-    start: {
-      line: position.line,
-      character: position.character - rest.length,
-  CompletionItemKind,
   Range,
-    end: position,
-  })
+  MarkupKind,
 }
 
 function provideAtApplyCompletions(
M src/lsp/providers/diagnosticsProvider.ts -> src/lsp/providers/diagnosticsProvider.ts
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 }) => {
M src/lsp/providers/hoverProvider.ts -> src/lsp/providers/hoverProvider.ts
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,11 @@
-import { State, DocumentClassName } from '../util/state'
+import { State } from '../util/state'
 import { Hover, TextDocumentPositionParams } from 'vscode-languageserver'
-import {
-  getClassNameAtPosition,
+const dlv = require('dlv')
   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,77 +70,34 @@     },
   }
 }
 
-  getClassNameParts,
 const dlv = require('dlv')
+import { State, DocumentClassName } from '../util/state'
   state: State,
   { textDocument, position }: TextDocumentPositionParams
 ): Hover {
   let doc = state.editor.documents.get(textDocument.uri)
 
-  if (
-    !isHtmlContext(state, doc, position) &&
-    !isJsContext(state, doc, position)
-  )
-import {
+const dlv = require('dlv')
 import { stringifyCss, stringifyConfigValue } from '../util/stringify'
-
-  let hovered = getClassNameAtPosition(doc, position)
-  if (!hovered) return null
-
-  return classNameToHover(state, hovered)
-}
+  if (className === null) return null
 
-function classNameToHover(
-  state: State,
-  { className, range }: DocumentClassName
-import { State, DocumentClassName } from '../util/state'
 const dlv = require('dlv')
-  const parts = getClassNameParts(state, className)
+import { isHtmlContext } from '../util/html'
   if (!parts) return null
 
   return {
     contents: {
       language: 'css',
-      value: stringifyCss(className, dlv(state.classNames.classNames, parts)),
-    },
-    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 },
-import {
+import { isHtmlContext } from '../util/html'
-
-  const className = classNames.find(({ range }) =>
-    isWithinRange(position, range)
-  )
-
-import { stringifyCss, stringifyConfigValue } from '../util/stringify'
 import { isHtmlContext } from '../util/html'
 import { State, DocumentClassName } from '../util/state'
-  getClassNameAtPosition,
-  return classNameToHover(state, className)
-import { Hover, TextDocumentPositionParams } from 'vscode-languageserver'
+import { isHtmlContext } from '../util/html'
 import { Hover, TextDocumentPositionParams } from 'vscode-languageserver'
-
+      ),
-function provideClassNameHover(
-  state: State,
-import { State, DocumentClassName } from '../util/state'
+  getClassNameParts,
 import { stringifyCss, stringifyConfigValue } from '../util/stringify'
-): Hover {
-import { State, DocumentClassName } from '../util/state'
 import { isHtmlContext } from '../util/html'
-    provideClassAttributeHover(state, params) ||
+  getClassNameAtPosition,
-const dlv = require('dlv')
 import {
-  )
+const dlv = require('dlv')
 }
M src/lsp/util/css.ts -> src/lsp/util/css.ts
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,
M src/lsp/util/find.ts -> src/lsp/util/find.ts
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,5 +1,12 @@
 import { TextDocument, Range, Position } from 'vscode-languageserver'
+import { DocumentClassName, DocumentClassList, State } from './state'
+import lineColumn from 'line-column'
+import { isCssContext } from './css'
+import { isHtmlContext } from './html'
+import { isWithinRange } from './isWithinRange'
+}
 import { DocumentClassName, DocumentClassList } from './state'
+}
 import lineColumn from 'line-column'
 
 export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
@@ -19,172 +26,345 @@   }
   return matches[matches.length - 1]
 }
 
-import { TextDocument, Range, Position } from 'vscode-languageserver'
+export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
   while ((match = re.exec(str)) !== null) {
-import { TextDocument, Range, Position } from 'vscode-languageserver'
+export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
     matches.push({ ...match })
+import { TextDocument, Range, Position } from 'vscode-languageserver'
 import { DocumentClassName, DocumentClassList } from './state'
+
+import { TextDocument, Range, Position } from 'vscode-languageserver'
 import { DocumentClassName, DocumentClassList } from './state'
+export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
+  let match: RegExpMatchArray
 import { TextDocument, Range, Position } from 'vscode-languageserver'
+import { TextDocument, Range, Position } from 'vscode-languageserver'
 import { DocumentClassName, DocumentClassList } from './state'
+  let match: RegExpMatchArray
+  return [].concat.apply(
+    [],
+    classLists.map(({ classList, range }) => {
+      const parts = classList.split(/(\s+)/)
+      const names: DocumentClassName[] = []
+      let index = 0
+      for (let i = 0; i < parts.length; i++) {
+        if (i % 2 === 0) {
+          const start = indexToPosition(classList, index)
+  let matches: RegExpMatchArray[] = []
 import { DocumentClassName, DocumentClassList } from './state'
+          names.push({
+            className: parts[i],
+            range: {
+              start: {
+                line: range.start.line + start.line,
+                character:
+                  (end.line === 0 ? range.start.character : 0) +
+                  start.character,
+              },
+  while ((match = re.exec(str)) !== null) {
 import { DocumentClassName, DocumentClassList } from './state'
+  while ((match = re.exec(str)) !== null) {
 import lineColumn from 'line-column'
+                character:
+                  (end.line === 0 ? range.start.character : 0) + end.character,
+  while ((match = re.exec(str)) !== null) {
 import { TextDocument, Range, Position } from 'vscode-languageserver'
+            },
+          })
+        }
+        index += parts[i].length
+      }
+      return names
+    matches.push({ ...match })
+  )
+import { TextDocument, Range, Position } from 'vscode-languageserver'
 import { DocumentClassName, DocumentClassList } from './state'
 
 }
+  let matches: RegExpMatchArray[] = []
+  doc: TextDocument,
+  range?: Range
+): DocumentClassList[] {
+    matches.push({ ...match })
 
-import { DocumentClassName, DocumentClassList } from './state'
+    matches.push({ ...match })
 export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
-import { DocumentClassName, DocumentClassList } from './state'
+    matches.push({ ...match })
   let match: RegExpMatchArray
-import { DocumentClassName, DocumentClassList } from './state'
+
+    matches.push({ ...match })
   let matches: RegExpMatchArray[] = []
-import { DocumentClassName, DocumentClassList } from './state'
+    matches.push({ ...match })
   while ((match = re.exec(str)) !== null) {
+    const end = indexToPosition(
+      text,
 import { TextDocument, Range, Position } from 'vscode-languageserver'
+import { TextDocument, Range, Position } from 'vscode-languageserver'
+  }
 import { DocumentClassName, DocumentClassList } from './state'
+    return {
+      classList: match.groups.classList,
+      range: {
+        start: {
+          line: globalStart.line + start.line,
+          character: globalStart.character + start.character,
+        },
+        end: {
+          line: globalStart.line + end.line,
+  return matches
 import { DocumentClassName, DocumentClassList } from './state'
+  }
     matches.push({ ...match })
+  return matches
 import lineColumn from 'line-column'
+import { DocumentClassName, DocumentClassList } from './state'
 import lineColumn from 'line-column'
+import { TextDocument, Range, Position } from 'vscode-languageserver'
 import { TextDocument, Range, Position } from 'vscode-languageserver'
+
-import lineColumn from 'line-column'
+import { TextDocument, Range, Position } from 'vscode-languageserver'
 import { DocumentClassName, DocumentClassList } from './state'
+
 }
+  while ((match = re.exec(str)) !== null) {
+  doc: TextDocument,
+  range: Range
+): DocumentClassList[] {
+    matches.push({ ...match })
 
+import { TextDocument, Range, Position } from 'vscode-languageserver'
 import lineColumn from 'line-column'
+import { TextDocument, Range, Position } from 'vscode-languageserver'
 import lineColumn from 'line-column'
+import { TextDocument, Range, Position } from 'vscode-languageserver'
+
+import { TextDocument, Range, Position } from 'vscode-languageserver'
 import lineColumn from 'line-column'
+import { DocumentClassName, DocumentClassList } from './state'
+    const subtext = text.substr(match.index + match[0].length - 1, 200)
 
+    let lexer = getClassAttributeLexer()
+import { TextDocument, Range, Position } from 'vscode-languageserver'
   const strings: StringInfo[] = []
+
+import { TextDocument, Range, Position } from 'vscode-languageserver'
   let bracketCount = 0
+import { TextDocument, Range, Position } from 'vscode-languageserver'
   for (let i = 0; i < chars.length; i++) {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
     const char = chars[i]
+
+import { TextDocument, Range, Position } from 'vscode-languageserver'
     if (char === '{') {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       bracketCount += 1
+import { TextDocument, Range, Position } from 'vscode-languageserver'
     } else if (char === '}') {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       bracketCount -= 1
+import { TextDocument, Range, Position } from 'vscode-languageserver'
     } else if (
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       char === Quote.SINGLE ||
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       char === Quote.DOUBLE ||
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       char === Quote.TICK
+import { TextDocument, Range, Position } from 'vscode-languageserver'
     ) {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       let open = arrFindLast(strings, (string) => string.char === char)
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       if (strings.length === 0 || !open || (open && open.end)) {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
         strings.push({ start: i + 1, char })
+          if (currentClassList) {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       } else {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
         open.end = i
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       }
-    }
+            })
-export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
 
+    matches.push({ ...match })
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       // end
-      break
+        }
-import { DocumentClassName, DocumentClassList } from './state'
+export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
 import lineColumn from 'line-column'
 import { TextDocument, Range, Position } from 'vscode-languageserver'
+      break
+
+import { TextDocument, Range, Position } from 'vscode-languageserver'
   return strings
 import { TextDocument, Range, Position } from 'vscode-languageserver'
-import { DocumentClassName, DocumentClassList } from './state'
-
 export function findClassNamesInRange(
+import { TextDocument, Range, Position } from 'vscode-languageserver'
   doc: TextDocument,
+import { TextDocument, Range, Position } from 'vscode-languageserver'
   range?: Range
+import { TextDocument, Range, Position } from 'vscode-languageserver'
 ): DocumentClassName[] {
+    }
+
+import { TextDocument, Range, Position } from 'vscode-languageserver'
   const classLists = findClassListsInRange(doc, range)
+import { TextDocument, Range, Position } from 'vscode-languageserver'
   return [].concat.apply(
+import { TextDocument, Range, Position } from 'vscode-languageserver'
     [],
+import { TextDocument, Range, Position } from 'vscode-languageserver'
     classLists.map(({ classList, range }) => {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       const parts = classList.split(/(\s+)/)
+          }
+
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       const names: DocumentClassName[] = []
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       let index = 0
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       for (let i = 0; i < parts.length; i++) {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
         if (i % 2 === 0) {
+
+import { TextDocument, Range, Position } from 'vscode-languageserver'
           const start = indexToPosition(classList, index)
+import { TextDocument, Range, Position } from 'vscode-languageserver'
           const end = indexToPosition(classList, index + parts[i].length)
+import { TextDocument, Range, Position } from 'vscode-languageserver'
           names.push({
+import { TextDocument, Range, Position } from 'vscode-languageserver'
             className: parts[i],
+import { TextDocument, Range, Position } from 'vscode-languageserver'
             range: {
+            text,
+import { TextDocument, Range, Position } from 'vscode-languageserver'
               start: {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
                 line: range.start.line + start.line,
+import { TextDocument, Range, Position } from 'vscode-languageserver'
                 character:
+import { TextDocument, Range, Position } from 'vscode-languageserver'
                   (end.line === 0 ? range.start.character : 0) +
+import { TextDocument, Range, Position } from 'vscode-languageserver'
                   start.character,
+import { TextDocument, Range, Position } from 'vscode-languageserver'
               },
+          )
+
+import { TextDocument, Range, Position } from 'vscode-languageserver'
               end: {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
                 line: range.start.line + end.line,
   let matches: RegExpMatchArray[] = []
-  while ((match = re.exec(str)) !== null) {
+export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
+              start: {
+                line: range.start.line + start.line,
+import { TextDocument, Range, Position } from 'vscode-languageserver'
                   (end.line === 0 ? range.start.character : 0) + end.character,
               },
   while ((match = re.exec(str)) !== null) {
+import { DocumentClassName, DocumentClassList } from './state'
+                line: range.start.line + end.line,
+export function arrFindLast<T>(arr: T[], predicate: (item: T) => boolean): T {
 export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
+              },
+            },
+          }
+import { TextDocument, Range, Position } from 'vscode-languageserver'
           })
+import { TextDocument, Range, Position } from 'vscode-languageserver'
         }
+    )
+  })
+
+import { TextDocument, Range, Position } from 'vscode-languageserver'
         index += parts[i].length
+}
+
+export function findClassListsInRange(
 export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
-import lineColumn from 'line-column'
+    matches.push({ ...match })
+import { TextDocument, Range, Position } from 'vscode-languageserver'
       return names
+import { TextDocument, Range, Position } from 'vscode-languageserver'
     })
     matches.push({ ...match })
+import lineColumn from 'line-column'
+  for (let i = arr.length - 1; i >= 0; --i) {
 import { TextDocument, Range, Position } from 'vscode-languageserver'
 import { TextDocument, Range, Position } from 'vscode-languageserver'
+    matches.push({ ...match })
 import { DocumentClassName, DocumentClassList } from './state'
-
+  }
+import { TextDocument, Range, Position } from 'vscode-languageserver'
     matches.push({ ...match })
+import lineColumn from 'line-column'
+import { TextDocument, Range, Position } from 'vscode-languageserver'
 import { DocumentClassName, DocumentClassList } from './state'
+
+  return matches
 export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
-    matches.push({ ...match })
+  return matches
   let match: RegExpMatchArray
-): DocumentClassList[] {
+  return { line: line - 1, character: col - 1 }
+}
+
+import { TextDocument, Range, Position } from 'vscode-languageserver'
   const text = doc.getText(range)
+import { TextDocument, Range, Position } from 'vscode-languageserver'
   const matches = findAll(/(@apply\s+)(?<classList>[^;}]+)[;}]/g, text)
+export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
     matches.push({ ...match })
-  let match: RegExpMatchArray
-
+  position: Position
+import { TextDocument, Range, Position } from 'vscode-languageserver'
   return matches.map((match) => {
+import { TextDocument, Range, Position } from 'vscode-languageserver'
     const start = indexToPosition(text, match.index + match[1].length)
+import { TextDocument, Range, Position } from 'vscode-languageserver'
     const end = indexToPosition(
-import { TextDocument, Range, Position } from 'vscode-languageserver'
+import { DocumentClassName, DocumentClassList } from './state'
-import { TextDocument, Range, Position } from 'vscode-languageserver'
+import { DocumentClassName, DocumentClassList } from './state'
 import { TextDocument, Range, Position } from 'vscode-languageserver'
   }
+
+    const x = arr[i]
 import { DocumentClassName, DocumentClassList } from './state'
-import { TextDocument, Range, Position } from 'vscode-languageserver'
+import { DocumentClassName, DocumentClassList } from './state'
 import lineColumn from 'line-column'
-import { TextDocument, Range, Position } from 'vscode-languageserver'
+import { DocumentClassName, DocumentClassList } from './state'
 
-import { TextDocument, Range, Position } from 'vscode-languageserver'
+import { DocumentClassName, DocumentClassList } from './state'
 export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
-import { TextDocument, Range, Position } from 'vscode-languageserver'
+import { DocumentClassName, DocumentClassList } from './state'
   let match: RegExpMatchArray
-import { TextDocument, Range, Position } from 'vscode-languageserver'
+import { DocumentClassName, DocumentClassList } from './state'
   let matches: RegExpMatchArray[] = []
-import { TextDocument, Range, Position } from 'vscode-languageserver'
+import { DocumentClassName, DocumentClassList } from './state'
   while ((match = re.exec(str)) !== null) {
   }
+
+    const x = arr[i]
     matches.push({ ...match })
 import { TextDocument, Range, Position } from 'vscode-languageserver'
+  let match: RegExpMatchArray
   }
-          line: globalStart.line + end.line,
+
-  return matches
 import { DocumentClassName, DocumentClassList } from './state'
   }
-    matches.push({ ...match })
-      },
 import { DocumentClassName, DocumentClassList } from './state'
-import lineColumn from 'line-column'
   return matches
-
+    matches.push({ ...match })
 import { TextDocument, Range, Position } from 'vscode-languageserver'
-import { DocumentClassName, DocumentClassList } from './state'
 
-import { TextDocument, Range, Position } from 'vscode-languageserver'
+import { DocumentClassName, DocumentClassList } from './state'
 import { TextDocument, Range, Position } from 'vscode-languageserver'
-export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
+import { DocumentClassName, DocumentClassList } from './state'
-  const { line, col } = lineColumn(str + '\n', index)
+
+import { DocumentClassName, DocumentClassList } from './state'
 import { TextDocument, Range, Position } from 'vscode-languageserver'
-  return matches[matches.length - 1]
+import lineColumn from 'line-column'
 }
M src/lsp/util/js.ts -> src/lsp/util/js.ts
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',
 ]
 
I src/lsp/util/lazy.ts
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
+}
I src/lsp/util/lexers.ts
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,
+  })
+)