@prisma/language-server
Version:
Prisma Language Server
170 lines • 8.11 kB
JavaScript
"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