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>
5 years ago
11 changed files, 282 additions(+), 145 deletions(-)
package-lock.jsonpackage.jsonsrc/lib/languages.tssrc/lsp/providers/completionProvider.tssrc/lsp/providers/diagnosticsProvider.tssrc/lsp/providers/hoverProvider.tssrc/lsp/util/css.tssrc/lsp/util/find.tssrc/lsp/util/js.tssrc/lsp/util/lazy.tssrc/lsp/util/lexers.ts
M package-lock.jsonpackage-lock.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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.jsonpackage.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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.tssrc/lib/languages.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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.tssrc/lsp/providers/completionProvider.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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(
M src/lsp/providers/diagnosticsProvider.tssrc/lsp/providers/diagnosticsProvider.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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.tssrc/lsp/providers/hoverProvider.ts
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
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)
-  )
-}
M src/lsp/util/css.tssrc/lsp/util/css.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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.tssrc/lsp/util/find.ts
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
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
+}
M src/lsp/util/js.tssrc/lsp/util/js.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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,
+  })
+)