diff --git a/package.json b/package.json index 45a6e1aa..8b4515a7 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ ], "devDependencies": { "@npmcli/package-json": "^5.0.0", + "esbuild": "^0.19.6", "semver": "^7.5.4" }, "prettier": { diff --git a/packages/tailwindcss-language-server/README.md b/packages/tailwindcss-language-server/README.md index b560187b..3dcc5e09 100644 --- a/packages/tailwindcss-language-server/README.md +++ b/packages/tailwindcss-language-server/README.md @@ -1,3 +1,7 @@ +# THIS IS NOT MAINTED BY TAILWINDCSS TEAM + +[Helix issue](https://github.com/helix-editor/helix/issues/2213) + # Tailwind CSS Language Server [Language Server Protocol](https://github.com/Microsoft/language-server-protocol) implementation for [Tailwind CSS](https://tailwindcss.com), used by [Tailwind CSS IntelliSense for VS Code](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss). diff --git a/packages/tailwindcss-language-server/package.json b/packages/tailwindcss-language-server/package.json index bee36c72..072f0586 100644 --- a/packages/tailwindcss-language-server/package.json +++ b/packages/tailwindcss-language-server/package.json @@ -1,8 +1,8 @@ { - "name": "@tailwindcss/language-server", + "name": "helix-twcss", "description": "Tailwind CSS Language Server", "license": "MIT", - "version": "0.0.14", + "version": "0.0.16", "repository": { "type": "git", "url": "git+https://github.com/tailwindlabs/tailwindcss-intellisense.git", @@ -13,17 +13,18 @@ }, "homepage": "https://github.com/tailwindlabs/tailwindcss-intellisense/tree/HEAD/packages/tailwindcss-language-server#readme", "scripts": { - "build": "npm run clean && npm run _esbuild && npm run hashbang", + "build": "npm run clean && npm run _esbuild && npm run hashbang && chmod +x ./bin/tailwindcss-language-server && npm run code2", "_esbuild": "node ../../esbuild.mjs src/server.ts --outfile=bin/tailwindcss-language-server --minify", "clean": "rimraf bin", "hashbang": "node scripts/hashbang.mjs", "create-notices-file": "node scripts/createNoticesFile.mjs", "prepublishOnly": "npm run build", "test": "vitest", - "test:prepare": "node tests/prepare.js" + "test:prepare": "node tests/prepare.js", + "code2": "cp -r ./bin/* /home/uros/.nvm/versions/node/v20.9.0/lib/node_modules/helix-twcss/bin" }, "bin": { - "tailwindcss-language-server": "./bin/tailwindcss-language-server" + "hx-tw": "./bin/tailwindcss-language-server" }, "files": [ "bin", @@ -84,4 +85,4 @@ "singleQuote": true, "printWidth": 100 } -} +} \ No newline at end of file diff --git a/packages/tailwindcss-language-server/src/fakeConstructor.ts b/packages/tailwindcss-language-server/src/fakeConstructor.ts new file mode 100644 index 00000000..0113950b --- /dev/null +++ b/packages/tailwindcss-language-server/src/fakeConstructor.ts @@ -0,0 +1,13 @@ +const fs = require("fs"); + +let logCounter = 0; + +export function log2(msg: string) { + if (logCounter == 0) { + fs.unlink("tailwind.txt", () => { }); + logCounter += 1; + } + let content = `\n${new Date().toString()}:\n\t msg:[\n${msg}\n]\n`; + fs.appendFile("tailwind.txt", content, (_err) => { + }); +} diff --git a/packages/tailwindcss-language-server/src/server.ts b/packages/tailwindcss-language-server/src/server.ts index 2c40e77d..404c5b92 100644 --- a/packages/tailwindcss-language-server/src/server.ts +++ b/packages/tailwindcss-language-server/src/server.ts @@ -248,7 +248,7 @@ async function getConfiguration(uri?: string) { editor: { tabSize: 2 }, tailwindCSS: { emmetCompletions: false, - classAttributes: ['class', 'className', 'ngClass'], + classAttributes: ['class', 'className', 'ngClass', '\\w.*_CLASS', '\\w.*_class'], codeActions: true, hovers: true, suggestions: true, diff --git a/packages/tailwindcss-language-service/src/completionProvider.ts b/packages/tailwindcss-language-service/src/completionProvider.ts index 6468b93a..35700faa 100644 --- a/packages/tailwindcss-language-service/src/completionProvider.ts +++ b/packages/tailwindcss-language-service/src/completionProvider.ts @@ -12,7 +12,7 @@ import type { TextDocument } from 'vscode-languageserver-textdocument' import dlv from 'dlv' import removeMeta from './util/removeMeta' import { getColor, getColorFromValue } from './util/color' -import { isHtmlContext } from './util/html' +import { isHtmlContext, isRustContext } from './util/html' import { isCssContext } from './util/css' import { findLast, matchClassAttributes } from './util/find' import { stringifyConfigValue, stringifyCss } from './util/stringify' @@ -36,6 +36,7 @@ import { addPixelEquivalentsToMediaQuery, addPixelEquivalentsToValue, } from './util/pixelEquivalents' +import { trimClass } from './fakeConstructor' let isUtil = (className) => Array.isArray(className.__info) @@ -51,13 +52,14 @@ export function completionsFromClassList( context?: CompletionContext ): CompletionList { let classNames = classList.split(/[\s+]/) - const partialClassName = classNames[classNames.length - 1] + let partialClassName = classNames[classNames.length - 1]; let sep = state.separator let parts = partialClassName.split(sep) let subset: any let subsetKey: string[] = [] let isSubset: boolean = false + let lastClass = trimClass(classList) let replacementRange = { ...classListRange, start: { @@ -156,9 +158,9 @@ export function completionsFromClassList( item.insertTextFormat === 2 // Snippet ? undefined : { - title: '', - command: 'editor.action.triggerSuggest', - }, + title: '', + command: 'editor.action.triggerSuggest', + }, sortText: '-' + naturalExpand(variantOrder++), ...item, } @@ -210,21 +212,21 @@ export function completionsFromClassList( additionalTextEdits: shouldSortVariants && resultingVariants.length > 1 ? [ - { - newText: - resultingVariants.slice(0, resultingVariants.length - 1).join(sep) + sep, - range: { - start: { - ...classListRange.start, - character: classListRange.end.character - partialClassName.length, - }, - end: { - ...replacementRange.start, - character: replacementRange.start.character, - }, + { + newText: + resultingVariants.slice(0, resultingVariants.length - 1).join(sep) + sep, + range: { + start: { + ...classListRange.start, + character: classListRange.end.character - partialClassName.length, + }, + end: { + ...replacementRange.start, + character: replacementRange.start.character, }, }, - ] + }, + ] : [], }) ) @@ -255,34 +257,44 @@ export function completionsFromClassList( } if (state.classList) { - return withDefaults( - { - isIncomplete: false, - items: items.concat( - state.classList.reduce((items, [className, { color }], index) => { - if ( - state.blocklist?.includes([...existingVariants, className].join(state.separator)) - ) { - return items - } + let isFirst = false; + let items2 = state.classList.reduce((items2, [className, { color }], index) => { + if ( + state.blocklist?.includes([...existingVariants, className].join(state.separator)) + ) { + return items2 + } - let kind: CompletionItemKind = color ? 16 : 21 - let documentation: string | undefined + let kind: CompletionItemKind = color ? 16 : 21 + let documentation: string | undefined - if (color && typeof color !== 'string') { - documentation = culori.formatRgb(color) - } + if (color && typeof color !== 'string') { + documentation = culori.formatRgb(color) + } + + let precise = className.includes(lastClass); + if (precise) { + if (isFirst == false) { + isFirst = true; + items = []; + } + let sortText = precise ? "-000000000" : naturalExpand(index, state.classList.length); + items2.push({ + label: className, + kind: kind, + ...(documentation ? { documentation } : {}), + sortText: sortText + // sortText: naturalExpand(index, state.classList.length), + }) + } + return items2 + }, [] as CompletionItem[]); - items.push({ - label: className, - kind, - ...(documentation ? { documentation } : {}), - sortText: naturalExpand(index, state.classList.length), - }) - return items - }, [] as CompletionItem[]) - ), + return withDefaults( + { + isIncomplete: false, + items: items.concat(items2), }, { data: { @@ -491,7 +503,7 @@ async function provideClassAttributeCompletions( context ) } - } catch (_) {} + } catch (_) { } return null } @@ -567,7 +579,7 @@ async function provideCustomClassNameCompletions( ) } } - } catch (_) {} + } catch (_) { } } return null @@ -634,7 +646,7 @@ async function provideClassNameCompletions( return provideAtApplyCompletions(state, document, position, context) } - if (isHtmlContext(state, document, position) || isJsxContext(state, document, position)) { + if (isHtmlContext(state, document, position) || isJsxContext(state, document, position) || isRustContext(document) ) { return provideClassAttributeCompletions(state, document, position, context) } @@ -759,17 +771,17 @@ function provideCssHelperCompletions( ...(insertClosingBrace ? { textEditText: `${item}]` } : {}), additionalTextEdits: replaceDot ? [ - { - newText: '[', - range: { - start: { - ...editRange.start, - character: editRange.start.character - 1, - }, - end: editRange.start, + { + newText: '[', + range: { + start: { + ...editRange.start, + character: editRange.start.character - 1, }, + end: editRange.start, }, - ] + }, + ] : [], } }), @@ -806,25 +818,25 @@ function provideTailwindDirectiveCompletions( let items = [ semver.gte(state.version, '1.0.0-beta.1') ? { - label: 'base', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `This injects Tailwind’s base styles and any base styles registered by plugins.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#tailwind' - )})`, - }, - } + label: 'base', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `This injects Tailwind’s base styles and any base styles registered by plugins.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#tailwind' + )})`, + }, + } : { - label: 'preflight', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `This injects Tailwind’s base styles, which is a combination of Normalize.css and some additional base styles.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#tailwind' - )})`, - }, + label: 'preflight', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `This injects Tailwind’s base styles, which is a combination of Normalize.css and some additional base styles.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#tailwind' + )})`, }, + }, { label: 'components', documentation: { @@ -847,25 +859,25 @@ function provideTailwindDirectiveCompletions( }, state.jit && semver.gte(state.version, '2.1.99') ? { - label: 'variants', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `Use this directive to control where Tailwind injects the utility variants.\n\nThis directive is considered an advanced escape hatch and it is recommended to omit it whenever possible. If omitted, Tailwind will append these classes to the very end of your stylesheet by default.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'just-in-time-mode#variants-are-inserted-at-tailwind-variants' - )})`, - }, - } + label: 'variants', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `Use this directive to control where Tailwind injects the utility variants.\n\nThis directive is considered an advanced escape hatch and it is recommended to omit it whenever possible. If omitted, Tailwind will append these classes to the very end of your stylesheet by default.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'just-in-time-mode#variants-are-inserted-at-tailwind-variants' + )})`, + }, + } : { - label: 'screens', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `Use this directive to control where Tailwind injects the responsive variations of each utility.\n\nIf omitted, Tailwind will append these classes to the very end of your stylesheet by default.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#tailwind' - )})`, - }, + label: 'screens', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `Use this directive to control where Tailwind injects the responsive variations of each utility.\n\nIf omitted, Tailwind will append these classes to the very end of your stylesheet by default.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#tailwind' + )})`, }, + }, ] return withDefaults( @@ -1019,29 +1031,29 @@ function withDefaults( ...completionList, ...(defaultData || defaultRange ? { - itemDefaults: { - ...(defaultData && defaults.data ? { data: defaults.data } : {}), - ...(defaultRange && defaults.range ? { editRange: defaults.range } : {}), - }, - } + itemDefaults: { + ...(defaultData && defaults.data ? { data: defaults.data } : {}), + ...(defaultRange && defaults.range ? { editRange: defaults.range } : {}), + }, + } : {}), items: defaultData && defaultRange ? completionList.items : completionList.items.map(({ textEditText, ...item }) => ({ - ...item, - ...(defaultData || !defaults.data || item.data ? {} : { data: defaults.data }), - ...(defaultRange || !defaults.range - ? textEditText - ? { textEditText } - : {} - : { - textEdit: { - newText: textEditText ?? item.label, - range: defaults.range, - }, - }), - })), + ...item, + ...(defaultData || !defaults.data || item.data ? {} : { data: defaults.data }), + ...(defaultRange || !defaults.range + ? textEditText + ? { textEditText } + : {} + : { + textEdit: { + newText: textEditText ?? item.label, + range: defaults.range, + }, + }), + })), } } @@ -1116,12 +1128,11 @@ function provideCssDirectiveCompletions( label: '@tailwind', documentation: { kind: 'markdown' as typeof MarkupKind.Markdown, - value: `Use the \`@tailwind\` directive to insert Tailwind’s \`base\`, \`components\`, \`utilities\` and \`${ - state.jit && semver.gte(state.version, '2.1.99') ? 'variants' : 'screens' - }\` styles into your CSS.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#tailwind' - )})`, + value: `Use the \`@tailwind\` directive to insert Tailwind’s \`base\`, \`components\`, \`utilities\` and \`${state.jit && semver.gte(state.version, '2.1.99') ? 'variants' : 'screens' + }\` styles into your CSS.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#tailwind' + )})`, }, }, { @@ -1146,55 +1157,55 @@ function provideCssDirectiveCompletions( }, ...(semver.gte(state.version, '1.8.0') ? [ - { - label: '@layer', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `Use the \`@layer\` directive to tell Tailwind which "bucket" a set of custom styles belong to. Valid layers are \`base\`, \`components\`, and \`utilities\`.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#layer' - )})`, - }, + { + label: '@layer', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `Use the \`@layer\` directive to tell Tailwind which "bucket" a set of custom styles belong to. Valid layers are \`base\`, \`components\`, and \`utilities\`.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#layer' + )})`, }, - ] + }, + ] : []), ...(semver.gte(state.version, '2.99.0') ? [] : [ - { - label: '@variants', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `You can generate \`responsive\`, \`hover\`, \`focus\`, \`active\`, and other variants of your own utilities by wrapping their definitions in the \`@variants\` directive.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#variants' - )})`, - }, + { + label: '@variants', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `You can generate \`responsive\`, \`hover\`, \`focus\`, \`active\`, and other variants of your own utilities by wrapping their definitions in the \`@variants\` directive.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#variants' + )})`, }, - { - label: '@responsive', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `You can generate responsive variants of your own classes by wrapping their definitions in the \`@responsive\` directive.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#responsive' - )})`, - }, + }, + { + label: '@responsive', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `You can generate responsive variants of your own classes by wrapping their definitions in the \`@responsive\` directive.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#responsive' + )})`, }, - ]), + }, + ]), ...(semver.gte(state.version, '3.2.0') ? [ - { - label: '@config', - documentation: { - kind: 'markdown' as typeof MarkupKind.Markdown, - value: `Use the \`@config\` directive to specify which config file Tailwind should use when compiling that CSS file.\n\n[Tailwind CSS Documentation](${docsUrl( - state.version, - 'functions-and-directives/#config' - )})`, - }, + { + label: '@config', + documentation: { + kind: 'markdown' as typeof MarkupKind.Markdown, + value: `Use the \`@config\` directive to specify which config file Tailwind should use when compiling that CSS file.\n\n[Tailwind CSS Documentation](${docsUrl( + state.version, + 'functions-and-directives/#config' + )})`, }, - ] + }, + ] : []), ] diff --git a/packages/tailwindcss-language-service/src/fakeConstructor.ts b/packages/tailwindcss-language-service/src/fakeConstructor.ts new file mode 100644 index 00000000..cb17ccd1 --- /dev/null +++ b/packages/tailwindcss-language-service/src/fakeConstructor.ts @@ -0,0 +1,32 @@ +const fs = require("fs"); + +let logCounter = 0; + +export function log2(msg: string) { + if (logCounter == 0) { + fs.unlink("tailwind.txt", () => { }); + logCounter += 1; + } + let content = `\n${new Date().toString()}:\n\t msg:[${msg}]\n`; + fs.appendFile("tailwind.txt", content, (_err) => { + }); +} + +export function trimClass(className: string): string { + let parts = className.split(" ") + let last = parts[parts.length - 1] + if (last) { + if (last.trim().length != 0) { + if (last.includes(":")) { + const last2 = last.split(":"); + let newLast = last2[last2.length - 1]; + if (newLast) { + return newLast + } + return last + } + return last + } + } + return "" +}; diff --git a/packages/tailwindcss-language-service/src/hoverProvider.ts b/packages/tailwindcss-language-service/src/hoverProvider.ts index ffa75673..2c10571d 100644 --- a/packages/tailwindcss-language-service/src/hoverProvider.ts +++ b/packages/tailwindcss-language-service/src/hoverProvider.ts @@ -43,6 +43,7 @@ function provideCssHelperHover(state: State, document: TextDocument, position: P if (value === null) { return null } + return { contents: { kind: 'markdown', value: ['```plaintext', value, '```'].join('\n') }, range: helperFn.ranges.path, diff --git a/packages/tailwindcss-language-service/src/util/find.ts b/packages/tailwindcss-language-service/src/util/find.ts index 5dfc7e71..6c6ebd25 100644 --- a/packages/tailwindcss-language-service/src/util/find.ts +++ b/packages/tailwindcss-language-service/src/util/find.ts @@ -3,7 +3,7 @@ import type { TextDocument } from 'vscode-languageserver-textdocument' import { DocumentClassName, DocumentClassList, State, DocumentHelperFunction } from './state' import lineColumn from 'line-column' import { isCssContext, isCssDoc } from './css' -import { isHtmlContext } from './html' +import { isHtmlContext, isRustContext } from './html' import { isWithinRange } from './isWithinRange' import { isJsxContext } from './js' import { dedupeByRange, flatten } from './array' @@ -74,7 +74,7 @@ export async function findClassNamesInRange( state: State, doc: TextDocument, range?: Range, - mode?: 'html' | 'css' | 'jsx', + mode?: 'html' | 'css' | 'jsx' | 'rust', includeCustom: boolean = true ): Promise { const classLists = await findClassListsInRange(state, doc, range, mode, includeCustom) @@ -177,7 +177,7 @@ async function findCustomClassLists( }) } } - } catch (_) {} + } catch (_) { } } return result @@ -238,7 +238,7 @@ export async function findClassListsInHtmlRange( currentClassList = undefined } } - } catch (_) {} + } catch (_) { } if (currentClassList) { classLists.push({ @@ -293,7 +293,7 @@ export async function findClassListsInRange( state: State, doc: TextDocument, range?: Range, - mode?: 'html' | 'css' | 'jsx', + mode?: 'html' | 'css' | 'jsx' | 'rust', includeCustom: boolean = true ): Promise { let classLists: DocumentClassList[] = [] @@ -323,9 +323,10 @@ export async function findClassListsInDocument( flatten([ ...(await Promise.all( boundaries - .filter((b) => b.type === 'html' || b.type === 'jsx') - .map(({ type, range }) => - findClassListsInHtmlRange(state, doc, type === 'html' ? 'html' : 'jsx', range) + .filter((b) => b.type === 'html' || b.type === 'jsx' || b.type == 'rust') + .map(({ type, range }) => { + return findClassListsInHtmlRange(state, doc, type === 'html' || type === 'rust' ? 'html' : 'jsx', range) + } ) )), ...boundaries @@ -449,7 +450,12 @@ export async function findClassNameAtPosition( classNames = await findClassNamesInRange(state, doc, searchRange, 'html') } else if (isJsxContext(state, doc, position)) { classNames = await findClassNamesInRange(state, doc, searchRange, 'jsx') - } else { + } + else if (isRustContext(doc)) { + classNames = await findClassNamesInRange(state, doc, searchRange, 'html') + } + + else { classNames = await findClassNamesInRange(state, doc, searchRange) } diff --git a/packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts b/packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts index 87a4c4b5..8d00d158 100644 --- a/packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts +++ b/packages/tailwindcss-language-service/src/util/getLanguageBoundaries.ts @@ -1,6 +1,6 @@ import type { Range } from 'vscode-languageserver' import type { TextDocument } from 'vscode-languageserver-textdocument' -import { isVueDoc, isHtmlDoc, isSvelteDoc } from './html' +import { isVueDoc, isHtmlDoc, isSvelteDoc, isRustContext } from './html' import { State } from './state' import { indexToPosition } from './find' import { isJsDoc } from './js' @@ -138,7 +138,7 @@ export function getLanguageBoundaries( let defaultType = isVueDoc(doc) ? 'none' - : isHtmlDoc(state, doc) || isSvelteDoc(doc) + : isHtmlDoc(state, doc) || isSvelteDoc(doc) || isRustContext(doc) ? 'html' : isJs ? 'jsx' diff --git a/packages/tailwindcss-language-service/src/util/html.ts b/packages/tailwindcss-language-service/src/util/html.ts index a992f08d..9fba9db0 100644 --- a/packages/tailwindcss-language-service/src/util/html.ts +++ b/packages/tailwindcss-language-service/src/util/html.ts @@ -30,3 +30,7 @@ export function isHtmlContext(state: State, doc: TextDocument, position: Positio return boundaries ? boundaries[boundaries.length - 1].type === 'html' : false } + +export function isRustContext(doc: TextDocument): boolean { + return doc.languageId === 'rust'; +}