UNPKG

@prisma/language-server

Version:
170 lines • 8.11 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.quickFix = quickFix; const vscode_languageserver_1 = require("vscode-languageserver"); const js_levenshtein_1 = __importDefault(require("js-levenshtein")); const codeActions_1 = __importDefault(require("../prisma-schema-wasm/codeActions")); const constants_1 = require("../constants"); const ast_1 = require("../ast"); /** * Given a name and a list of symbols whose names are *not* equal to the name, return a spelling suggestion if there is one that is close enough. * Names less than length 3 only check for case-insensitive equality, not levenshtein distance. * * If there is a candidate that's the same except for case, return that. * If there is a candidate that's within one edit of the name, return that. * Otherwise, return the candidate with the smallest Levenshtein distance, * except for candidates: * * With no name * * Whose meaning doesn't match the `meaning` parameter. * * Whose length differs from the target name by more than 0.34 of the length of the name. * * Whose levenshtein distance is more than 0.4 of the length of the name * (0.4 allows 1 substitution/transposition for every 5 characters, * and 1 insertion/deletion at 3 characters) */ function getSpellingSuggestions(name, possibleSuggestions) { const maximumLengthDifference = Math.min(2, Math.floor(name.length * 0.34)); let bestDistance = Math.floor(name.length * 0.4) + 1; // If the best result isn't better than this, don't bother. let bestCandidate; let justCheckExactMatches = false; const nameLowerCase = name.toLowerCase(); for (const candidate of possibleSuggestions) { if (!(Math.abs(candidate.length - nameLowerCase.length) <= maximumLengthDifference)) { continue; } const candidateNameLowerCase = candidate.toLowerCase(); if (candidateNameLowerCase === nameLowerCase) { return candidate; } if (justCheckExactMatches) { continue; } if (candidate.length < 3) { // Don't bother, user would have noticed a 2-character name having an extra character continue; } // Only care about a result better than the best so far. const distance = (0, js_levenshtein_1.default)(nameLowerCase, candidateNameLowerCase); if (distance > bestDistance) { continue; } if (distance < 3) { justCheckExactMatches = true; bestCandidate = candidate; } else { bestDistance = distance; bestCandidate = candidate; } } return bestCandidate; } function removeTypeModifiers(hasTypeModifierArray, hasTypeModifierOptional, input) { if (hasTypeModifierArray) return input.replace('[]', ''); if (hasTypeModifierOptional) return input.replace('?', ''); return input; } function addTypeModifiers(hasTypeModifierArray, hasTypeModifierOptional, suggestion) { if (hasTypeModifierArray) return `${suggestion}[]`; if (hasTypeModifierOptional) return `${suggestion}?`; return suggestion; } function quickFix(schema, initiatingDocument, params, onError) { const diagnostics = params.context.diagnostics; if (!diagnostics || diagnostics.length === 0) { return []; } const codeActionList = (0, codeActions_1.default)(JSON.stringify(schema), JSON.stringify(params), (errorMessage) => { if (onError) { onError(errorMessage); } }); // Add code actions from typescript side for (const diag of diagnostics) { if (diag.severity === vscode_languageserver_1.DiagnosticSeverity.Error && diag.message.startsWith('Type') && // In 5.13.0 and earlier we used "custom type" instead of "composite type" // So we only check the beginning of the message for simplicity // See https://github.com/prisma/prisma-engines/pull/4813 diag.message.includes('is neither a built-in type, nor refers to another model,')) { let diagText = initiatingDocument.getText(diag.range); const hasTypeModifierArray = diagText.endsWith('[]'); const hasTypeModifierOptional = diagText.endsWith('?'); diagText = removeTypeModifiers(hasTypeModifierArray, hasTypeModifierOptional, diagText); const spellingSuggestion = getSpellingSuggestions(diagText, (0, ast_1.getAllRelationNames)(schema, constants_1.relationNamesRegexFilter)); if (spellingSuggestion) { codeActionList.push({ title: `Change spelling to '${spellingSuggestion}'`, kind: vscode_languageserver_1.CodeActionKind.QuickFix, diagnostics: [diag], edit: { changes: { [params.textDocument.uri]: [ { range: diag.range, newText: addTypeModifiers(hasTypeModifierArray, hasTypeModifierOptional, spellingSuggestion), }, ], }, }, }); } } else if (diag.severity === vscode_languageserver_1.DiagnosticSeverity.Error && diag.message.includes('`experimentalFeatures`')) { codeActionList.push({ title: "Rename property to 'previewFeatures'", kind: vscode_languageserver_1.CodeActionKind.QuickFix, diagnostics: [diag], edit: { changes: { [params.textDocument.uri]: [ { range: diag.range, newText: 'previewFeatures', }, ], }, }, }); } else if (diag.severity === vscode_languageserver_1.DiagnosticSeverity.Error && diag.message.includes('It does not start with any known Prisma schema keyword.')) { const diagText = initiatingDocument.getText(diag.range).split(/\s/); if (diagText.length !== 0) { const spellingSuggestion = getSpellingSuggestions(diagText[0], ['model', 'enum', 'datasource', 'generator']); if (spellingSuggestion) { codeActionList.push({ title: `Change spelling to '${spellingSuggestion}'`, kind: vscode_languageserver_1.CodeActionKind.QuickFix, diagnostics: [diag], edit: { changes: { [params.textDocument.uri]: [ { range: { start: diag.range.start, end: { line: diag.range.start.line, character: diagText[0].length, }, }, // the red squiggly lines start at the beginning of the blog and end at the end of the line, include e.g. 'mode nameOfBlock {' but // we only want to replace e.g. 'mode' with 'model', not delete the whole line newText: spellingSuggestion, }, ], }, }, }); } } } } return codeActionList; } //# sourceMappingURL=index.js.map