Theme helper improvements
parent
c9acd0d124
commit
1b730cb656
|
@ -162,11 +162,16 @@ connection.onCompletion(async ({ textDocument, position }, _token) =>
|
||||||
{
|
{
|
||||||
...item,
|
...item,
|
||||||
label: 'theme()',
|
label: 'theme()',
|
||||||
|
filterText: 'theme',
|
||||||
documentation: {
|
documentation: {
|
||||||
kind: 'markdown',
|
kind: 'markdown',
|
||||||
value:
|
value:
|
||||||
'Use the `theme()` function to access your Tailwind config values using dot notation.',
|
'Use the `theme()` function to access your Tailwind config values using dot notation.',
|
||||||
},
|
},
|
||||||
|
command: {
|
||||||
|
title: '',
|
||||||
|
command: 'editor.action.triggerSuggest',
|
||||||
|
},
|
||||||
textEdit: {
|
textEdit: {
|
||||||
...item.textEdit,
|
...item.textEdit,
|
||||||
newText: item.textEdit.newText.replace(/^calc\(/, 'theme('),
|
newText: item.textEdit.newText.replace(/^calc\(/, 'theme('),
|
||||||
|
@ -357,6 +362,7 @@ function createVirtualCssDocument(textDocument: TextDocument): TextDocument {
|
||||||
/@media(\s+screen\s*\([^)]+\))/g,
|
/@media(\s+screen\s*\([^)]+\))/g,
|
||||||
(_match, screen) => `@media (${MEDIA_MARKER})${' '.repeat(screen.length - 4)}`
|
(_match, screen) => `@media (${MEDIA_MARKER})${' '.repeat(screen.length - 4)}`
|
||||||
)
|
)
|
||||||
|
.replace(/(?<=\b(?:theme|config)\([^)]*)[.[\]]/g, '_')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -112,6 +112,7 @@ const TRIGGER_CHARACTERS = [
|
||||||
// @apply and emmet-style
|
// @apply and emmet-style
|
||||||
'.',
|
'.',
|
||||||
// config/theme helper
|
// config/theme helper
|
||||||
|
'(',
|
||||||
'[',
|
'[',
|
||||||
// JIT "important" prefix
|
// JIT "important" prefix
|
||||||
'!',
|
'!',
|
||||||
|
|
|
@ -503,6 +503,11 @@ function provideAtApplyCompletions(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NUMBER_REGEX = /^(\d+\.?|\d*\.\d+)$/
|
||||||
|
function isNumber(str: string): boolean {
|
||||||
|
return NUMBER_REGEX.test(str)
|
||||||
|
}
|
||||||
|
|
||||||
async function provideClassNameCompletions(
|
async function provideClassNameCompletions(
|
||||||
state: State,
|
state: State,
|
||||||
document: TextDocument,
|
document: TextDocument,
|
||||||
|
@ -537,14 +542,26 @@ function provideCssHelperCompletions(
|
||||||
|
|
||||||
const match = text
|
const match = text
|
||||||
.substr(0, text.length - 1) // don't include that extra character from earlier
|
.substr(0, text.length - 1) // don't include that extra character from earlier
|
||||||
.match(/\b(?<helper>config|theme)\(['"](?<keys>[^'"]*)$/)
|
.match(/\b(?<helper>config|theme)\(\s*['"]?(?<path>[^)'"]*)$/)
|
||||||
|
|
||||||
if (match === null) {
|
if (match === null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let alpha: string
|
||||||
|
let path = match.groups.path.replace(/^['"]+/g, '')
|
||||||
|
let matches = path.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]*))$/)
|
||||||
|
if (matches) {
|
||||||
|
path = matches[1]
|
||||||
|
alpha = matches[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alpha !== undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
let base = match.groups.helper === 'config' ? state.config : dlv(state.config, 'theme', {})
|
let base = match.groups.helper === 'config' ? state.config : dlv(state.config, 'theme', {})
|
||||||
let parts = match.groups.keys.split(/([\[\].]+)/)
|
let parts = path.split(/([\[\].]+)/)
|
||||||
let keys = parts.filter((_, i) => i % 2 === 0)
|
let keys = parts.filter((_, i) => i % 2 === 0)
|
||||||
let separators = parts.filter((_, i) => i % 2 !== 0)
|
let separators = parts.filter((_, i) => i % 2 !== 0)
|
||||||
// let obj =
|
// let obj =
|
||||||
|
@ -557,7 +574,7 @@ function provideCssHelperCompletions(
|
||||||
}
|
}
|
||||||
|
|
||||||
let obj: any
|
let obj: any
|
||||||
let offset: number = 0
|
let offset: number = keys[keys.length - 1].length
|
||||||
let separator: string = separators.length ? separators[separators.length - 1] : null
|
let separator: string = separators.length ? separators[separators.length - 1] : null
|
||||||
|
|
||||||
if (keys.length === 1) {
|
if (keys.length === 1) {
|
||||||
|
@ -576,9 +593,32 @@ function provideCssHelperCompletions(
|
||||||
|
|
||||||
if (!obj) return null
|
if (!obj) return null
|
||||||
|
|
||||||
|
let editRange = {
|
||||||
|
start: {
|
||||||
|
line: position.line,
|
||||||
|
character: position.character - offset,
|
||||||
|
},
|
||||||
|
end: position,
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isIncomplete: false,
|
isIncomplete: false,
|
||||||
items: Object.keys(obj).map((item, index) => {
|
items: Object.keys(obj)
|
||||||
|
.sort((a, z) => {
|
||||||
|
let aIsNumber = isNumber(a)
|
||||||
|
let zIsNumber = isNumber(z)
|
||||||
|
if (aIsNumber && !zIsNumber) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (!aIsNumber && zIsNumber) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if (aIsNumber && zIsNumber) {
|
||||||
|
return parseFloat(a) - parseFloat(z)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
.map((item, index) => {
|
||||||
let color = getColorFromValue(obj[item])
|
let color = getColorFromValue(obj[item])
|
||||||
const replaceDot: boolean = item.indexOf('.') !== -1 && separator && separator.endsWith('.')
|
const replaceDot: boolean = item.indexOf('.') !== -1 && separator && separator.endsWith('.')
|
||||||
const insertClosingBrace: boolean =
|
const insertClosingBrace: boolean =
|
||||||
|
@ -588,8 +628,10 @@ function provideCssHelperCompletions(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: item,
|
label: item,
|
||||||
filterText: `${replaceDot ? '.' : ''}${item}`,
|
|
||||||
sortText: naturalExpand(index),
|
sortText: naturalExpand(index),
|
||||||
|
commitCharacters: [!item.includes('.') && '.', !item.includes('[') && '['].filter(
|
||||||
|
Boolean
|
||||||
|
),
|
||||||
kind: color ? 16 : isObject(obj[item]) ? 9 : 10,
|
kind: color ? 16 : isObject(obj[item]) ? 9 : 10,
|
||||||
// VS Code bug causes some values to not display in some cases
|
// VS Code bug causes some values to not display in some cases
|
||||||
detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail,
|
detail: detail === '0' || detail === 'transparent' ? `${detail} ` : detail,
|
||||||
|
@ -598,16 +640,23 @@ function provideCssHelperCompletions(
|
||||||
? culori.formatRgb(color)
|
? culori.formatRgb(color)
|
||||||
: null,
|
: null,
|
||||||
textEdit: {
|
textEdit: {
|
||||||
newText: `${replaceDot ? '[' : ''}${item}${insertClosingBrace ? ']' : ''}`,
|
newText: `${item}${insertClosingBrace ? ']' : ''}`,
|
||||||
|
range: editRange,
|
||||||
|
},
|
||||||
|
additionalTextEdits: replaceDot
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
newText: '[',
|
||||||
range: {
|
range: {
|
||||||
start: {
|
start: {
|
||||||
line: position.line,
|
...editRange.start,
|
||||||
character:
|
character: editRange.start.character - 1,
|
||||||
position.character - keys[keys.length - 1].length - (replaceDot ? 1 : 0) - offset,
|
|
||||||
},
|
},
|
||||||
end: position,
|
end: editRange.start,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
data: 'helper',
|
data: 'helper',
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
import { State, Settings } from '../util/state'
|
import { State, Settings } from '../util/state'
|
||||||
import type { TextDocument, Range, DiagnosticSeverity } from 'vscode-languageserver'
|
import type { TextDocument } from 'vscode-languageserver'
|
||||||
import { InvalidConfigPathDiagnostic, DiagnosticKind } from './types'
|
import { InvalidConfigPathDiagnostic, DiagnosticKind } from './types'
|
||||||
import { isCssDoc } from '../util/css'
|
import { findHelperFunctionsInDocument } from '../util/find'
|
||||||
import { getLanguageBoundaries } from '../util/getLanguageBoundaries'
|
|
||||||
import { findAll, indexToPosition } from '../util/find'
|
|
||||||
import { stringToPath } from '../util/stringToPath'
|
import { stringToPath } from '../util/stringToPath'
|
||||||
import isObject from '../util/isObject'
|
import isObject from '../util/isObject'
|
||||||
import { closest } from '../util/closest'
|
import { closest } from '../util/closest'
|
||||||
import { absoluteRange } from '../util/absoluteRange'
|
|
||||||
import { combinations } from '../util/combinations'
|
import { combinations } from '../util/combinations'
|
||||||
import dlv from 'dlv'
|
import dlv from 'dlv'
|
||||||
import { getTextWithoutComments } from '../util/doc'
|
|
||||||
|
|
||||||
function pathToString(path: string | string[]): string {
|
function pathToString(path: string | string[]): string {
|
||||||
if (typeof path === 'string') return path
|
if (typeof path === 'string') return path
|
||||||
|
@ -167,47 +163,18 @@ export function getInvalidConfigPathDiagnostics(
|
||||||
if (severity === 'ignore') return []
|
if (severity === 'ignore') return []
|
||||||
|
|
||||||
let diagnostics: InvalidConfigPathDiagnostic[] = []
|
let diagnostics: InvalidConfigPathDiagnostic[] = []
|
||||||
let ranges: Range[] = []
|
|
||||||
|
|
||||||
if (isCssDoc(state, document)) {
|
findHelperFunctionsInDocument(state, document).forEach((helperFn) => {
|
||||||
ranges.push(undefined)
|
let base = helperFn.helper === 'theme' ? ['theme'] : []
|
||||||
} else {
|
let result = validateConfigPath(state, helperFn.path, base)
|
||||||
let boundaries = getLanguageBoundaries(state, document)
|
|
||||||
if (!boundaries) return []
|
|
||||||
ranges.push(...boundaries.filter((b) => b.type === 'css').map(({ range }) => range))
|
|
||||||
}
|
|
||||||
|
|
||||||
ranges.forEach((range) => {
|
|
||||||
let text = getTextWithoutComments(document, 'css', range)
|
|
||||||
let matches = findAll(
|
|
||||||
/(?<prefix>\s|^)(?<helper>config|theme)\((?<quote>['"])(?<key>[^)]+)\k<quote>[^)]*\)/g,
|
|
||||||
text
|
|
||||||
)
|
|
||||||
|
|
||||||
matches.forEach((match) => {
|
|
||||||
let base = match.groups.helper === 'theme' ? ['theme'] : []
|
|
||||||
let result = validateConfigPath(state, match.groups.key, base)
|
|
||||||
|
|
||||||
if (result.isValid === true) {
|
if (result.isValid === true) {
|
||||||
return null
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let startIndex =
|
|
||||||
match.index +
|
|
||||||
match.groups.prefix.length +
|
|
||||||
match.groups.helper.length +
|
|
||||||
1 + // open paren
|
|
||||||
match.groups.quote.length
|
|
||||||
|
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
code: DiagnosticKind.InvalidConfigPath,
|
code: DiagnosticKind.InvalidConfigPath,
|
||||||
range: absoluteRange(
|
range: helperFn.ranges.path,
|
||||||
{
|
|
||||||
start: indexToPosition(text, startIndex),
|
|
||||||
end: indexToPosition(text, startIndex + match.groups.key.length),
|
|
||||||
},
|
|
||||||
range
|
|
||||||
),
|
|
||||||
severity:
|
severity:
|
||||||
severity === 'error'
|
severity === 'error'
|
||||||
? 1 /* DiagnosticSeverity.Error */
|
? 1 /* DiagnosticSeverity.Error */
|
||||||
|
@ -216,7 +183,6 @@ export function getInvalidConfigPathDiagnostics(
|
||||||
suggestions: result.suggestions,
|
suggestions: result.suggestions,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
return diagnostics
|
return diagnostics
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,12 +36,12 @@ export async function getDocumentColors(
|
||||||
|
|
||||||
let helperFns = findHelperFunctionsInDocument(state, document)
|
let helperFns = findHelperFunctionsInDocument(state, document)
|
||||||
helperFns.forEach((fn) => {
|
helperFns.forEach((fn) => {
|
||||||
let keys = stringToPath(fn.value)
|
let keys = stringToPath(fn.path)
|
||||||
let base = fn.helper === 'theme' ? ['theme'] : []
|
let base = fn.helper === 'theme' ? ['theme'] : []
|
||||||
let value = dlv(state.config, [...base, ...keys])
|
let value = dlv(state.config, [...base, ...keys])
|
||||||
let color = getColorFromValue(value)
|
let color = getColorFromValue(value)
|
||||||
if (color && typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
|
if (color && typeof color !== 'string' && (color.alpha ?? 1) !== 0) {
|
||||||
colors.push({ range: fn.valueRange, color: culoriColorToVscodeColor(color) })
|
colors.push({ range: fn.ranges.path, color: culoriColorToVscodeColor(color) })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,12 @@ import type { Hover, TextDocument, Position } from 'vscode-languageserver'
|
||||||
import { stringifyCss, stringifyConfigValue } from './util/stringify'
|
import { stringifyCss, stringifyConfigValue } from './util/stringify'
|
||||||
import dlv from 'dlv'
|
import dlv from 'dlv'
|
||||||
import { isCssContext } from './util/css'
|
import { isCssContext } from './util/css'
|
||||||
import { findClassNameAtPosition } from './util/find'
|
import { findClassNameAtPosition, findHelperFunctionsInRange } from './util/find'
|
||||||
import { validateApply } from './util/validateApply'
|
import { validateApply } from './util/validateApply'
|
||||||
import { getClassNameParts } from './util/getClassNameAtPosition'
|
import { getClassNameParts } from './util/getClassNameAtPosition'
|
||||||
import * as jit from './util/jit'
|
import * as jit from './util/jit'
|
||||||
import { validateConfigPath } from './diagnostics/getInvalidConfigPathDiagnostics'
|
import { validateConfigPath } from './diagnostics/getInvalidConfigPathDiagnostics'
|
||||||
import { getTextWithoutComments } from './util/doc'
|
import { isWithinRange } from './util/isWithinRange'
|
||||||
|
|
||||||
export async function doHover(
|
export async function doHover(
|
||||||
state: State,
|
state: State,
|
||||||
|
@ -22,50 +22,35 @@ export async function doHover(
|
||||||
}
|
}
|
||||||
|
|
||||||
function provideCssHelperHover(state: State, document: TextDocument, position: Position): Hover {
|
function provideCssHelperHover(state: State, document: TextDocument, position: Position): Hover {
|
||||||
if (!isCssContext(state, document, position)) return null
|
if (!isCssContext(state, document, position)) {
|
||||||
|
|
||||||
const line = getTextWithoutComments(document, 'css').split('\n')[position.line]
|
|
||||||
|
|
||||||
const match = line.match(/(?<helper>theme|config)\((?<quote>['"])(?<key>[^)]+)\k<quote>[^)]*\)/)
|
|
||||||
|
|
||||||
if (match === null) return null
|
|
||||||
|
|
||||||
const startChar = match.index + match.groups.helper.length + 2
|
|
||||||
const endChar = startChar + match.groups.key.length
|
|
||||||
|
|
||||||
if (position.character < startChar || position.character >= endChar) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = match.groups.key
|
let helperFns = findHelperFunctionsInRange(document, {
|
||||||
.split(/(\[[^\]]+\]|\.)/)
|
start: { line: position.line, character: 0 },
|
||||||
.filter(Boolean)
|
end: { line: position.line + 1, character: 0 },
|
||||||
.filter((x) => x !== '.')
|
})
|
||||||
.map((x) => x.replace(/^\[([^\]]+)\]$/, '$1'))
|
|
||||||
|
|
||||||
if (key.length === 0) return null
|
for (let helperFn of helperFns) {
|
||||||
|
if (isWithinRange(position, helperFn.ranges.path)) {
|
||||||
if (match.groups.helper === 'theme') {
|
let validated = validateConfigPath(
|
||||||
key = ['theme', ...key]
|
state,
|
||||||
|
helperFn.path,
|
||||||
|
helperFn.helper === 'theme' ? ['theme'] : []
|
||||||
|
)
|
||||||
|
let value = validated.isValid ? stringifyConfigValue(validated.value) : null
|
||||||
|
if (value === null) {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = validateConfigPath(state, key).isValid
|
|
||||||
? stringifyConfigValue(dlv(state.config, key))
|
|
||||||
: null
|
|
||||||
|
|
||||||
if (value === null) return null
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contents: { kind: 'markdown', value: ['```plaintext', value, '```'].join('\n') },
|
contents: { kind: 'markdown', value: ['```plaintext', value, '```'].join('\n') },
|
||||||
range: {
|
range: helperFn.ranges.path,
|
||||||
start: { line: position.line, character: startChar },
|
|
||||||
end: {
|
|
||||||
line: position.line,
|
|
||||||
character: endChar,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
async function provideClassNameHover(
|
async function provideClassNameHover(
|
||||||
state: State,
|
state: State,
|
||||||
|
|
|
@ -359,36 +359,48 @@ export function findHelperFunctionsInRange(
|
||||||
range?: Range
|
range?: Range
|
||||||
): DocumentHelperFunction[] {
|
): DocumentHelperFunction[] {
|
||||||
const text = getTextWithoutComments(doc, 'css', range)
|
const text = getTextWithoutComments(doc, 'css', range)
|
||||||
const matches = findAll(
|
let matches = findAll(
|
||||||
/(?<before>^|\s)(?<helper>theme|config)\((?:(?<single>')([^']+)'|(?<double>")([^"]+)")[^)]*\)/gm,
|
/(?<prefix>\s|^)(?<helper>config|theme)(?<innerPrefix>\(\s*)(?<path>[^)]*?)\s*\)/g,
|
||||||
text
|
text
|
||||||
)
|
)
|
||||||
|
|
||||||
return matches.map((match) => {
|
return matches.map((match) => {
|
||||||
let value = match[4] || match[6]
|
let quotesBefore = ''
|
||||||
let startIndex = match.index + match.groups.before.length
|
let path = match.groups.path.replace(/['"]+$/, '').replace(/^['"]+/, (m) => {
|
||||||
|
quotesBefore = m
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
let matches = path.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]+))$/)
|
||||||
|
if (matches) {
|
||||||
|
path = matches[1]
|
||||||
|
}
|
||||||
|
path = path.replace(/['"]*\s*$/, '')
|
||||||
|
|
||||||
|
let startIndex =
|
||||||
|
match.index +
|
||||||
|
match.groups.prefix.length +
|
||||||
|
match.groups.helper.length +
|
||||||
|
match.groups.innerPrefix.length
|
||||||
|
|
||||||
return {
|
return {
|
||||||
full: match[0].substr(match.groups.before.length),
|
|
||||||
value,
|
|
||||||
helper: match.groups.helper === 'theme' ? 'theme' : 'config',
|
helper: match.groups.helper === 'theme' ? 'theme' : 'config',
|
||||||
quotes: match.groups.single ? "'" : '"',
|
path,
|
||||||
range: resolveRange(
|
ranges: {
|
||||||
|
full: resolveRange(
|
||||||
{
|
{
|
||||||
start: indexToPosition(text, startIndex),
|
start: indexToPosition(text, startIndex),
|
||||||
end: indexToPosition(text, match.index + match[0].length),
|
end: indexToPosition(text, startIndex + match.groups.path.length),
|
||||||
},
|
},
|
||||||
range
|
range
|
||||||
),
|
),
|
||||||
valueRange: resolveRange(
|
path: resolveRange(
|
||||||
{
|
{
|
||||||
start: indexToPosition(text, startIndex + match.groups.helper.length + 1),
|
start: indexToPosition(text, startIndex + quotesBefore.length),
|
||||||
end: indexToPosition(
|
end: indexToPosition(text, startIndex + quotesBefore.length + path.length),
|
||||||
text,
|
|
||||||
startIndex + match.groups.helper.length + 1 + 1 + value.length + 1
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
range
|
range
|
||||||
),
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,12 +124,12 @@ export type DocumentClassName = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocumentHelperFunction = {
|
export type DocumentHelperFunction = {
|
||||||
full: string
|
|
||||||
helper: 'theme' | 'config'
|
helper: 'theme' | 'config'
|
||||||
value: string
|
path: string
|
||||||
quotes: '"' | "'"
|
ranges: {
|
||||||
range: Range
|
full: Range
|
||||||
valueRange: Range
|
path: Range
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClassNameMeta = {
|
export type ClassNameMeta = {
|
||||||
|
|
|
@ -378,7 +378,11 @@ export async function activate(context: ExtensionContext) {
|
||||||
async resolveCompletionItem(item, token, next) {
|
async resolveCompletionItem(item, token, next) {
|
||||||
let result = await next(item, token)
|
let result = await next(item, token)
|
||||||
let selections = Window.activeTextEditor.selections
|
let selections = Window.activeTextEditor.selections
|
||||||
if (selections.length > 1 && result.additionalTextEdits?.length > 0) {
|
if (
|
||||||
|
result['data'] === 'variant' &&
|
||||||
|
selections.length > 1 &&
|
||||||
|
result.additionalTextEdits?.length > 0
|
||||||
|
) {
|
||||||
let length =
|
let length =
|
||||||
selections[0].start.character - result.additionalTextEdits[0].range.start.character
|
selections[0].start.character - result.additionalTextEdits[0].range.start.character
|
||||||
let prefixLength =
|
let prefixLength =
|
||||||
|
|
Loading…
Reference in New Issue