Home

tailwind-ctp-intellisense @master - refs - log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
tree log patch
Merge branch 'color-decorators'
Brad Cornes <bradlc41@gmail.com>
4 years ago
10 changed files, 347 additions(+), 28 deletions(-)
package-lock.jsonpackage.jsonsrc/extension.tssrc/lib/registerColorDecorator.tssrc/lsp/providers/documentColorProvider.tssrc/lsp/server.tssrc/lsp/util/color.tssrc/lsp/util/find.tssrc/lsp/util/resolveRange.tssrc/lsp/util/state.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 3617ee8e60c972655beb158107c47bb0ba1723bd..f79d333c85f536da05d9bdf000525a25dbbe4168 100755
--- a/package-lock.json
+++ b/package-lock.json
@@ -1019,6 +1019,12 @@ 			"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
 			"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
 			"dev": true
 		},
+		"@types/debounce": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.0.tgz",
+			"integrity": "sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw==",
+			"dev": true
+		},
 		"@types/graceful-fs": {
 			"version": "4.1.3",
 			"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz",
@@ -1958,6 +1964,12 @@ 		"date-fns": {
 			"version": "2.11.0",
 			"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.11.0.tgz",
 			"integrity": "sha512-8P1cDi8ebZyDxUyUprBXwidoEtiQAawYPGvpfb+Dg0G6JrQ+VozwOmm91xYC0vAv1+0VmLehEPb+isg4BGUFfA==",
+			"dev": true
+		},
+		"debounce": {
+			"version": "1.2.0",
+			"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
+			"integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==",
 			"dev": true
 		},
 		"debug": {
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
31
32
33
34
35
36
37
38
39
40
41
42
43
diff --git a/package.json b/package.json
index 0cdb709c6539c495c4114edaaf2222479b237252..7815ce18bfdbad0cb2f858a749d55aecc269c885 100755
--- a/package.json
+++ b/package.json
@@ -71,6 +71,22 @@           },
           "default": {},
           "markdownDescription": "Enable features in languages that are not supported by default. Add a mapping here between the new language and an already supported language.\n E.g.: `{\"plaintext\": \"html\"}`"
         },
+        "tailwindCSS.colorDecorators": {
+          "type": "string",
+          "enum": [
+            "inherit",
+            "on",
+            "off"
+          ],
+          "markdownEnumDescriptions": [
+            "Color decorators are rendered if `editor.colorDecorators` is enabled.",
+            "Color decorators are rendered.",
+            "Color decorators are not rendered."
+          ],
+          "default": "inherit",
+          "markdownDescription": "Controls whether the editor should render inline color decorators for Tailwind CSS classes and helper functions.",
+          "scope": "language-overridable"
+        },
         "tailwindCSS.validate": {
           "type": "boolean",
           "default": true,
@@ -157,6 +173,7 @@     "test": "jest"
   },
   "devDependencies": {
     "@ctrl/tinycolor": "^3.1.0",
+    "@types/debounce": "^1.2.0",
     "@types/mocha": "^5.2.0",
     "@types/moo": "^0.5.3",
     "@types/node": "^13.9.3",
@@ -166,6 +183,7 @@     "callsite": "^1.0.0",
     "chokidar": "^3.3.1",
     "concurrently": "^5.1.0",
     "css.escape": "^1.5.1",
+    "debounce": "^1.2.0",
     "detect-indent": "^6.0.0",
     "dlv": "^1.1.3",
     "dset": "^2.0.1",
M src/extension.tssrc/extension.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
diff --git a/src/extension.ts b/src/extension.ts
index 279ec969590ba1e30120474c179722fa450c238d..fe982e92a2882d37e61976a3f1d2733cee6e01e2 100755
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -24,6 +24,7 @@ import isObject from './util/isObject'
 import { dedupe, equal } from './util/array'
 import { createEmitter } from './lib/emitter'
 import { onMessage } from './lsp/notifications'
+import { registerColorDecorator } from './lib/registerColorDecorator'
 
 const CLIENT_ID = 'tailwindcss-intellisense'
 const CLIENT_NAME = 'Tailwind CSS IntelliSense'
@@ -152,6 +153,7 @@ 
     client.onReady().then(() => {
       let emitter = createEmitter(client)
       registerConfigErrorHandler(emitter)
+      registerColorDecorator(client, context, emitter)
       onMessage(client, 'getConfiguration', async (scope) => {
         return Workspace.getConfiguration('tailwindCSS', scope)
       })
I src/lib/registerColorDecorator.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
diff --git a/src/lib/registerColorDecorator.ts b/src/lib/registerColorDecorator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..66d6767ee428934b351b9ccdcd884c0f236477db
--- /dev/null
+++ b/src/lib/registerColorDecorator.ts
@@ -0,0 +1,132 @@
+import { window, workspace, ExtensionContext, TextEditor } from 'vscode'
+import { NotificationEmitter } from './emitter'
+import { LanguageClient } from 'vscode-languageclient'
+import debounce from 'debounce'
+
+const colorDecorationType = window.createTextEditorDecorationType({
+  before: {
+    width: '0.8em',
+    height: '0.8em',
+    contentText: ' ',
+    border: '0.1em solid',
+    margin: '0.1em 0.2em 0',
+  },
+  dark: {
+    before: {
+      borderColor: '#eeeeee',
+    },
+  },
+  light: {
+    before: {
+      borderColor: '#000000',
+    },
+  },
+})
+
+export function registerColorDecorator(
+  client: LanguageClient,
+  context: ExtensionContext,
+  emitter: NotificationEmitter
+) {
+  let activeEditor = window.activeTextEditor
+
+  async function updateDecorations() {
+    return updateDecorationsInEditor(activeEditor)
+  }
+
+  async function updateDecorationsInEditor(editor: TextEditor) {
+    if (!editor) return
+    if (editor.document.uri.scheme !== 'file') return
+
+    let workspaceFolder = workspace.getWorkspaceFolder(editor.document.uri)
+    if (
+      !workspaceFolder ||
+      workspaceFolder.uri.toString() !==
+        client.clientOptions.workspaceFolder.uri.toString()
+    ) {
+      return
+    }
+
+    let preference =
+      workspace.getConfiguration('tailwindCSS', editor.document)
+        .colorDecorators || 'inherit'
+
+    let enabled: boolean =
+      preference === 'inherit'
+        ? Boolean(workspace.getConfiguration('editor').colorDecorators)
+        : preference === 'on'
+
+    if (!enabled) {
+      editor.setDecorations(colorDecorationType, [])
+      return
+    }
+
+    let { colors } = await emitter.emit('getDocumentColors', {
+      document: editor.document.uri.toString(),
+    })
+
+    editor.setDecorations(
+      colorDecorationType,
+      colors
+        .filter(({ color }) => color !== 'rgba(0, 0, 0, 0.01)')
+        .map(({ range, color }) => ({
+          range,
+          renderOptions: { before: { backgroundColor: color } },
+        }))
+    )
+  }
+
+  const triggerUpdateDecorations = debounce(updateDecorations, 200)
+
+  if (activeEditor) {
+    triggerUpdateDecorations()
+  }
+
+  window.onDidChangeActiveTextEditor(
+    (editor) => {
+      activeEditor = editor
+      if (editor) {
+        triggerUpdateDecorations()
+      }
+    },
+    null,
+    context.subscriptions
+  )
+
+  workspace.onDidChangeTextDocument(
+    (event) => {
+      if (activeEditor && event.document === activeEditor.document) {
+        triggerUpdateDecorations()
+      }
+    },
+    null,
+    context.subscriptions
+  )
+
+  workspace.onDidOpenTextDocument(
+    (document) => {
+      if (activeEditor && document === activeEditor.document) {
+        triggerUpdateDecorations()
+      }
+    },
+    null,
+    context.subscriptions
+  )
+
+  workspace.onDidChangeConfiguration((e) => {
+    if (
+      e.affectsConfiguration('editor.colorDecorators') ||
+      e.affectsConfiguration('tailwindCSS.colorDecorators')
+    ) {
+      window.visibleTextEditors.forEach(updateDecorationsInEditor)
+    }
+  })
+
+  emitter.on('configUpdated', () => {
+    window.visibleTextEditors.forEach(updateDecorationsInEditor)
+  })
+
+  emitter.on('configError', () => {
+    window.visibleTextEditors.forEach(updateDecorationsInEditor)
+  })
+}
I src/lsp/providers/documentColorProvider.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
diff --git a/src/lsp/providers/documentColorProvider.ts b/src/lsp/providers/documentColorProvider.ts
new file mode 100644
index 0000000000000000000000000000000000000000..688ee746ab6a01dca0217c5515e14e599bb1f235
--- /dev/null
+++ b/src/lsp/providers/documentColorProvider.ts
@@ -0,0 +1,49 @@
+import { onMessage } from '../notifications'
+import { State } from '../util/state'
+import {
+  findClassListsInDocument,
+  getClassNamesInClassList,
+  findHelperFunctionsInDocument,
+} from '../util/find'
+import { getClassNameParts } from '../util/getClassNameAtPosition'
+import { getColor, getColorFromValue } from '../util/color'
+import { stringToPath } from '../util/stringToPath'
+const dlv = require('dlv')
+
+export function registerDocumentColorProvider(state: State) {
+  onMessage(
+    state.editor.connection,
+    'getDocumentColors',
+    async ({ document }) => {
+      let colors = []
+      if (!state.enabled) return { colors }
+      let doc = state.editor.documents.get(document)
+      if (!doc) return { colors }
+
+      let classLists = findClassListsInDocument(state, doc)
+      classLists.forEach((classList) => {
+        let classNames = getClassNamesInClassList(classList)
+        classNames.forEach((className) => {
+          let parts = getClassNameParts(state, className.className)
+          if (!parts) return
+          let color = getColor(state, parts)
+          if (!color) return
+          colors.push({ range: className.range, color: color.documentation })
+        })
+      })
+
+      let helperFns = findHelperFunctionsInDocument(state, doc)
+      helperFns.forEach((fn) => {
+        let keys = stringToPath(fn.value)
+        let base = fn.helper === 'theme' ? ['theme'] : []
+        let value = dlv(state.config, [...base, ...keys])
+        let color = getColorFromValue(value)
+        if (color) {
+          colors.push({ range: fn.valueRange, color })
+        }
+      })
+
+      return { colors }
+    }
+  )
+}
M src/lsp/server.tssrc/lsp/server.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
diff --git a/src/lsp/server.ts b/src/lsp/server.ts
index 4b149f82663e120586d51e1026c6f9f8eaf552f7..c4cb2d1dab02b93616d6f761d90e1ef7050dadd9 100644
--- a/src/lsp/server.ts
+++ b/src/lsp/server.ts
@@ -35,9 +35,10 @@   clearAllDiagnostics,
 } from './providers/diagnostics/diagnosticsProvider'
 import { createEmitter } from '../lib/emitter'
 import { provideCodeActions } from './providers/codeActions/codeActionProvider'
+import { registerDocumentColorProvider } from './providers/documentColorProvider'
 
 let connection = createConnection(ProposedFeatures.all)
-let state: State = { enabled: false, emitter: createEmitter(connection) }
+const state: State = { enabled: false, emitter: createEmitter(connection) }
 let documents = new TextDocuments()
 let workspaceFolder: string | null
 
@@ -73,7 +74,7 @@ connection.onInitialize(
   async (params: InitializeParams): Promise<InitializeResult> => {
     const capabilities = params.capabilities
 
-    const editorState: EditorState = {
+    state.editor = {
       connection,
       documents,
       documentSettings,
@@ -99,12 +100,7 @@       {
         // @ts-ignore
         onChange: (newState: State): void => {
           if (newState && !newState.error) {
-            state = {
-              ...newState,
-              enabled: true,
-              emitter: state.emitter,
-              editor: editorState,
-            }
+            Object.assign(state, newState, { enabled: true })
             connection.sendNotification('tailwindcss/configUpdated', [
               state.configPath,
               state.config,
@@ -112,11 +108,7 @@               state.plugins,
             ])
             updateAllDiagnostics(state)
           } else {
-            state = {
-              enabled: false,
-              emitter: state.emitter,
-              editor: editorState,
-            }
+            state.enabled = false
             if (newState && newState.error) {
               const payload: {
                 message: string
@@ -140,14 +132,9 @@       }
     )
 
     if (tailwindState) {
-      state = {
-        enabled: true,
-        emitter: state.emitter,
-        editor: editorState,
-        ...tailwindState,
-      }
+      Object.assign(state, tailwindState, { enabled: true })
     } else {
-      state = { enabled: false, emitter: state.emitter, editor: editorState }
+      state.enabled = false
     }
 
     return {
@@ -195,6 +182,8 @@       state.configPath,
       state.config,
       state.plugins,
     ])
+
+    registerDocumentColorProvider(state)
   })
 
 connection.onDidChangeConfiguration((change) => {
M src/lsp/util/color.tssrc/lsp/util/color.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
diff --git a/src/lsp/util/color.ts b/src/lsp/util/color.ts
index 95d54170555430e9334be51c46aee88876164365..31d56ec33a369f671737f625a3d61c8960ca7d4b 100644
--- a/src/lsp/util/color.ts
+++ b/src/lsp/util/color.ts
@@ -48,17 +48,43 @@     propsToCheck.map((prop) => ensureArray(item[prop]).map(createColor))
   )
 
   // check that all of the values are valid colors
-  if (colors.some((color) => !color.isValid)) {
+  if (colors.some((color) => color !== 'transparent' && !color.isValid)) {
     return null
   }
 
-  // check that all of the values are the same color
-  const colorStrings = colors.map((color) => color.toRgbString())
-  if (dedupe(colorStrings).length !== 1) {
+  // check that all of the values are the same color, ignoring alpha
+  const colorStrings = dedupe(
+    colors.map((color) =>
+      color === 'transparent'
+        ? 'transparent'
+        : `${color.r}-${color.g}-${color.b}`
+    )
+  )
+  if (colorStrings.length !== 1) {
     return null
   }
 
-  return { documentation: colorStrings[0] }
+  if (colorStrings[0] === 'transparent') {
+    return {
+      documentation: 'rgba(0, 0, 0, 0.01)',
+    }
+  }
+
+  const nonTransparentColors = colors.filter(
+    (color): color is TinyColor => color !== 'transparent'
+  )
+
+  const alphas = dedupe(nonTransparentColors.map((color) => color.a))
+
+  if (alphas.length === 1 || (alphas.length === 2 && alphas.includes(0))) {
+    return {
+      documentation: nonTransparentColors
+        .find((color) => color.a !== 0)
+        .toRgbString(),
+    }
+  }
+
+  return null
 }
 
 export function getColorFromValue(value: unknown): string {
@@ -73,9 +99,9 @@   }
   return null
 }
 
-function createColor(str: string): TinyColor {
+function createColor(str: string): TinyColor | 'transparent' {
   if (str === 'transparent') {
-    return new TinyColor({ r: 0, g: 0, b: 0, a: 0.01 })
+    return 'transparent'
   }
 
   // matches: rgba(<r>, <g>, <b>, var(--bg-opacity))
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
diff --git a/src/lsp/util/find.ts b/src/lsp/util/find.ts
index db5609e3f06571bde42d9a555268d95f6318fc47..8ff4dedcaaa9d3ab1e20e53208b641fde0bf8a89 100644
--- a/src/lsp/util/find.ts
+++ b/src/lsp/util/find.ts
@@ -1,5 +1,10 @@
 import { TextDocument, Range, Position } from 'vscode-languageserver'
-import { DocumentClassName, DocumentClassList, State } from './state'
+import {
+  DocumentClassName,
+  DocumentClassList,
+  State,
+  DocumentHelperFunction,
+} from './state'
 import lineColumn from 'line-column'
 import { isCssContext, isCssDoc } from './css'
 import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html'
@@ -11,6 +16,7 @@   getClassAttributeLexer,
   getComputedClassAttributeLexer,
 } from './lexers'
 import { getLanguageBoundaries } from './getLanguageBoundaries'
+import { resolveRange } from './resolveRange'
 
 export function findAll(re: RegExp, str: string): RegExpMatchArray[] {
   let match: RegExpMatchArray
@@ -252,6 +258,64 @@   return flatten([
     ...boundaries.html.map((range) => findClassListsInHtmlRange(doc, range)),
     ...boundaries.css.map((range) => findClassListsInCssRange(doc, range)),
   ])
+}
+
+export function findHelperFunctionsInDocument(
+  state: State,
+  doc: TextDocument
+): DocumentHelperFunction[] {
+  if (isCssDoc(state, doc)) {
+    return findHelperFunctionsInRange(doc)
+  }
+
+  let boundaries = getLanguageBoundaries(state, doc)
+  if (!boundaries) return []
+
+  return flatten(
+    boundaries.css.map((range) => findHelperFunctionsInRange(doc, range))
+  )
+}
+
+export function findHelperFunctionsInRange(
+  doc: TextDocument,
+  range?: Range
+): DocumentHelperFunction[] {
+  const text = doc.getText(range)
+  const matches = findAll(
+    /(?<before>^|\s)(?<helper>theme|config)\((?:(?<single>')([^']+)'|(?<double>")([^"]+)")\)/gm,
+    text
+  )
+
+  return matches.map((match) => {
+    let value = match[4] || match[6]
+    let startIndex = match.index + match.groups.before.length
+    return {
+      full: match[0].substr(match.groups.before.length),
+      value,
+      helper: match.groups.helper === 'theme' ? 'theme' : 'config',
+      quotes: match.groups.single ? "'" : '"',
+      range: resolveRange(
+        {
+          start: indexToPosition(text, startIndex),
+          end: indexToPosition(text, match.index + match[0].length),
+        },
+        range
+      ),
+      valueRange: resolveRange(
+        {
+          start: indexToPosition(
+            text,
+            startIndex + match.groups.helper.length + 1
+          ),
+          end: indexToPosition(
+            text,
+            startIndex + match.groups.helper.length + 1 + 1 + value.length + 1
+          ),
+        },
+        range
+      ),
+    }
+  })
 }
 
 export function indexToPosition(str: string, index: number): Position {
I src/lsp/util/resolveRange.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
diff --git a/src/lsp/util/resolveRange.ts b/src/lsp/util/resolveRange.ts
new file mode 100644
index 0000000000000000000000000000000000000000..96fe343afd5328b288c76f2fdc26e1d08ea1b004
--- /dev/null
+++ b/src/lsp/util/resolveRange.ts
@@ -0,0 +1,18 @@
+import { Range } from 'vscode-languageserver'
+
+export function resolveRange(range: Range, relativeTo?: Range) {
+  return {
+    start: {
+      line: (relativeTo?.start.line || 0) + range.start.line,
+      character:
+        (range.end.line === 0 ? relativeTo?.start.character || 0 : 0) +
+        range.start.character,
+    },
+    end: {
+      line: (relativeTo?.start.line || 0) + range.end.line,
+      character:
+        (range.end.line === 0 ? relativeTo?.start.character || 0 : 0) +
+        range.end.character,
+    },
+  }
+}
M src/lsp/util/state.tssrc/lsp/util/state.ts
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
diff --git a/src/lsp/util/state.ts b/src/lsp/util/state.ts
index 47b976d397b70e7ac0e0d26a42ee76cb54f40374..8369e920fc74458c375f3cf1f6debd7b1a0f5350 100644
--- a/src/lsp/util/state.ts
+++ b/src/lsp/util/state.ts
@@ -75,6 +75,15 @@   relativeRange: Range
   classList: DocumentClassList
 }
 
+export type DocumentHelperFunction = {
+  full: string
+  helper: 'theme' | 'config'
+  value: string
+  quotes: '"' | "'"
+  range: Range
+  valueRange: Range
+}
+
 export type ClassNameMeta = {
   source: 'base' | 'components' | 'utilities'
   pseudo: string[]