fish-lsp
Version:
LSP implementation for fish/fish-shell
257 lines (256 loc) • 9.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiagnosticCommentsHandler = exports.DIAGNOSTIC_COMMENT_REGEX = void 0;
exports.isDiagnosticComment = isDiagnosticComment;
exports.isValidErrorCode = isValidErrorCode;
exports.parseDiagnosticComment = parseDiagnosticComment;
const node_types_1 = require("../utils/node-types");
const error_codes_1 = require("./error-codes");
const config_1 = require("../config");
exports.DIAGNOSTIC_COMMENT_REGEX = /^#\s*-lsp-(disable|enable)(?:-(next-line))?\s*([0-9\s]*)?$/;
function isDiagnosticComment(node) {
if (!(0, node_types_1.isComment)(node))
return false;
return exports.DIAGNOSTIC_COMMENT_REGEX.test(node.text.trim());
}
function isValidErrorCode(code) {
return Object.values(error_codes_1.ErrorCodes).includes(code);
}
function parseDiagnosticComment(node) {
if (!isDiagnosticComment(node))
return null;
const match = node.text.trim().match(exports.DIAGNOSTIC_COMMENT_REGEX);
if (!match)
return null;
const [, action, nextLine, codesStr] = match;
const codeStrings = codesStr ? codesStr.trim().split(/\s+/) : [];
const parsedCodes = codeStrings
.map(codeStr => parseInt(codeStr, 10))
.filter(code => !isNaN(code));
const validCodes = [];
const invalidCodes = [];
codeStrings.forEach((codeStr, idx) => {
const code = parsedCodes[idx];
if (code && !isNaN(code) && isValidErrorCode(code)) {
validCodes.push(code);
}
else {
invalidCodes.push(codeStr);
}
});
return {
action: action,
target: nextLine ? 'next-line' : 'line',
codes: validCodes.length > 0 ? validCodes : error_codes_1.ErrorCodes.allErrorCodes,
lineNumber: node.startPosition.row,
invalidCodes: invalidCodes.length > 0 ? invalidCodes : undefined,
};
}
class DiagnosticCommentsHandler {
stateStack = [];
controlPoints = [];
lineStateMap = new Map();
invalidCodeWarnings = new Map();
enabledComments = [];
constructor() {
this.pushState(this.initialState);
this.enabledComments = Array.from(this.currentState.enabledCodes);
}
get initialState() {
return {
enabledCodes: new Set(this.globalEnabledCodes()),
comment: {
action: 'enable',
target: 'line',
codes: this.globalEnabledCodes(),
lineNumber: -1,
},
};
}
get currentState() {
return this.stateStack[this.stateStack.length - 1];
}
globalEnabledCodes() {
if (config_1.config.fish_lsp_diagnostic_disable_error_codes.length > 0) {
return error_codes_1.ErrorCodes.allErrorCodes.filter(code => !config_1.config.fish_lsp_diagnostic_disable_error_codes.includes(code)).filter(code => error_codes_1.ErrorCodes.nonDeprecatedErrorCodes.some(e => e.code === code));
}
return error_codes_1.ErrorCodes.allErrorCodes.filter(code => error_codes_1.ErrorCodes.nonDeprecatedErrorCodes.some(e => e.code === code));
}
pushState(state) {
this.stateStack.push(state);
}
popState() {
if (this.stateStack.length > 1) {
this.stateStack.pop();
this.enabledComments = Array.from(this.currentState.enabledCodes);
}
}
handleNode(node) {
this.cleanupNextLineComments(node.startPosition.row);
if (!isDiagnosticComment(node)) {
return;
}
const comment = parseDiagnosticComment(node);
if (!comment)
return;
if (comment.invalidCodes && comment.invalidCodes.length > 0) {
this.invalidCodeWarnings.set(comment.lineNumber, comment.invalidCodes);
}
this.processComment(comment);
}
processComment(comment) {
const newEnabledCodes = new Set(this.currentState.enabledCodes);
if (comment.action === 'disable') {
comment.codes.forEach(code => newEnabledCodes.delete(code));
}
else {
comment.codes.forEach(code => newEnabledCodes.add(code));
}
const newState = {
enabledCodes: newEnabledCodes,
comment,
invalidCodes: comment.invalidCodes,
};
const controlPoint = {
line: comment.lineNumber,
action: comment.action,
codes: comment.codes,
isNextLine: comment.target === 'next-line',
};
this.controlPoints.push(controlPoint);
this.controlPoints.sort((a, b) => a.line - b.line);
if (comment.target === 'next-line') {
this.pushState(newState);
}
else {
if (this.stateStack.length > 1) {
this.popState();
}
this.pushState(newState);
}
this.enabledComments = Array.from(newEnabledCodes);
}
cleanupNextLineComments(currentLine) {
while (this.stateStack.length > 1 &&
this.currentState.comment.target === 'next-line' &&
currentLine > this.currentState.comment.lineNumber + 1) {
this.popState();
}
}
finalizeStateMap(maxLine) {
let currentState = {
enabledCodes: new Set(this.globalEnabledCodes()),
};
const regularPoints = this.controlPoints.filter(p => !p.isNextLine);
const nextLinePoints = new Map();
for (const point of this.controlPoints) {
if (point.isNextLine) {
const targetLine = point.line + 1;
const existing = nextLinePoints.get(targetLine) || [];
existing.push({ ...point, line: targetLine });
nextLinePoints.set(targetLine, existing);
}
}
for (let line = 0; line <= maxLine; line++) {
for (const point of regularPoints) {
if (point.line <= line) {
this.applyControlPointToState(currentState, point);
}
}
const baseState = {
enabledCodes: new Set(currentState.enabledCodes),
};
const nextLineDirs = nextLinePoints.get(line) || [];
for (const directive of nextLineDirs) {
this.applyControlPointToState(currentState, directive);
}
this.lineStateMap.set(line, {
enabledCodes: new Set(currentState.enabledCodes),
});
if (nextLineDirs.length > 0) {
currentState = baseState;
}
}
}
applyControlPointToState(state, point) {
if (point.action === 'disable') {
for (const code of point.codes) {
state.enabledCodes.delete(code);
}
}
else {
for (const code of point.codes) {
state.enabledCodes.add(code);
}
}
}
isCodeEnabledAtNode(code, node) {
const position = { line: node.startPosition.row, character: node.startPosition.column };
return this.isCodeEnabledAtPosition(code, position);
}
isCodeEnabledAtPosition(code, position) {
if (this.lineStateMap.has(position.line)) {
const state = this.lineStateMap.get(position.line);
return state.enabledCodes.has(code);
}
return this.computeStateAtPosition(position).enabledCodes.has(code);
}
computeStateAtPosition(position) {
const state = {
enabledCodes: new Set(this.globalEnabledCodes()),
};
for (const point of this.controlPoints) {
if (point.line > position.line) {
break;
}
if (!point.isNextLine && point.line <= position.line) {
this.applyControlPointToState(state, point);
}
if (point.isNextLine && point.line + 1 === position.line) {
this.applyControlPointToState(state, { ...point, line: position.line });
}
}
return state;
}
isCodeEnabled(code) {
return this.currentState.enabledCodes.has(code);
}
getStackDepth() {
return this.stateStack.length;
}
getCurrentState() {
return this.currentState;
}
getCurrentStateVerbose() {
const currentState = this.getCurrentState();
const disabledCodes = error_codes_1.ErrorCodes.allErrorCodes.filter(e => !currentState.enabledCodes.has(e));
const enabledCodes = Array.from(currentState.enabledCodes)
.map(e => error_codes_1.ErrorCodes.codes[e].code)
.concat(disabledCodes)
.sort((a, b) => a - b)
.map(item => {
if (disabledCodes.includes(item))
return '....';
return item;
})
.join(' | ');
const invalidCodes = Array.from(this.invalidCodeWarnings.entries())
.map(([line, codes]) => `${line}: ${codes.join(' | ')}`);
const lineStates = Array.from(this.lineStateMap.entries())
.map(([line, state]) => `Line ${line}: ${Array.from(state.enabledCodes).length} enabled codes`);
return {
depth: this.getStackDepth(),
enabledCodes: enabledCodes,
invalidCodes: invalidCodes,
currentState: {
action: currentState.comment.action,
target: currentState.comment.target,
codes: currentState.comment?.codes.join(' | '),
lineNumber: currentState.comment.lineNumber,
},
controlPoints: this.controlPoints.length,
lineStates: lineStates.slice(0, 10),
};
}
}
exports.DiagnosticCommentsHandler = DiagnosticCommentsHandler;