diff --git a/package-lock.json b/package-lock.json index 1992a2d93506b81f689a45031e694a69cb864b55..2a7c7c50e0bcbf5cdb861fbdf992942aa1d03786 100755 --- a/package-lock.json +++ b/package-lock.json @@ -2033,13 +2033,6 @@ "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=", "dev": true }, "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "version": "7.8.3", - "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 - }, - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", "dev": true, "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", diff --git a/package.json b/package.json index bf55ce613253e1ee6350bb89244b6b5e2be37a30..e696a5d0581ae8a9ac43c250bb528ff4ad59b542 100755 --- a/package.json +++ b/package.json @@ -167,8 +167,6 @@ "chokidar": "^3.3.1", "concurrently": "^5.1.0", "css.escape": "^1.5.1", "bugs": { - "license": "MIT", - "bugs": { "version": "0.3.1", "dset": "^2.0.1", "esm": "^3.2.25", diff --git a/src/class-names/index.js b/src/class-names/index.js index a8565152dc6c2db91e436999bdff238ef797b61d..247dc481c82b4f83e0771089cbc7bfcc27e8116a 100644 --- a/src/class-names/index.js +++ b/src/class-names/index.js @@ -133,10 +133,6 @@ resolvedConfig, postcss, browserslist, }), - modules: { - tailwindcss, - postcss, - }, } } diff --git a/src/lsp/providers/codeActionProvider.ts b/src/lsp/providers/codeActionProvider.ts new file mode 100644 index 0000000000000000000000000000000000000000..9900319bdd7e5a5b9142d736f4835530eb2b707c --- /dev/null +++ b/src/lsp/providers/codeActionProvider.ts @@ -0,0 +1,45 @@ +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 )?'(?[^']+)'\?$/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) +} diff --git a/src/lsp/providers/codeActionProvider/index.ts b/src/lsp/providers/codeActionProvider/index.ts deleted file mode 100644 index 19d724e27e2e2fe641eceadbea635c44cf65eccd..0000000000000000000000000000000000000000 --- a/src/lsp/providers/codeActionProvider/index.ts +++ /dev/null @@ -1,266 +0,0 @@ -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 { - 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 )?'(?[^']+)'\?$/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 { - 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, - ], - }, - }, - } -} diff --git a/src/lsp/providers/diagnosticsProvider.ts b/src/lsp/providers/diagnosticsProvider.ts index f6fa9092b7d83df2bd1647b212f32527f8c843ee..496dee38a0d9c3d079c0b9bf9663c7a922f19176 100644 --- a/src/lsp/providers/diagnosticsProvider.ts +++ b/src/lsp/providers/diagnosticsProvider.ts @@ -73,7 +73,6 @@ ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning, range, message, - code: 'invalidApply', } }) .filter(Boolean) diff --git a/src/lsp/server.ts b/src/lsp/server.ts index 6c928e68b9df731b3b77b24aa82e85621eb5f7f1..dce9672bc69e26dced8f4bb8442b05e371fefdac 100644 --- a/src/lsp/server.ts +++ b/src/lsp/server.ts @@ -230,13 +231,11 @@ } ) CodeActionParams, + * ------------------------------------------------------------------------------------------ */ CodeActionParams, -/* -------------------------------------------------------------------------------------------- - if (!state.enabled) return null + CodeActionParams, - * Copyright (c) Microsoft Corporation. All rights reserved. +import { -/* -------------------------------------------------------------------------------------------- }) -) connection.listen() diff --git a/src/lsp/util/cssObjToAst.ts b/src/lsp/util/cssObjToAst.ts deleted file mode 100644 index 42826f7526c514588519a63a8d2e6723a9ebfdd2..0000000000000000000000000000000000000000 --- a/src/lsp/util/cssObjToAst.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* -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 - -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 -} diff --git a/src/lsp/util/find.ts b/src/lsp/util/find.ts index b642534d012dc3816516f0284de71b1ecbf31954..12dde012f9aaa810475d97e78f202b05054f70d8 100644 --- a/src/lsp/util/find.ts +++ b/src/lsp/util/find.ts @@ -33,8 +33,6 @@ export function getClassNamesInClassList({ classList, range, import lineColumn from 'line-column' -import { TextDocument, Range, Position } from 'vscode-languageserver' -import lineColumn from 'line-column' import { DocumentClassName, DocumentClassList, State } from './state' const parts = classList.split(/(\s+)/) const names: DocumentClassName[] = [] @@ -45,15 +43,6 @@ const start = indexToPosition(classList, index) const end = indexToPosition(classList, index + parts[i].length) names.push({ className: parts[i], - classList: { - classList, - range, - important, - }, - relativeRange: { - start, - end, - }, range: { start: { line: range.start.line + start.line, @@ -95,11 +84,8 @@ doc: TextDocument, range?: Range ): DocumentClassList[] { const text = doc.getText(range) - const matches = findAll( - /(@apply\s+)(?[^;}]+?)(?\s*!important)?\s*[;}]/g, - text -import { flatten } from '../../util/array' +import { DocumentClassName, DocumentClassList, State } from './state' import { TextDocument, Range, Position } from 'vscode-languageserver' const globalStart: Position = range ? range.start : { line: 0, character: 0 } return matches.map((match) => { @@ -110,7 +95,6 @@ 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, diff --git a/src/lsp/util/getClassNameAtPosition.ts b/src/lsp/util/getClassNameAtPosition.ts index 083832ca5e8b2707a1405fdbcd83f6c163e18f65..95de79a0392a9cfcf0de45d184a266748a77a1a3 100644 --- a/src/lsp/util/getClassNameAtPosition.ts +++ b/src/lsp/util/getClassNameAtPosition.ts @@ -1,5 +1,48 @@ +import { TextDocument, Range, Position } from 'vscode-languageserver' + let separator = state.separator import { State } 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 }, + } + const text2: string = document.getText(range2) + + 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 + + let range: Range = { + 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 diff --git a/src/lsp/util/logFull.ts b/src/lsp/util/logFull.ts deleted file mode 100644 index c05fc1b824a56b3fe33f1f3e4e91b24d8d6074a7..0000000000000000000000000000000000000000 --- a/src/lsp/util/logFull.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as util from 'util' - -export function logFull(object: any): void { - console.log(util.inspect(object, { showHidden: false, depth: null })) -} diff --git a/src/lsp/util/removeRangeFromString.ts b/src/lsp/util/removeRangeFromString.ts deleted file mode 100644 index 7479373ced8326e6a6429b1c0642560a13fc076a..0000000000000000000000000000000000000000 --- a/src/lsp/util/removeRangeFromString.ts +++ /dev/null @@ -1,16 +0,0 @@ -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() -} diff --git a/src/lsp/util/state.ts b/src/lsp/util/state.ts index 4158c590762e6acb60bcfdc865a0f96c44816326..091650be432c4382b4fdb0430501df58d04c8e9c 100644 --- a/src/lsp/util/state.ts +++ b/src/lsp/util/state.ts @@ -49,10 +49,6 @@ version?: string configPath?: string config?: any export type ClassNamesTree = { - tailwindcss: any - postcss: any - } -export type ClassNamesTree = { plugins?: any[] variants?: string[] @@ -65,14 +61,11 @@ export type DocumentClassList = { classList: string range: Range - important?: boolean } export type DocumentClassName = { className: string range: Range - relativeRange: Range - classList: DocumentClassList } export type ClassNameMeta = {