Add Tailwind CSS language mode (#518)
* Use `esbuild` * Replace direct `eval` * add initial language mode * Update capabilities, add rename provider * Update vscode types * Add completion middleware to match built-in CSS provider * Update language config to match built-in CSS language * Add folding region completion provider * Add stylesheet cache * Add log and error handling * Update settings handling, debounce validation * Fix response flakiness by always augmenting CSS * Respect folding range limit * Use uncommon symbol as `@media` placeholder * Update readme * Use `esbuild` for language server build * Add `theme()` completion item * Avoid errors when using `@media screen()` * Update readme * Add CSS server to build script * Rename language mode server file in prod * Update VS Code `engines` and types versions * Update grammarmaster
parent
0a6e5def00
commit
fbbd4bc2d0
File diff suppressed because it is too large
Load Diff
|
@ -29,7 +29,7 @@
|
|||
"@tailwindcss/typography": "0.5.0",
|
||||
"@types/debounce": "1.2.0",
|
||||
"@types/node": "14.14.34",
|
||||
"@types/vscode": "1.52.0",
|
||||
"@types/vscode": "1.60.0",
|
||||
"builtin-modules": "3.2.0",
|
||||
"chokidar": "3.5.1",
|
||||
"color-name": "1.1.4",
|
||||
|
@ -57,6 +57,7 @@
|
|||
"tailwindcss": "3.0.11",
|
||||
"terser": "4.6.12",
|
||||
"typescript": "4.2.4",
|
||||
"vscode-css-languageservice": "5.4.1",
|
||||
"vscode-languageserver": "7.0.0",
|
||||
"vscode-languageserver-textdocument": "1.0.1",
|
||||
"vscode-uri": "3.0.2"
|
||||
|
|
|
@ -0,0 +1,401 @@
|
|||
import {
|
||||
getCSSLanguageService,
|
||||
LanguageSettings,
|
||||
DocumentContext,
|
||||
} from 'vscode-css-languageservice/lib/esm/cssLanguageService'
|
||||
import {
|
||||
createConnection,
|
||||
InitializeParams,
|
||||
ProposedFeatures,
|
||||
TextDocuments,
|
||||
TextDocumentSyncKind,
|
||||
WorkspaceFolder,
|
||||
Disposable,
|
||||
ConfigurationRequest,
|
||||
CompletionItemKind,
|
||||
} from 'vscode-languageserver/node'
|
||||
import { TextDocument } from 'vscode-languageserver-textdocument'
|
||||
import { Utils, URI } from 'vscode-uri'
|
||||
import { getLanguageModelCache } from './languageModelCache'
|
||||
import { Stylesheet } from 'vscode-css-languageservice'
|
||||
import dlv from 'dlv'
|
||||
|
||||
let connection = createConnection(ProposedFeatures.all)
|
||||
|
||||
console.log = connection.console.log.bind(connection.console)
|
||||
console.error = connection.console.error.bind(connection.console)
|
||||
|
||||
function formatError(message: string, err: any): string {
|
||||
if (err instanceof Error) {
|
||||
let error = <Error>err
|
||||
return `${message}: ${error.message}\n${error.stack}`
|
||||
} else if (typeof err === 'string') {
|
||||
return `${message}: ${err}`
|
||||
} else if (err) {
|
||||
return `${message}: ${err.toString()}`
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', (e: any) => {
|
||||
connection.console.error(formatError(`Unhandled exception`, e))
|
||||
})
|
||||
|
||||
let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument)
|
||||
|
||||
let cssLanguageService = getCSSLanguageService()
|
||||
|
||||
let workspaceFolders: WorkspaceFolder[]
|
||||
|
||||
let foldingRangeLimit = Number.MAX_VALUE
|
||||
const MEDIA_MARKER = '℘'
|
||||
|
||||
const stylesheets = getLanguageModelCache<Stylesheet>(10, 60, (document) =>
|
||||
cssLanguageService.parseStylesheet(document)
|
||||
)
|
||||
documents.onDidClose(({ document }) => {
|
||||
stylesheets.onDocumentRemoved(document)
|
||||
})
|
||||
connection.onShutdown(() => {
|
||||
stylesheets.dispose()
|
||||
})
|
||||
|
||||
connection.onInitialize((params: InitializeParams) => {
|
||||
workspaceFolders = (<any>params).workspaceFolders
|
||||
if (!Array.isArray(workspaceFolders)) {
|
||||
workspaceFolders = []
|
||||
if (params.rootPath) {
|
||||
workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() })
|
||||
}
|
||||
}
|
||||
|
||||
foldingRangeLimit = dlv(
|
||||
params.capabilities,
|
||||
'textDocument.foldingRange.rangeLimit',
|
||||
Number.MAX_VALUE
|
||||
)
|
||||
|
||||
return {
|
||||
capabilities: {
|
||||
textDocumentSync: TextDocumentSyncKind.Full,
|
||||
completionProvider: { resolveProvider: false, triggerCharacters: ['/', '-', ':'] },
|
||||
hoverProvider: true,
|
||||
foldingRangeProvider: true,
|
||||
colorProvider: {},
|
||||
definitionProvider: true,
|
||||
documentHighlightProvider: true,
|
||||
documentSymbolProvider: true,
|
||||
selectionRangeProvider: true,
|
||||
referencesProvider: true,
|
||||
codeActionProvider: true,
|
||||
documentLinkProvider: { resolveProvider: false },
|
||||
renameProvider: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
function getDocumentContext(
|
||||
documentUri: string,
|
||||
workspaceFolders: WorkspaceFolder[]
|
||||
): DocumentContext {
|
||||
function getRootFolder(): string | undefined {
|
||||
for (let folder of workspaceFolders) {
|
||||
let folderURI = folder.uri
|
||||
if (!folderURI.endsWith('/')) {
|
||||
folderURI = folderURI + '/'
|
||||
}
|
||||
if (documentUri.startsWith(folderURI)) {
|
||||
return folderURI
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
resolveReference: (ref: string, base = documentUri) => {
|
||||
if (ref[0] === '/') {
|
||||
// resolve absolute path against the current workspace folder
|
||||
let folderUri = getRootFolder()
|
||||
if (folderUri) {
|
||||
return folderUri + ref.substr(1)
|
||||
}
|
||||
}
|
||||
base = base.substr(0, base.lastIndexOf('/') + 1)
|
||||
return Utils.resolvePath(URI.parse(base), ref).toString()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function withDocumentAndSettings<T>(
|
||||
uri: string,
|
||||
callback: (result: {
|
||||
document: TextDocument
|
||||
settings: LanguageSettings | undefined
|
||||
}) => T | Promise<T>
|
||||
): Promise<T> {
|
||||
let document = documents.get(uri)
|
||||
if (!document) {
|
||||
return null
|
||||
}
|
||||
return await callback({
|
||||
document: createVirtualCssDocument(document),
|
||||
settings: await getDocumentSettings(document),
|
||||
})
|
||||
}
|
||||
|
||||
connection.onCompletion(async ({ textDocument, position }, _token) =>
|
||||
withDocumentAndSettings(textDocument.uri, async ({ document, settings }) => {
|
||||
let result = await cssLanguageService.doComplete2(
|
||||
document,
|
||||
position,
|
||||
stylesheets.get(document),
|
||||
getDocumentContext(document.uri, workspaceFolders),
|
||||
settings?.completion
|
||||
)
|
||||
return {
|
||||
isIncomplete: result.isIncomplete,
|
||||
items: result.items.flatMap((item) => {
|
||||
// Add the `theme()` function
|
||||
if (item.kind === CompletionItemKind.Function && item.label === 'calc()') {
|
||||
return [
|
||||
item,
|
||||
{
|
||||
...item,
|
||||
label: 'theme()',
|
||||
documentation: {
|
||||
kind: 'markdown',
|
||||
value:
|
||||
'Use the `theme()` function to access your Tailwind config values using dot notation.',
|
||||
},
|
||||
textEdit: {
|
||||
...item.textEdit,
|
||||
newText: item.textEdit.newText.replace(/^calc\(/, 'theme('),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
return item
|
||||
}),
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
connection.onHover(({ textDocument, position }, _token) =>
|
||||
withDocumentAndSettings(textDocument.uri, ({ document, settings }) =>
|
||||
cssLanguageService.doHover(document, position, stylesheets.get(document), settings?.hover)
|
||||
)
|
||||
)
|
||||
|
||||
connection.onFoldingRanges(({ textDocument }, _token) =>
|
||||
withDocumentAndSettings(textDocument.uri, ({ document }) =>
|
||||
cssLanguageService.getFoldingRanges(document, { rangeLimit: foldingRangeLimit })
|
||||
)
|
||||
)
|
||||
|
||||
connection.onDocumentColor(({ textDocument }) =>
|
||||
withDocumentAndSettings(textDocument.uri, ({ document }) =>
|
||||
cssLanguageService.findDocumentColors(document, stylesheets.get(document))
|
||||
)
|
||||
)
|
||||
|
||||
connection.onColorPresentation(({ textDocument, color, range }) =>
|
||||
withDocumentAndSettings(textDocument.uri, ({ document }) =>
|
||||
cssLanguageService.getColorPresentations(document, stylesheets.get(document), color, range)
|
||||
)
|
||||
)
|
||||
|
||||
connection.onDefinition(({ textDocument, position }) =>
|
||||
withDocumentAndSettings(textDocument.uri, ({ document }) =>
|
||||
cssLanguageService.findDefinition(document, position, stylesheets.get(document))
|
||||
)
|
||||
)
|
||||
|
||||
connection.onDocumentHighlight(({ textDocument, position }) =>
|
||||
withDocumentAndSettings(textDocument.uri, ({ document }) =>
|
||||
cssLanguageService.findDocumentHighlights(document, position, stylesheets.get(document))
|
||||
)
|
||||
)
|
||||
|
||||
connection.onDocumentSymbol(({ textDocument }) =>
|
||||
withDocumentAndSettings(textDocument.uri, ({ document }) =>
|
||||
cssLanguageService.findDocumentSymbols(document, stylesheets.get(document)).map((symbol) => {
|
||||
if (symbol.name === `@media (${MEDIA_MARKER})`) {
|
||||
let doc = documents.get(symbol.location.uri)
|
||||
let text = doc.getText(symbol.location.range)
|
||||
let match = text.trim().match(/^(@[^\s]+)([^{]+){/)
|
||||
if (match) {
|
||||
symbol.name = `${match[1]} ${match[2].trim()}`
|
||||
}
|
||||
}
|
||||
return symbol
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
connection.onSelectionRanges(({ textDocument, positions }) =>
|
||||
withDocumentAndSettings(textDocument.uri, ({ document }) =>
|
||||
cssLanguageService.getSelectionRanges(document, positions, stylesheets.get(document))
|
||||
)
|
||||
)
|
||||
|
||||
connection.onReferences(({ textDocument, position }) =>
|
||||
withDocumentAndSettings(textDocument.uri, ({ document }) =>
|
||||
cssLanguageService.findReferences(document, position, stylesheets.get(document))
|
||||
)
|
||||
)
|
||||
|
||||
connection.onCodeAction(({ textDocument, range, context }) =>
|
||||
withDocumentAndSettings(textDocument.uri, ({ document }) =>
|
||||
cssLanguageService.doCodeActions2(document, range, context, stylesheets.get(document))
|
||||
)
|
||||
)
|
||||
|
||||
connection.onDocumentLinks(({ textDocument }) =>
|
||||
withDocumentAndSettings(textDocument.uri, ({ document }) =>
|
||||
cssLanguageService.findDocumentLinks2(
|
||||
document,
|
||||
stylesheets.get(document),
|
||||
getDocumentContext(document.uri, workspaceFolders)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
connection.onRenameRequest(({ textDocument, position, newName }) =>
|
||||
withDocumentAndSettings(textDocument.uri, ({ document }) =>
|
||||
cssLanguageService.doRename(document, position, newName, stylesheets.get(document))
|
||||
)
|
||||
)
|
||||
|
||||
let documentSettings: { [key: string]: Thenable<LanguageSettings | undefined> } = {}
|
||||
documents.onDidClose((e) => {
|
||||
delete documentSettings[e.document.uri]
|
||||
})
|
||||
function getDocumentSettings(textDocument: TextDocument): Thenable<LanguageSettings | undefined> {
|
||||
let promise = documentSettings[textDocument.uri]
|
||||
if (!promise) {
|
||||
const configRequestParam = {
|
||||
items: [{ scopeUri: textDocument.uri, section: 'css' }],
|
||||
}
|
||||
promise = connection
|
||||
.sendRequest(ConfigurationRequest.type, configRequestParam)
|
||||
.then((s) => s[0])
|
||||
documentSettings[textDocument.uri] = promise
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
connection.onDidChangeConfiguration((change) => {
|
||||
updateConfiguration(<LanguageSettings>change.settings.css)
|
||||
})
|
||||
|
||||
function updateConfiguration(settings: LanguageSettings) {
|
||||
cssLanguageService.configure(settings)
|
||||
// reset all document settings
|
||||
documentSettings = {}
|
||||
documents.all().forEach(triggerValidation)
|
||||
}
|
||||
|
||||
const pendingValidationRequests: { [uri: string]: Disposable } = {}
|
||||
const validationDelayMs = 500
|
||||
|
||||
const timer = {
|
||||
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable {
|
||||
const handle = setTimeout(callback, ms, ...args)
|
||||
return { dispose: () => clearTimeout(handle) }
|
||||
},
|
||||
}
|
||||
|
||||
documents.onDidChangeContent((change) => {
|
||||
triggerValidation(change.document)
|
||||
})
|
||||
|
||||
documents.onDidClose((event) => {
|
||||
cleanPendingValidation(event.document)
|
||||
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] })
|
||||
})
|
||||
|
||||
function cleanPendingValidation(textDocument: TextDocument): void {
|
||||
const request = pendingValidationRequests[textDocument.uri]
|
||||
if (request) {
|
||||
request.dispose()
|
||||
delete pendingValidationRequests[textDocument.uri]
|
||||
}
|
||||
}
|
||||
|
||||
function triggerValidation(textDocument: TextDocument): void {
|
||||
cleanPendingValidation(textDocument)
|
||||
pendingValidationRequests[textDocument.uri] = timer.setTimeout(() => {
|
||||
delete pendingValidationRequests[textDocument.uri]
|
||||
validateTextDocument(textDocument)
|
||||
}, validationDelayMs)
|
||||
}
|
||||
|
||||
function replace(delta = 0) {
|
||||
return (_match: string, p1: string) => {
|
||||
let lines = p1.split('\n')
|
||||
if (lines.length > 1) {
|
||||
return `@media(${MEDIA_MARKER})${'\n'.repeat(lines.length - 1)}${' '.repeat(
|
||||
lines[lines.length - 1].length
|
||||
)}{`
|
||||
}
|
||||
return `@media(${MEDIA_MARKER})${' '.repeat(p1.length + delta)}{`
|
||||
}
|
||||
}
|
||||
|
||||
function createVirtualCssDocument(textDocument: TextDocument): TextDocument {
|
||||
return TextDocument.create(
|
||||
textDocument.uri,
|
||||
textDocument.languageId,
|
||||
textDocument.version,
|
||||
textDocument
|
||||
.getText()
|
||||
.replace(/@screen(\s+[^{]+){/g, replace(-2))
|
||||
.replace(/@variants(\s+[^{]+){/g, replace())
|
||||
.replace(/@responsive(\s*){/g, replace())
|
||||
.replace(/@layer(\s+[^{]{2,}){/g, replace(-3))
|
||||
.replace(
|
||||
/@media(\s+screen\s*\([^)]+\))/g,
|
||||
(_match, screen) => `@media (${MEDIA_MARKER})${' '.repeat(screen.length - 4)}`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async function validateTextDocument(textDocument: TextDocument): Promise<void> {
|
||||
textDocument = createVirtualCssDocument(textDocument)
|
||||
|
||||
let settings = await getDocumentSettings(textDocument)
|
||||
|
||||
// let stylesheet = cssLanguageService.parseStylesheet(textDocument) as any
|
||||
// stylesheet.acceptVisitor({
|
||||
// visitNode(node) {
|
||||
// if (node instanceof nodes.UnknownAtRule) {
|
||||
// console.log(
|
||||
// node.accept((node) => {
|
||||
// console.log(node)
|
||||
// })
|
||||
// )
|
||||
// }
|
||||
// if (node.getText().includes('base')) {
|
||||
// // console.log(node)
|
||||
// }
|
||||
// return true
|
||||
// },
|
||||
// })
|
||||
|
||||
let diagnostics = cssLanguageService
|
||||
.doValidation(textDocument, cssLanguageService.parseStylesheet(textDocument), settings)
|
||||
.filter((diagnostic) => {
|
||||
if (
|
||||
diagnostic.code === 'unknownAtRules' &&
|
||||
/Unknown at rule @(tailwind|apply)/.test(diagnostic.message)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics })
|
||||
}
|
||||
|
||||
documents.listen(connection)
|
||||
connection.listen()
|
|
@ -0,0 +1,91 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TextDocument } from 'vscode-css-languageservice'
|
||||
|
||||
export interface LanguageModelCache<T> {
|
||||
get(document: TextDocument): T
|
||||
onDocumentRemoved(document: TextDocument): void
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
export function getLanguageModelCache<T>(
|
||||
maxEntries: number,
|
||||
cleanupIntervalTimeInSec: number,
|
||||
parse: (document: TextDocument) => T
|
||||
): LanguageModelCache<T> {
|
||||
let languageModels: {
|
||||
[uri: string]: { version: number; languageId: string; cTime: number; languageModel: T }
|
||||
} = {}
|
||||
let nModels = 0
|
||||
|
||||
let cleanupInterval: NodeJS.Timer | undefined = undefined
|
||||
if (cleanupIntervalTimeInSec > 0) {
|
||||
cleanupInterval = setInterval(() => {
|
||||
let cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000
|
||||
let uris = Object.keys(languageModels)
|
||||
for (let uri of uris) {
|
||||
let languageModelInfo = languageModels[uri]
|
||||
if (languageModelInfo.cTime < cutoffTime) {
|
||||
delete languageModels[uri]
|
||||
nModels--
|
||||
}
|
||||
}
|
||||
}, cleanupIntervalTimeInSec * 1000)
|
||||
}
|
||||
|
||||
return {
|
||||
get(document: TextDocument): T {
|
||||
let version = document.version
|
||||
let languageId = document.languageId
|
||||
let languageModelInfo = languageModels[document.uri]
|
||||
if (
|
||||
languageModelInfo &&
|
||||
languageModelInfo.version === version &&
|
||||
languageModelInfo.languageId === languageId
|
||||
) {
|
||||
languageModelInfo.cTime = Date.now()
|
||||
return languageModelInfo.languageModel
|
||||
}
|
||||
let languageModel = parse(document)
|
||||
languageModels[document.uri] = { languageModel, version, languageId, cTime: Date.now() }
|
||||
if (!languageModelInfo) {
|
||||
nModels++
|
||||
}
|
||||
|
||||
if (nModels === maxEntries) {
|
||||
let oldestTime = Number.MAX_VALUE
|
||||
let oldestUri = null
|
||||
for (let uri in languageModels) {
|
||||
let languageModelInfo = languageModels[uri]
|
||||
if (languageModelInfo.cTime < oldestTime) {
|
||||
oldestUri = uri
|
||||
oldestTime = languageModelInfo.cTime
|
||||
}
|
||||
}
|
||||
if (oldestUri) {
|
||||
delete languageModels[oldestUri]
|
||||
nModels--
|
||||
}
|
||||
}
|
||||
return languageModel
|
||||
},
|
||||
onDocumentRemoved(document: TextDocument) {
|
||||
let uri = document.uri
|
||||
if (languageModels[uri]) {
|
||||
delete languageModels[uri]
|
||||
nModels--
|
||||
}
|
||||
},
|
||||
dispose() {
|
||||
if (typeof cleanupInterval !== 'undefined') {
|
||||
clearInterval(cleanupInterval)
|
||||
cleanupInterval = undefined
|
||||
languageModels = {}
|
||||
nModels = 0
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
|
@ -32,7 +32,16 @@ export const htmlLanguages = [
|
|||
'twig',
|
||||
]
|
||||
|
||||
export const cssLanguages = ['css', 'less', 'postcss', 'sass', 'scss', 'stylus', 'sugarss']
|
||||
export const cssLanguages = [
|
||||
'css',
|
||||
'less',
|
||||
'postcss',
|
||||
'sass',
|
||||
'scss',
|
||||
'stylus',
|
||||
'sugarss',
|
||||
'tailwindcss',
|
||||
]
|
||||
|
||||
export const jsLanguages = [
|
||||
'javascript',
|
||||
|
|
|
@ -28,18 +28,24 @@ See the complete CSS for a Tailwind class name by hovering over it.
|
|||
|
||||
<img src="https://raw.githubusercontent.com/bradlc/vscode-tailwindcss/master/packages/vscode-tailwindcss/.github/hover.png" alt="" />
|
||||
|
||||
### CSS Syntax Highlighting
|
||||
### Tailwind CSS Language Mode
|
||||
|
||||
Provides syntax definitions so that Tailwind features are highlighted correctly.
|
||||
An alternative to VS Code's built-in CSS language mode which maintains full CSS IntelliSense support even when using Tailwind-specific at-rules. Syntax definitions are also provided so that Tailwind-specific syntax is highlighted correctly in all CSS contexts.
|
||||
|
||||
## Recommended VS Code Settings
|
||||
|
||||
VS Code has built-in CSS validation which may display errors when using Tailwind-specific syntax, such as `@apply`. You can disable this with the `css.validate` setting:
|
||||
### `files.associations`
|
||||
|
||||
Use the `files.associations` setting to tell VS Code to always open `.css` files in Tailwind CSS mode:
|
||||
|
||||
```
|
||||
"css.validate": false
|
||||
"files.associations": {
|
||||
"*.css": "tailwindcss"
|
||||
}
|
||||
```
|
||||
|
||||
### `editor.quickSuggestions`
|
||||
|
||||
By default VS Code will not trigger completions when editing "string" content, for example within JSX attribute values. Updating the `editor.quickSuggestions` setting may improve your experience:
|
||||
|
||||
```
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"vscode"
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.52.0"
|
||||
"vscode": "^1.60.0"
|
||||
},
|
||||
"categories": [
|
||||
"Linters",
|
||||
|
@ -43,6 +43,21 @@
|
|||
"virtualWorkspaces": false
|
||||
},
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
"id": "tailwindcss",
|
||||
"aliases": [
|
||||
"Tailwind CSS"
|
||||
],
|
||||
"extensions": [
|
||||
".css"
|
||||
],
|
||||
"mimetypes": [
|
||||
"text/css"
|
||||
],
|
||||
"configuration": "./tailwindcss.language.configuration.json"
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"command": "tailwindCSS.showOutput",
|
||||
|
@ -51,6 +66,14 @@
|
|||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "tailwindcss",
|
||||
"scopeName": "source.css.tailwind",
|
||||
"path": "./syntaxes/source.css.tailwind.tmLanguage.json",
|
||||
"tokenTypes": {
|
||||
"meta.function.url string.quoted": "other"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scopeName": "tailwindcss.at-rules.injection",
|
||||
"path": "./syntaxes/at-rules.tmLanguage.json",
|
||||
|
@ -258,10 +281,10 @@
|
|||
}
|
||||
},
|
||||
"scripts": {
|
||||
"_esbuild": "node ../../esbuild.js src/extension.ts src/server.ts --outdir=dist --external=pnpapi --external=vscode",
|
||||
"_esbuild": "node ../../esbuild.js src/extension.ts src/server.ts src/cssServer.ts --outdir=dist --external=pnpapi --external=vscode",
|
||||
"dev": "concurrently --raw --kill-others \"npm run watch\" \"npm run check -- --watch\"",
|
||||
"watch": "npm run clean && npm run _esbuild -- --watch",
|
||||
"build": "npm run check && npm run clean && npm run _esbuild -- --minify && mv dist/server.js dist/tailwindServer.js",
|
||||
"build": "npm run check && npm run clean && npm run _esbuild -- --minify && mv dist/server.js dist/tailwindServer.js && mv dist/cssServer.js dist/tailwindModeServer.js",
|
||||
"package": "vsce package",
|
||||
"publish": "vsce publish",
|
||||
"copy:notices": "cp ../tailwindcss-language-server/ThirdPartyNotices.txt ./dist/ThirdPartyNotices.txt",
|
||||
|
@ -271,7 +294,7 @@
|
|||
"check": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "1.52.0",
|
||||
"@types/vscode": "1.60.0",
|
||||
"color-name": "1.1.4",
|
||||
"concurrently": "7.0.0",
|
||||
"rimraf": "3.0.2",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
import 'tailwindcss-language-server/src/language/cssServer'
|
|
@ -6,6 +6,7 @@ import * as path from 'path'
|
|||
import {
|
||||
workspace as Workspace,
|
||||
window as Window,
|
||||
languages as Languages,
|
||||
ExtensionContext,
|
||||
TextDocument,
|
||||
OutputChannel,
|
||||
|
@ -19,6 +20,12 @@ import {
|
|||
RelativePattern,
|
||||
ConfigurationScope,
|
||||
WorkspaceConfiguration,
|
||||
CompletionItem,
|
||||
CompletionItemKind,
|
||||
CompletionList,
|
||||
ProviderResult,
|
||||
SnippetString,
|
||||
TextEdit,
|
||||
} from 'vscode'
|
||||
import {
|
||||
LanguageClient,
|
||||
|
@ -27,6 +34,7 @@ import {
|
|||
TransportKind,
|
||||
State as LanguageClientState,
|
||||
RevealOutputChannelOn,
|
||||
Disposable,
|
||||
} from 'vscode-languageclient/node'
|
||||
import { languages as defaultLanguages } from 'tailwindcss-language-service/src/util/languages'
|
||||
import isObject from 'tailwindcss-language-service/src/util/isObject'
|
||||
|
@ -184,6 +192,117 @@ export async function activate(context: ExtensionContext) {
|
|||
})
|
||||
)
|
||||
|
||||
let cssServerBooted = false
|
||||
async function bootCssServer() {
|
||||
if (cssServerBooted) return
|
||||
cssServerBooted = true
|
||||
|
||||
let module = context.asAbsolutePath(path.join('dist', 'cssServer.js'))
|
||||
let prod = path.join('dist', 'tailwindModeServer.js')
|
||||
|
||||
try {
|
||||
await Workspace.fs.stat(Uri.joinPath(context.extensionUri, prod))
|
||||
module = context.asAbsolutePath(prod)
|
||||
} catch (_) {}
|
||||
|
||||
let client = new LanguageClient(
|
||||
'tailwindcss-intellisense-css',
|
||||
'Tailwind CSS',
|
||||
{
|
||||
run: {
|
||||
module,
|
||||
transport: TransportKind.ipc,
|
||||
},
|
||||
debug: {
|
||||
module,
|
||||
transport: TransportKind.ipc,
|
||||
options: {
|
||||
execArgv: ['--nolazy', '--inspect=6051'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
documentSelector: [{ language: 'tailwindcss' }],
|
||||
outputChannelName: 'Tailwind CSS Language Mode',
|
||||
synchronize: { configurationSection: ['css'] },
|
||||
middleware: {
|
||||
provideCompletionItem(document, position, context, token, next) {
|
||||
function updateRanges(item: CompletionItem) {
|
||||
const range = item.range
|
||||
if (
|
||||
range instanceof Range &&
|
||||
range.end.isAfter(position) &&
|
||||
range.start.isBeforeOrEqual(position)
|
||||
) {
|
||||
item.range = { inserting: new Range(range.start, position), replacing: range }
|
||||
}
|
||||
}
|
||||
function updateLabel(item: CompletionItem) {
|
||||
if (item.kind === CompletionItemKind.Color) {
|
||||
item.label = {
|
||||
label: item.label as string,
|
||||
description: item.documentation as string,
|
||||
}
|
||||
}
|
||||
}
|
||||
function updateProposals(
|
||||
r: CompletionItem[] | CompletionList | null | undefined
|
||||
): CompletionItem[] | CompletionList | null | undefined {
|
||||
if (r) {
|
||||
;(Array.isArray(r) ? r : r.items).forEach(updateRanges)
|
||||
;(Array.isArray(r) ? r : r.items).forEach(updateLabel)
|
||||
}
|
||||
return r
|
||||
}
|
||||
const isThenable = <T>(obj: ProviderResult<T>): obj is Thenable<T> =>
|
||||
obj && (<any>obj)['then']
|
||||
|
||||
const r = next(document, position, context, token)
|
||||
if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {
|
||||
return r.then(updateProposals)
|
||||
}
|
||||
return updateProposals(r)
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
client.onReady().then(() => {
|
||||
context.subscriptions.push(initCompletionProvider())
|
||||
})
|
||||
|
||||
context.subscriptions.push(client.start())
|
||||
|
||||
function initCompletionProvider(): Disposable {
|
||||
const regionCompletionRegExpr = /^(\s*)(\/(\*\s*(#\w*)?)?)?$/
|
||||
|
||||
return Languages.registerCompletionItemProvider(['tailwindcss'], {
|
||||
provideCompletionItems(doc: TextDocument, pos: Position) {
|
||||
let lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos))
|
||||
let match = lineUntilPos.match(regionCompletionRegExpr)
|
||||
if (match) {
|
||||
let range = new Range(new Position(pos.line, match[1].length), pos)
|
||||
let beginProposal = new CompletionItem('#region', CompletionItemKind.Snippet)
|
||||
beginProposal.range = range
|
||||
TextEdit.replace(range, '/* #region */')
|
||||
beginProposal.insertText = new SnippetString('/* #region $1*/')
|
||||
beginProposal.documentation = 'Folding Region Start'
|
||||
beginProposal.filterText = match[2]
|
||||
beginProposal.sortText = 'za'
|
||||
let endProposal = new CompletionItem('#endregion', CompletionItemKind.Snippet)
|
||||
endProposal.range = range
|
||||
endProposal.insertText = '/* #endregion */'
|
||||
endProposal.documentation = 'Folding Region End'
|
||||
endProposal.sortText = 'zb'
|
||||
endProposal.filterText = match[2]
|
||||
return [beginProposal, endProposal]
|
||||
}
|
||||
return null
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function bootWorkspaceClient(folder: WorkspaceFolder) {
|
||||
if (clients.has(folder.uri.toString())) {
|
||||
return
|
||||
|
@ -282,7 +401,8 @@ export async function activate(context: ExtensionContext) {
|
|||
return { range, newText: result.additionalTextEdits[0].newText }
|
||||
})
|
||||
} else {
|
||||
result.insertText = result.label
|
||||
result.insertText =
|
||||
typeof result.label === 'string' ? result.label : result.label.label
|
||||
result.additionalTextEdits = []
|
||||
}
|
||||
}
|
||||
|
@ -401,6 +521,10 @@ export async function activate(context: ExtensionContext) {
|
|||
}
|
||||
|
||||
async function didOpenTextDocument(document: TextDocument): Promise<void> {
|
||||
if (document.languageId === 'tailwindcss') {
|
||||
bootCssServer()
|
||||
}
|
||||
|
||||
// We are only interested in language mode text
|
||||
if (document.uri.scheme !== 'file') {
|
||||
return
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "TailwindCSS",
|
||||
"scopeName": "source.css.tailwind",
|
||||
"patterns": [
|
||||
{
|
||||
"include": "source.css"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"comments": {
|
||||
"blockComment": ["/*", "*/"]
|
||||
},
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}", "notIn": ["string", "comment"] },
|
||||
{ "open": "[", "close": "]", "notIn": ["string", "comment"] },
|
||||
{ "open": "(", "close": ")", "notIn": ["string", "comment"] },
|
||||
{ "open": "\"", "close": "\"", "notIn": ["string", "comment"] },
|
||||
{ "open": "'", "close": "'", "notIn": ["string", "comment"] }
|
||||
],
|
||||
"surroundingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""],
|
||||
["'", "'"]
|
||||
],
|
||||
"folding": {
|
||||
"markers": {
|
||||
"start": "^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/",
|
||||
"end": "^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/"
|
||||
}
|
||||
},
|
||||
"indentationRules": {
|
||||
"increaseIndentPattern": "(^.*\\{[^}]*$)",
|
||||
"decreaseIndentPattern": "^\\s*\\}"
|
||||
},
|
||||
"wordPattern": "(#?-?\\d*\\.\\d\\w*%?)|(::?[\\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\\w-?]+%?|[@#!.])"
|
||||
}
|
Loading…
Reference in New Issue