Home

tailwind-ctp-intellisense @master - refs - log -
-
https://git.jolheiser.com/tailwind-ctp-intellisense.git
Tailwind intellisense + Catppuccin
tree log patch
wip quick fix for invalid @apply
Brad Cornes <bradlc41@gmail.com>
4 years ago
13 changed files, 456 additions(+), 89 deletions(-)
M package-lock.json -> package-lock.json
diff --git a/package-lock.json b/package-lock.json
index 2a7c7c50e0bcbf5cdb861fbdf992942aa1d03786..1992a2d93506b81f689a45031e694a69cb864b55 100755
--- a/package-lock.json
+++ b/package-lock.json
@@ -2032,6 +2032,12 @@ 			"resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz",
 			"integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=",
 			"dev": true
 		},
+		"detect-indent": {
+			"version": "6.0.0",
+			"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.0.0.tgz",
+			"integrity": "sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==",
+			"dev": true
+		},
 		"detect-newline": {
 			"version": "3.1.0",
 			"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
M package.json -> package.json
diff --git a/package.json b/package.json
index e696a5d0581ae8a9ac43c250bb528ff4ad59b542..bf55ce613253e1ee6350bb89244b6b5e2be37a30 100755
--- a/package.json
+++ b/package.json
@@ -167,6 +167,8 @@     "chokidar": "^3.3.1",
     "concurrently": "^5.1.0",
     "css.escape": "^1.5.1",
 {
+      {
+{
   "license": "MIT",
     "dset": "^2.0.1",
     "esm": "^3.2.25",
M src/class-names/index.js -> src/class-names/index.js
diff --git a/src/class-names/index.js b/src/class-names/index.js
index 247dc481c82b4f83e0771089cbc7bfcc27e8116a..a8565152dc6c2db91e436999bdff238ef797b61d 100644
--- a/src/class-names/index.js
+++ b/src/class-names/index.js
@@ -133,6 +133,10 @@         resolvedConfig,
         postcss,
         browserslist,
       }),
+      modules: {
+        tailwindcss,
+        postcss,
+      },
     }
   }
 
D src/lsp/providers/codeActionProvider.ts
diff --git a/src/lsp/providers/codeActionProvider.ts b/src/lsp/providers/codeActionProvider.ts
deleted file mode 100644
index 9900319bdd7e5a5b9142d736f4835530eb2b707c..0000000000000000000000000000000000000000
--- a/src/lsp/providers/codeActionProvider.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import {
-  CodeAction,
-  CodeActionParams,
-  CodeActionKind,
-} from 'vscode-languageserver'
-import { State } from '../util/state'
-import { findLast } from '../util/find'
-
-export function provideCodeActions(
-  _state: State,
-  params: CodeActionParams
-): CodeAction[] {
-  if (params.context.diagnostics.length === 0) {
-    return null
-  }
-
-  return params.context.diagnostics
-    .map((diagnostic) => {
-      let match = findLast(
-        / Did you mean (?:something like )?'(?<replacement>[^']+)'\?$/g,
-        diagnostic.message
-      )
-
-      if (!match) {
-        return null
-      }
-
-      return {
-        title: `Replace with '${match.groups.replacement}'`,
-        kind: CodeActionKind.QuickFix,
-        diagnostics: [diagnostic],
-        edit: {
-          changes: {
-            [params.textDocument.uri]: [
-              {
-                range: diagnostic.range,
-                newText: match.groups.replacement,
-              },
-            ],
-          },
-        },
-      }
-    })
-    .filter(Boolean)
-}
I src/lsp/providers/codeActionProvider/index.ts
diff --git a/src/lsp/providers/codeActionProvider/index.ts b/src/lsp/providers/codeActionProvider/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19d724e27e2e2fe641eceadbea635c44cf65eccd
--- /dev/null
+++ b/src/lsp/providers/codeActionProvider/index.ts
@@ -0,0 +1,266 @@
+import {
+  CodeAction,
+  CodeActionParams,
+  CodeActionKind,
+  Range,
+  TextEdit,
+  Diagnostic,
+} from 'vscode-languageserver'
+import { State } from '../../util/state'
+import { findLast, findClassNamesInRange } from '../../util/find'
+import { isWithinRange } from '../../util/isWithinRange'
+import { getClassNameParts } from '../../util/getClassNameAtPosition'
+const dlv = require('dlv')
+import dset from 'dset'
+import { removeRangeFromString } from '../../util/removeRangeFromString'
+import detectIndent from 'detect-indent'
+import { cssObjToAst } from '../../util/cssObjToAst'
+import isObject from '../../../util/isObject'
+
+export function provideCodeActions(
+  state: State,
+  params: CodeActionParams
+): Promise<CodeAction[]> {
+  if (params.context.diagnostics.length === 0) {
+    return null
+  }
+
+  return Promise.all(
+    params.context.diagnostics
+      .map((diagnostic) => {
+        if (diagnostic.code === 'invalidApply') {
+          return provideInvalidApplyCodeAction(state, params, diagnostic)
+        }
+
+        let match = findLast(
+          / Did you mean (?:something like )?'(?<replacement>[^']+)'\?$/g,
+          diagnostic.message
+        )
+
+        if (!match) {
+          return null
+        }
+
+        return {
+          title: `Replace with '${match.groups.replacement}'`,
+          kind: CodeActionKind.QuickFix,
+          diagnostics: [diagnostic],
+          edit: {
+            changes: {
+              [params.textDocument.uri]: [
+                {
+                  range: diagnostic.range,
+                  newText: match.groups.replacement,
+                },
+              ],
+            },
+          },
+        }
+      })
+      .filter(Boolean)
+  )
+}
+
+function classNameToAst(
+  state: State,
+  className: string,
+  selector: string = `.${className}`,
+  important: boolean = false
+) {
+  const parts = getClassNameParts(state, className)
+  if (!parts) {
+    return null
+  }
+  const baseClassName = dlv(
+    state.classNames.classNames,
+    parts[parts.length - 1]
+  )
+  if (!baseClassName) {
+    return null
+  }
+  const info = dlv(state.classNames.classNames, parts)
+  let context = info.__context || []
+  let pseudo = info.__pseudo || []
+  const globalContexts = state.classNames.context
+  let screens = dlv(
+    state.config,
+    'theme.screens',
+    dlv(state.config, 'screens', {})
+  )
+  if (!isObject(screens)) screens = {}
+  screens = Object.keys(screens)
+  const path = []
+
+  for (let i = 0; i < parts.length - 1; i++) {
+    let part = parts[i]
+    let common = globalContexts[part]
+    if (!common) return null
+    if (screens.includes(part)) {
+      path.push(`@screen ${part}`)
+      context = context.filter((con) => !common.includes(con))
+    }
+  }
+
+  path.push(...context)
+
+  let obj = {}
+  for (let i = 1; i <= path.length; i++) {
+    dset(obj, path.slice(0, i), {})
+  }
+  let rule = {
+    // TODO: use proper selector parser
+    [selector + pseudo.join('')]: {
+      [`@apply ${parts[parts.length - 1]}${
+        important ? ' !important' : ''
+      }`]: '',
+    },
+  }
+  if (path.length) {
+    dset(obj, path, rule)
+  } else {
+    obj = rule
+  }
+
+  return cssObjToAst(obj, state.modules.postcss)
+}
+
+async function provideInvalidApplyCodeAction(
+  state: State,
+  params: CodeActionParams,
+  diagnostic: Diagnostic
+): Promise<CodeAction> {
+  let document = state.editor.documents.get(params.textDocument.uri)
+  let documentText = document.getText()
+  const { postcss } = state.modules
+  let change: TextEdit
+
+  let documentClassNames = findClassNamesInRange(
+    document,
+    {
+      start: {
+        line: Math.max(0, diagnostic.range.start.line - 10),
+        character: 0,
+      },
+      end: { line: diagnostic.range.start.line + 10, character: 0 },
+    },
+    'css'
+  )
+  let documentClassName = documentClassNames.find((className) =>
+    isWithinRange(diagnostic.range.start, className.range)
+  )
+  if (!documentClassName) {
+    return null
+  }
+  let totalClassNamesInClassList = documentClassName.classList.classList.split(
+    /\s+/
+  ).length
+
+  await postcss([
+    postcss.plugin('', (_options = {}) => {
+      return (root) => {
+        root.walkRules((rule) => {
+          if (change) return false
+
+          rule.walkAtRules('apply', (atRule) => {
+            let { start, end } = atRule.source
+            let range: Range = {
+              start: {
+                line: start.line - 1,
+                character: start.column - 1,
+              },
+              end: {
+                line: end.line - 1,
+                character: end.column - 1,
+              },
+            }
+
+            if (!isWithinRange(diagnostic.range.start, range)) {
+              // keep looking
+              return true
+            }
+
+            let className = document.getText(diagnostic.range)
+            let ast = classNameToAst(
+              state,
+              className,
+              rule.selector,
+              documentClassName.classList.important
+            )
+
+            if (!ast) {
+              return false
+            }
+
+            rule.after(ast.nodes)
+            let insertedRule = rule.next()
+
+            if (totalClassNamesInClassList === 1) {
+              atRule.remove()
+            }
+
+            let outputIndent: string
+            let documentIndent = detectIndent(documentText)
+
+            change = {
+              range: {
+                start: {
+                  line: rule.source.start.line - 1,
+                  character: rule.source.start.column - 1,
+                },
+                end: {
+                  line: rule.source.end.line - 1,
+                  character: rule.source.end.column,
+                },
+              },
+              newText:
+                rule.toString() +
+                (insertedRule.raws.before || '\n\n') +
+                insertedRule
+                  .toString()
+                  .replace(/\n\s*\n/g, '\n')
+                  .replace(/(@apply [^;\n]+)$/gm, '$1;')
+                  .replace(/([^\s^]){$/gm, '$1 {')
+                  .replace(/^\s+/gm, (m: string) => {
+                    if (typeof outputIndent === 'undefined') outputIndent = m
+                    return m.replace(
+                      new RegExp(outputIndent, 'g'),
+                      documentIndent.indent
+                    )
+                  }),
+            }
+
+            return false
+          })
+        })
+      }
+    }),
+  ]).process(documentText, { from: undefined })
+
+  if (!change) {
+    return null
+  }
+
+  return {
+    title: 'Extract to new rule.',
+    kind: CodeActionKind.QuickFix,
+    diagnostics: [diagnostic],
+    edit: {
+      changes: {
+        [params.textDocument.uri]: [
+          ...(totalClassNamesInClassList > 1
+            ? [
+                {
+                  range: documentClassName.classList.range,
+                  newText: removeRangeFromString(
+                    documentClassName.classList.classList,
+                    documentClassName.relativeRange
+                  ),
+                },
+              ]
+            : []),
+          change,
+        ],
+      },
+    },
+  }
+}
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 496dee38a0d9c3d079c0b9bf9663c7a922f19176..f6fa9092b7d83df2bd1647b212f32527f8c843ee 100644
--- a/src/lsp/providers/diagnosticsProvider.ts
+++ b/src/lsp/providers/diagnosticsProvider.ts
@@ -73,6 +73,7 @@             ? DiagnosticSeverity.Error
             : DiagnosticSeverity.Warning,
         range,
         message,
+        code: 'invalidApply',
       }
     })
     .filter(Boolean)
M src/lsp/server.ts -> src/lsp/server.ts
diff --git a/src/lsp/server.ts b/src/lsp/server.ts
index dce9672bc69e26dced8f4bb8442b05e371fefdac..6c928e68b9df731b3b77b24aa82e85621eb5f7f1 100644
--- a/src/lsp/server.ts
+++ b/src/lsp/server.ts
@@ -230,10 +231,15 @@   }
 )
 
   CodeActionParams,
+ * ------------------------------------------------------------------------------------------ */
   CodeActionParams,
+
 /* --------------------------------------------------------------------------------------------
+          capabilities.textDocument.publishDiagnostics.relatedInformation,
   CodeActionParams,
- * Copyright (c) Microsoft Corporation. All rights reserved.
+import {
+/* --------------------------------------------------------------------------------------------
 })
+)
 
 connection.listen()
I src/lsp/util/cssObjToAst.ts
diff --git a/src/lsp/util/cssObjToAst.ts b/src/lsp/util/cssObjToAst.ts
new file mode 100644
index 0000000000000000000000000000000000000000..42826f7526c514588519a63a8d2e6723a9ebfdd2
--- /dev/null
+++ b/src/lsp/util/cssObjToAst.ts
@@ -0,0 +1,127 @@
+/*
+This is a modified version of the postcss-js 'parse' function which accepts the
+postcss module as an argument. License below:
+
+The MIT License (MIT)
+
+Copyright 2015 Andrey Sitnik <andrey@sitnik.ru>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+var IMPORTANT = /\s*!important\s*$/i
+
+var unitless = {
+  'box-flex': true,
+  'box-flex-group': true,
+  'column-count': true,
+  flex: true,
+  'flex-grow': true,
+  'flex-positive': true,
+  'flex-shrink': true,
+  'flex-negative': true,
+  'font-weight': true,
+  'line-clamp': true,
+  'line-height': true,
+  opacity: true,
+  order: true,
+  orphans: true,
+  'tab-size': true,
+  widows: true,
+  'z-index': true,
+  zoom: true,
+  'fill-opacity': true,
+  'stroke-dashoffset': true,
+  'stroke-opacity': true,
+  'stroke-width': true,
+}
+
+function dashify(str) {
+  return str
+    .replace(/([A-Z])/g, '-$1')
+    .replace(/^ms-/, '-ms-')
+    .toLowerCase()
+}
+
+function decl(parent, name, value, postcss) {
+  if (value === false || value === null) return
+
+  name = dashify(name)
+  if (typeof value === 'number') {
+    if (value === 0 || unitless[name]) {
+      value = value.toString()
+    } else {
+      value = value.toString() + 'px'
+    }
+  }
+
+  if (name === 'css-float') name = 'float'
+
+  if (IMPORTANT.test(value)) {
+    value = value.replace(IMPORTANT, '')
+    parent.push(postcss.decl({ prop: name, value: value, important: true }))
+  } else {
+    parent.push(postcss.decl({ prop: name, value: value }))
+  }
+}
+
+function atRule(parent, parts, value, postcss) {
+  var node = postcss.atRule({ name: parts[1], params: parts[3] || '' })
+  if (typeof value === 'object') {
+    node.nodes = []
+    parse(value, node, postcss)
+  }
+  parent.push(node)
+}
+
+function parse(obj, parent, postcss) {
+  var name, value, node, i
+  for (name in obj) {
+    if (obj.hasOwnProperty(name)) {
+      value = obj[name]
+      if (value === null || typeof value === 'undefined') {
+        continue
+      } else if (name[0] === '@') {
+        var parts = name.match(/@([^\s]+)(\s+([\w\W]*)\s*)?/)
+        if (Array.isArray(value)) {
+          for (i = 0; i < value.length; i++) {
+            atRule(parent, parts, value[i], postcss)
+          }
+        } else {
+          atRule(parent, parts, value, postcss)
+        }
+      } else if (Array.isArray(value)) {
+        for (i = 0; i < value.length; i++) {
+          decl(parent, name, value[i], postcss)
+        }
+      } else if (typeof value === 'object') {
+        node = postcss.rule({ selector: name })
+        parse(value, node, postcss)
+        parent.push(node)
+      } else {
+        decl(parent, name, value, postcss)
+      }
+    }
+  }
+}
+
+export function cssObjToAst(obj, postcss) {
+  var root = postcss.root()
+  parse(obj, root, postcss)
+  return root
+}
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 12dde012f9aaa810475d97e78f202b05054f70d8..b642534d012dc3816516f0284de71b1ecbf31954 100644
--- a/src/lsp/util/find.ts
+++ b/src/lsp/util/find.ts
@@ -32,6 +32,7 @@
 export function getClassNamesInClassList({
   classList,
   range,
+  important,
 }: DocumentClassList): DocumentClassName[] {
   const parts = classList.split(/(\s+)/)
   const names: DocumentClassName[] = []
@@ -42,6 +43,15 @@       const start = indexToPosition(classList, index)
       const end = indexToPosition(classList, index + parts[i].length)
       names.push({
 import { isCssContext, isCssDoc } from './css'
+        classList: {
+          classList,
+          range,
+          important,
+        },
+        relativeRange: {
+          start,
+          end,
+import { isHtmlContext, isHtmlDoc, isSvelteDoc, isVueDoc } from './html'
         range: {
           start: {
             line: range.start.line + start.line,
@@ -83,7 +93,12 @@   doc: TextDocument,
   range?: Range
 ): DocumentClassList[] {
   const text = doc.getText(range)
+  }
 import { isJsContext, isJsDoc } from './js'
+import { DocumentClassName, DocumentClassList, State } from './state'
+import { flatten } from '../../util/array'
+    text
+  )
   const globalStart: Position = range ? range.start : { line: 0, character: 0 }
 
   return matches.map((match) => {
@@ -94,6 +109,7 @@       match.index + match[1].length + match.groups.classList.length
     )
     return {
       classList: match.groups.classList,
+      important: Boolean(match.groups.important),
       range: {
         start: {
           line: globalStart.line + start.line,
M src/lsp/util/getClassNameAtPosition.ts -> src/lsp/util/getClassNameAtPosition.ts
diff --git a/src/lsp/util/getClassNameAtPosition.ts b/src/lsp/util/getClassNameAtPosition.ts
index 95de79a0392a9cfcf0de45d184a266748a77a1a3..083832ca5e8b2707a1405fdbcd83f6c163e18f65 100644
--- a/src/lsp/util/getClassNameAtPosition.ts
+++ b/src/lsp/util/getClassNameAtPosition.ts
@@ -1,49 +1,6 @@
-import { TextDocument, Range, Position } from 'vscode-languageserver'
-import { State, DocumentClassName } from './state'
-const dlv = require('dlv')
-
-export function getClassNameAtPosition(
-  document: TextDocument,
   position: Position
-): DocumentClassName {
-  const range1: Range = {
-    start: { line: Math.max(position.line - 5, 0), character: 0 },
-    end: position,
-  }
-  const text1: string = document.getText(range1)
-
-  if (!/\bclass(Name)?=['"][^'"]*$/.test(text1)) return null
-
-  const range2: Range = {
-    start: { line: Math.max(position.line - 5, 0), character: 0 },
-    end: { line: position.line + 1, character: position.character },
-  }
-import { TextDocument, Range, Position } from 'vscode-languageserver'
   position: Position
-
-  let str: string = text1 + text2.substr(text1.length).match(/^([^"' ]*)/)[0]
-  let matches: RegExpMatchArray = str.match(/\bclass(Name)?=["']([^"']+)$/)
-
-  if (!matches) return null
-
-  let className: string = matches[2].split(' ').pop()
-  if (!className) return null
-
-import { State, DocumentClassName } from './state'
 const dlv = require('dlv')
-    start: {
-      line: position.line,
-      character:
-        position.character + str.length - text1.length - className.length,
-    },
-    end: {
-      line: position.line,
-      character: position.character + str.length - text1.length,
-    },
-  }
-
-  return { className, range }
-}
 
 export function getClassNameParts(state: State, className: string): string[] {
   let separator = state.separator
I src/lsp/util/logFull.ts
diff --git a/src/lsp/util/logFull.ts b/src/lsp/util/logFull.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c05fc1b824a56b3fe33f1f3e4e91b24d8d6074a7
--- /dev/null
+++ b/src/lsp/util/logFull.ts
@@ -0,0 +1,5 @@
+import * as util from 'util'
+
+export function logFull(object: any): void {
+  console.log(util.inspect(object, { showHidden: false, depth: null }))
+}
I src/lsp/util/removeRangeFromString.ts
diff --git a/src/lsp/util/removeRangeFromString.ts b/src/lsp/util/removeRangeFromString.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7479373ced8326e6a6429b1c0642560a13fc076a
--- /dev/null
+++ b/src/lsp/util/removeRangeFromString.ts
@@ -0,0 +1,16 @@
+import { Range } from 'vscode-languageserver'
+import lineColumn from 'line-column'
+
+export function removeRangeFromString(str: string, range: Range): string {
+  let finder = lineColumn(str + '\n', { origin: 0 })
+  let start = finder.toIndex(range.start.line, range.start.character)
+  let end = finder.toIndex(range.end.line, range.end.character)
+  for (let i = start - 1; i >= 0; i--) {
+    if (/\s/.test(str.charAt(i))) {
+      start = i
+    } else {
+      break
+    }
+  }
+  return (str.substr(0, start) + str.substr(end)).trim()
+}
M src/lsp/util/state.ts -> src/lsp/util/state.ts
diff --git a/src/lsp/util/state.ts b/src/lsp/util/state.ts
index 091650be432c4382b4fdb0430501df58d04c8e9c..4158c590762e6acb60bcfdc865a0f96c44816326 100644
--- a/src/lsp/util/state.ts
+++ b/src/lsp/util/state.ts
@@ -48,6 +48,10 @@   emitter: NotificationEmitter
   version?: string
   configPath?: string
   config?: any
+  modules?: {
+    tailwindcss: any
+    postcss: any
+  }
   separator?: string
   plugins?: any[]
   variants?: string[]
@@ -61,10 +65,13 @@ export type DocumentClassList = {
   classList: string
   range: Range
 }
+}
 
 export type DocumentClassName = {
   className: string
   range: Range
+  relativeRange: Range
+  classList: DocumentClassList
 }
 
 export type ClassNameMeta = {