mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-04-21 17:17:49 +01:00
213 lines
6.8 KiB
TypeScript
213 lines
6.8 KiB
TypeScript
import { getErrorMessage } from "Common/ErrorHandlingUtils";
|
|
import { monaco } from "Explorer/LazyMonaco";
|
|
|
|
export enum QueryErrorSeverity {
|
|
Error = "Error",
|
|
Warning = "Warning",
|
|
}
|
|
|
|
export class QueryErrorLocation {
|
|
constructor(
|
|
public start: ErrorPosition,
|
|
public end: ErrorPosition,
|
|
) {}
|
|
}
|
|
|
|
export class ErrorPosition {
|
|
constructor(
|
|
public offset: number,
|
|
public lineNumber?: number,
|
|
public column?: number,
|
|
) {}
|
|
}
|
|
|
|
// Maps severities to numbers for sorting.
|
|
const severityMap: Record<QueryErrorSeverity, number> = {
|
|
Error: 1,
|
|
Warning: 0,
|
|
};
|
|
|
|
export function compareSeverity(left: QueryErrorSeverity, right: QueryErrorSeverity): number {
|
|
return severityMap[left] - severityMap[right];
|
|
}
|
|
|
|
export function createMonacoErrorLocationResolver(
|
|
editor: monaco.editor.IStandaloneCodeEditor,
|
|
selection?: monaco.Selection,
|
|
): (location: { start: number; end: number }) => QueryErrorLocation {
|
|
return ({ start, end }) => {
|
|
// Start and end are absolute offsets (character index) in the document.
|
|
// But we need line numbers and columns for the monaco editor.
|
|
// To get those, we use the editor's model to convert the offsets to positions.
|
|
const model = editor.getModel();
|
|
if (!model) {
|
|
return new QueryErrorLocation(new ErrorPosition(start), new ErrorPosition(end));
|
|
}
|
|
|
|
// If the error was found in a selection, adjust the start and end positions to be relative to the document.
|
|
if (selection) {
|
|
// Get the character index of the start of the selection.
|
|
const selectionStartOffset = model.getOffsetAt(selection.getStartPosition());
|
|
|
|
// Adjust the start and end positions to be relative to the document.
|
|
start = selectionStartOffset + start;
|
|
end = selectionStartOffset + end;
|
|
|
|
// Now, when we resolve the positions, they will be relative to the document and appear in the correct location.
|
|
}
|
|
|
|
const startPos = model.getPositionAt(start);
|
|
const endPos = model.getPositionAt(end);
|
|
return new QueryErrorLocation(
|
|
new ErrorPosition(start, startPos.lineNumber, startPos.column),
|
|
new ErrorPosition(end, endPos.lineNumber, endPos.column),
|
|
);
|
|
};
|
|
}
|
|
|
|
export const createMonacoMarkersForQueryErrors = (errors: QueryError[]) => {
|
|
if (!errors) {
|
|
return [];
|
|
}
|
|
|
|
return errors
|
|
.map((error): monaco.editor.IMarkerData => {
|
|
// Validate that we have what we need to make a marker
|
|
if (
|
|
error.location === undefined ||
|
|
error.location.start === undefined ||
|
|
error.location.end === undefined ||
|
|
error.location.start.lineNumber === undefined ||
|
|
error.location.end.lineNumber === undefined ||
|
|
error.location.start.column === undefined ||
|
|
error.location.end.column === undefined
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
message: error.message,
|
|
severity: error.getMonacoSeverity(),
|
|
startLineNumber: error.location.start.lineNumber,
|
|
startColumn: error.location.start.column,
|
|
endLineNumber: error.location.end.lineNumber,
|
|
endColumn: error.location.end.column,
|
|
};
|
|
})
|
|
.filter((marker) => !!marker);
|
|
};
|
|
|
|
export default class QueryError {
|
|
constructor(
|
|
public message: string,
|
|
public severity: QueryErrorSeverity,
|
|
public code?: string,
|
|
public location?: QueryErrorLocation,
|
|
) {}
|
|
|
|
getMonacoSeverity(): monaco.MarkerSeverity {
|
|
// It's very difficult to use the monaco.MarkerSeverity enum from here, so we'll just use the numbers directly.
|
|
// See: https://microsoft.github.io/monaco-editor/typedoc/enums/MarkerSeverity.html
|
|
switch (this.severity) {
|
|
case QueryErrorSeverity.Error:
|
|
return 8;
|
|
case QueryErrorSeverity.Warning:
|
|
return 4;
|
|
default:
|
|
return 2; // Info
|
|
}
|
|
}
|
|
|
|
/** Attempts to parse a query error from a string or object.
|
|
*
|
|
* @param error The error to parse.
|
|
* @returns An array of query errors if the error could be parsed, or null otherwise.
|
|
*/
|
|
static tryParse(
|
|
error: unknown,
|
|
locationResolver?: (location: { start: number; end: number }) => QueryErrorLocation,
|
|
): QueryError[] {
|
|
locationResolver =
|
|
locationResolver ||
|
|
(({ start, end }) => new QueryErrorLocation(new ErrorPosition(start), new ErrorPosition(end)));
|
|
const errors = QueryError.tryParseObject(error, locationResolver);
|
|
if (errors !== null) {
|
|
return errors;
|
|
}
|
|
|
|
const errorMessage = getErrorMessage(error as string | Error);
|
|
|
|
// Map some well known messages to richer errors
|
|
const knownError = knownErrors[errorMessage];
|
|
if (knownError) {
|
|
return [knownError];
|
|
} else {
|
|
return [new QueryError(errorMessage, QueryErrorSeverity.Error)];
|
|
}
|
|
}
|
|
|
|
static read(
|
|
error: unknown,
|
|
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
|
|
): QueryError | null {
|
|
if (typeof error !== "object" || error === null) {
|
|
return null;
|
|
}
|
|
|
|
const message = "message" in error && typeof error.message === "string" ? error.message : undefined;
|
|
if (!message) {
|
|
return null; // Invalid error (no message).
|
|
}
|
|
|
|
const severity =
|
|
"severity" in error && typeof error.severity === "string" ? (error.severity as QueryErrorSeverity) : undefined;
|
|
const location =
|
|
"location" in error && typeof error.location === "object"
|
|
? locationResolver(error.location as { start: number; end: number })
|
|
: undefined;
|
|
const code = "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
return new QueryError(message, severity, code, location);
|
|
}
|
|
|
|
private static tryParseObject(
|
|
error: unknown,
|
|
locationResolver: (location: { start: number; end: number }) => QueryErrorLocation,
|
|
): QueryError[] | null {
|
|
if (typeof error === "object" && "message" in error) {
|
|
error = error.message;
|
|
}
|
|
|
|
if (typeof error !== "string") {
|
|
return null;
|
|
}
|
|
|
|
// Assign to a new variable because of a TypeScript flow typing quirk, see below.
|
|
let message = error;
|
|
if (message.startsWith("Message: ")) {
|
|
// Reassigning this to 'error' restores the original type of 'error', which is 'unknown'.
|
|
// So we use a separate variable to avoid this.
|
|
message = message.substring("Message: ".length);
|
|
}
|
|
|
|
const lines = message.split("\n");
|
|
message = lines[0].trim();
|
|
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(message);
|
|
} catch (e) {
|
|
// Not a query error.
|
|
return null;
|
|
}
|
|
|
|
if (typeof parsed === "object" && "errors" in parsed && Array.isArray(parsed.errors)) {
|
|
return parsed.errors.map((e) => QueryError.read(e, locationResolver)).filter((e) => e !== null);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const knownErrors: Record<string, QueryError> = {
|
|
"User aborted query.": new QueryError("User aborted query.", QueryErrorSeverity.Warning),
|
|
};
|