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 grammar
master
Brad Cornes 2022-04-13 14:05:41 +01:00 committed by GitHub
parent 0a6e5def00
commit fbbd4bc2d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 20511 additions and 36895 deletions

56684
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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()

View File

@ -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
}
},
}
}

View File

@ -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',

View File

@ -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:
```

View File

@ -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",

View File

@ -0,0 +1 @@
import 'tailwindcss-language-server/src/language/cssServer'

View File

@ -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

View File

@ -0,0 +1,9 @@
{
"name": "TailwindCSS",
"scopeName": "source.css.tailwind",
"patterns": [
{
"include": "source.css"
}
]
}

View File

@ -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-?]+%?|[@#!.])"
}