diff --git a/src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts b/src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts index ccf52fcb1aa3b88bd1b73aa9963e0e7729b29cca..95293341d536810ead06332a6125e96c6a77a776 100644 --- a/src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts +++ b/src/lsp/providers/diagnostics/getInvalidConfigPathDiagnostics.ts @@ -8,164 +8,8 @@ import { stringToPath } from '../../util/stringToPath' import isObject from '../../../util/isObject' import { closest } from '../../util/closest' import { absoluteRange } from '../../util/absoluteRange' -import { combinations } from '../../util/combinations' const dlv = require('dlv') -function pathToString(path: string | string[]): string { - if (typeof path === 'string') return path - return path.reduce((acc, cur, i) => { - if (i === 0) return cur - if (cur.includes('.')) return `${acc}[${cur}]` - return `${acc}.${cur}` - }, '') -} - -function validateConfigPath( - state: State, - path: string | string[], - base: string[] = [] -): - | { isValid: true; value: any } - | { isValid: false; reason: string; suggestions: string[] } { - let keys = Array.isArray(path) ? path : stringToPath(path) - let value = dlv(state.config, [...base, ...keys]) - let suggestions: string[] = [] - - function findAlternativePath(): string[] { - let points = combinations('123456789'.substr(0, keys.length - 1)).map((x) => - x.split('').map((x) => parseInt(x, 10)) - ) - - let possibilities: string[][] = points - .map((p) => { - let result = [] - let i = 0 - p.forEach((x) => { - result.push(keys.slice(i, x).join('.')) - i = x - }) - result.push(keys.slice(i).join('.')) - return result - }) - .slice(1) // skip original path - - return possibilities.find( - (possibility) => validateConfigPath(state, possibility, base).isValid - ) - } - - if (typeof value === 'undefined') { - let reason = `'${pathToString(path)}' does not exist in your theme config.` - let parentPath = [...base, ...keys.slice(0, keys.length - 1)] - let parentValue = dlv(state.config, parentPath) - - if (isObject(parentValue)) { - let closestValidKey = closest( - keys[keys.length - 1], - Object.keys(parentValue).filter( - (key) => validateConfigPath(state, [...parentPath, key]).isValid - ) - ) - if (closestValidKey) { - suggestions.push( - pathToString([...keys.slice(0, keys.length - 1), closestValidKey]) - ) - reason += ` Did you mean '${suggestions[0]}'?` - } - } else { - let altPath = findAlternativePath() - if (altPath) { - return { - isValid: false, - reason: `${reason} Did you mean '${pathToString(altPath)}'?`, - suggestions: [pathToString(altPath)], - } - } - } - - return { - isValid: false, - reason, - suggestions, - } - } - - if ( - !( - typeof value === 'string' || - typeof value === 'number' || - value instanceof String || - value instanceof Number || - Array.isArray(value) - ) - ) { - let reason = `'${pathToString( - path - )}' was found but does not resolve to a string.` - - if (isObject(value)) { - let validKeys = Object.keys(value).filter( - (key) => validateConfigPath(state, [...keys, key], base).isValid - ) - if (validKeys.length) { - suggestions.push( - ...validKeys.map((validKey) => pathToString([...keys, validKey])) - ) - reason += ` Did you mean something like '${suggestions[0]}'?` - } - } - return { - isValid: false, - reason, - suggestions, - } - } - - // The value resolves successfully, but we need to check that there - // wasn't any funny business. If you have a theme object: - // { msg: 'hello' } and do theme('msg.0') - // this will resolve to 'h', which is probably not intentional, so we - // check that all of the keys are object or array keys (i.e. not string - // indexes) - let isValid = true - for (let i = keys.length - 1; i >= 0; i--) { - let key = keys[i] - let parentValue = dlv(state.config, [...base, ...keys.slice(0, i)]) - if (/^[0-9]+$/.test(key)) { - if (!isObject(parentValue) && !Array.isArray(parentValue)) { - isValid = false - break - } - } else if (!isObject(parentValue)) { - isValid = false - break - } - } - if (!isValid) { - let reason = `'${pathToString(path)}' does not exist in your theme config.` - - let altPath = findAlternativePath() - if (altPath) { - return { - isValid: false, - reason: `${reason} Did you mean '${pathToString(altPath)}'?`, - suggestions: [pathToString(altPath)], - } - } - - return { - isValid: false, - reason, - suggestions: [], - } - } - - return { - isValid: true, - value, - } -} - export function getInvalidConfigPathDiagnostics( state: State, document: TextDocument, @@ -194,9 +38,85 @@ ) matches.forEach((match) => { let base = match.groups.helper === 'theme' ? ['theme'] : [] - let result = validateConfigPath(state, match.groups.key, base) + let keys = stringToPath(match.groups.key) + let value = dlv(state.config, [...base, ...keys]) - if (result.isValid === true) { + const isValid = (val: unknown): boolean => + typeof val === 'string' || + typeof val === 'number' || + val instanceof String || + val instanceof Number || + Array.isArray(val) + + const stitch = (keys: string[]): string => + keys.reduce((acc, cur, i) => { + if (i === 0) return cur + if (cur.includes('.')) return `${acc}[${cur}]` + return `${acc}.${cur}` + }, '') + + let message: string + let suggestions: string[] = [] + + if (isValid(value)) { + // The value resolves successfully, but we need to check that there + // wasn't any funny business. If you have a theme object: + // { msg: 'hello' } and do theme('msg.0') + // this will resolve to 'h', which is probably not intentional, so we + // check that all of the keys are object or array keys (i.e. not string + // indexes) + let valid = true + for (let i = keys.length - 1; i >= 0; i--) { + let key = keys[i] + let parentValue = dlv(state.config, [...base, ...keys.slice(0, i)]) + if (/^[0-9]+$/.test(key)) { + if (!isObject(parentValue) && !Array.isArray(parentValue)) { + valid = false + break + } + } else if (!isObject(parentValue)) { + valid = false + break + } + } + if (!valid) { + message = `'${match.groups.key}' does not exist in your theme config.` + } + } else if (typeof value === 'undefined') { + message = `'${match.groups.key}' does not exist in your theme config.` + let parentValue = dlv(state.config, [ + ...base, + ...keys.slice(0, keys.length - 1), + ]) + if (isObject(parentValue)) { + let closestValidKey = closest( + keys[keys.length - 1], + Object.keys(parentValue).filter((key) => isValid(parentValue[key])) + ) + if (closestValidKey) { + suggestions.push( + stitch([...keys.slice(0, keys.length - 1), closestValidKey]) + ) + message += ` Did you mean '${suggestions[0]}'?` + } + } + } else { + message = `'${match.groups.key}' was found but does not resolve to a string.` + + if (isObject(value)) { + let validKeys = Object.keys(value).filter((key) => + isValid(value[key]) + ) + if (validKeys.length) { + suggestions.push( + ...validKeys.map((validKey) => stitch([...keys, validKey])) + ) + message += ` Did you mean something like '${suggestions[0]}'?` + } + } + } + + if (!message) { return null } @@ -220,8 +140,8 @@ severity: severity === 'error' ? DiagnosticSeverity.Error : DiagnosticSeverity.Warning, - message: result.reason, - suggestions: result.suggestions, + message, + suggestions, }) }) }) diff --git a/src/lsp/util/combinations.ts b/src/lsp/util/combinations.ts deleted file mode 100644 index 2c9868b207b01b60cc0789b800058285257e187a..0000000000000000000000000000000000000000 --- a/src/lsp/util/combinations.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function combinations(str: string): string[] { - let fn = function (active: string, rest: string, a: string[]) { - if (!active && !rest) return - if (!rest) { - a.push(active) - } else { - fn(active + rest[0], rest.slice(1), a) - fn(active, rest.slice(1), a) - } - return a - } - return fn('', str, []) -} diff --git a/src/lsp/util/getClassNameAtPosition.ts b/src/lsp/util/getClassNameAtPosition.ts index 7418b2f157723967380453978a082e42fdb9ce49..083832ca5e8b2707a1405fdbcd83f6c163e18f65 100644 --- a/src/lsp/util/getClassNameAtPosition.ts +++ b/src/lsp/util/getClassNameAtPosition.ts @@ -1,5 +1,4 @@ import { State } from './state' -import { combinations } from './combinations' const dlv = require('dlv') export function getClassNameParts(state: State, className: string): string[] { @@ -42,3 +41,17 @@ } return false }) } + +function combinations(str: string): string[] { + let fn = function (active: string, rest: string, a: string[]) { + if (!active && !rest) return + if (!rest) { + a.push(active) + } else { + fn(active + rest[0], rest.slice(1), a) + fn(active, rest.slice(1), a) + } + return a + } + return fn('', str, []) +}