Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/platform/packages/shared/kbn-monaco/src/console/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
import { foldingRangeProvider } from './folding_range_provider';
import { ESQL_AUTOCOMPLETE_TRIGGER_CHARS } from '../esql';
import { wrapAsMonacoSuggestions } from '../esql/lib/converters/suggestions';
import { checkForTripleQuotesAndQueries, unescapeInvalidChars } from './utils';
import { checkForTripleQuotesAndEsqlQuery, unescapeInvalidChars } from './utils';

export { CONSOLE_LANG_ID, CONSOLE_OUTPUT_LANG_ID } from './constants';
/**
Expand Down Expand Up @@ -56,10 +56,10 @@ export const ConsoleLang: LangModuleType = {
const fullText = model.getValue();
const cursorOffset = model.getOffsetAt(position);
const textBeforeCursor = fullText.slice(0, cursorOffset);
const { insideSingleQuotesQuery, insideTripleQuotesQuery, queryIndex } =
checkForTripleQuotesAndQueries(textBeforeCursor);
if (esqlCallbacks && (insideSingleQuotesQuery || insideTripleQuotesQuery)) {
const queryText = textBeforeCursor.slice(queryIndex, cursorOffset);
const { insideTripleQuotes, insideEsqlQuery, esqlQueryIndex } =
checkForTripleQuotesAndEsqlQuery(textBeforeCursor);
if (esqlCallbacks && insideEsqlQuery) {
const queryText = textBeforeCursor.slice(esqlQueryIndex, cursorOffset);
const unescapedQuery = unescapeInvalidChars(queryText);
const esqlSuggestions = await suggest(
unescapedQuery,
Expand All @@ -72,7 +72,7 @@ export const ConsoleLang: LangModuleType = {
esqlSuggestions,
queryText,
false,
insideSingleQuotesQuery
!insideTripleQuotes
),
};
} else if (actionsProvider.current) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,97 +7,150 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { checkForTripleQuotesAndQueries, unescapeInvalidChars } from './autocomplete_utils';
import { checkForTripleQuotesAndEsqlQuery, unescapeInvalidChars } from './autocomplete_utils';

describe('autocomplete_utils', () => {
describe('checkForTripleQuotesAndQueries', () => {
it('should return false for all flags for an empty string', () => {
expect(checkForTripleQuotesAndQueries('')).toEqual({
it('returns false for all flags for an empty string', () => {
expect(checkForTripleQuotesAndEsqlQuery('')).toEqual({
insideTripleQuotes: false,
insideSingleQuotesQuery: false,
insideTripleQuotesQuery: false,
queryIndex: -1,
insideEsqlQuery: false,
esqlQueryIndex: -1,
});
});

it('should return false for all flags for a request without triple quotes', () => {
it('returns false for all flags for a request without triple quotes or ESQL query', () => {
const request = `POST _search\n{\n "query": {\n "match": {\n "message": "hello world"\n }\n }\n}`;
expect(checkForTripleQuotesAndQueries(request)).toEqual({
expect(checkForTripleQuotesAndEsqlQuery(request)).toEqual({
insideTripleQuotes: false,
insideSingleQuotesQuery: false,
insideTripleQuotesQuery: false,
queryIndex: -1,
insideEsqlQuery: false,
esqlQueryIndex: -1,
});
});

it('should return true for insideTripleQuotes and false for insideTripleQuotesQuery when triple quotes are outside a query', () => {
it('returns true for insideTripleQuotes and false for ESQL flags when triple quotes are outside a query', () => {
const request = `POST _ingest/pipeline/_simulate\n{\n "pipeline": {\n "processors": [\n {\n "script": {\n "source":\n """\n for (field in params['fields']){\n if (!$(field, '').isEmpty()){\n`;
expect(checkForTripleQuotesAndQueries(request)).toEqual({
expect(checkForTripleQuotesAndEsqlQuery(request)).toEqual({
insideTripleQuotes: true,
insideSingleQuotesQuery: false,
insideTripleQuotesQuery: false,
queryIndex: -1,
insideEsqlQuery: false,
esqlQueryIndex: -1,
});
});

it('should return false for all flags when triple-quoted string is properly closed', () => {
const request = `POST _ingest/pipeline/_simulate\n{\n "pipeline": {\n "processors": [\n {\n "script": {\n "source":\n """\n return 'hello';\n """\n }\n }\n ]\n }\n}`;
expect(checkForTripleQuotesAndQueries(request)).toEqual({
insideTripleQuotes: false,
insideSingleQuotesQuery: false,
insideTripleQuotesQuery: false,
queryIndex: -1,
});
});

it('should return true for both insideTripleQuotes and insideTripleQuotesQuery if inside a "query" field', () => {
it('returns true for insideTripleQuotes but false for ESQL flags inside a non-_query request query field', () => {
const request = `POST _search\n{\n "query": """FROM test `;
expect(checkForTripleQuotesAndQueries(request)).toEqual({
expect(checkForTripleQuotesAndEsqlQuery(request)).toEqual({
insideTripleQuotes: true,
insideSingleQuotesQuery: false,
insideTripleQuotesQuery: true,
queryIndex: request.indexOf('"""') + 3,
insideEsqlQuery: false,
esqlQueryIndex: -1,
});
});

it('should return true for insideSingleQuotesQuery if inside a single-quoted "query" string', () => {
it('returns false for ESQL flags inside a single-quoted query for non-_query request', () => {
const request = `GET index/_search\n{\n "query": "SELECT * FROM logs `;
expect(checkForTripleQuotesAndQueries(request)).toEqual({
const result = checkForTripleQuotesAndEsqlQuery(request);
expect(result).toEqual({
insideTripleQuotes: false,
insideSingleQuotesQuery: true,
insideTripleQuotesQuery: false,
queryIndex: 32,
insideEsqlQuery: false,
esqlQueryIndex: -1,
});
});

it('should return false for all flags if single quote is closed', () => {
const request = `GET index/_search\n{\n "query": "SELECT * FROM logs" }`;
expect(checkForTripleQuotesAndQueries(request)).toEqual({
it('returns false for all flags if single quote is closed', () => {
const request = `POST _query\n{\n "query": "SELECT * FROM logs" }`;
expect(checkForTripleQuotesAndEsqlQuery(request)).toEqual({
insideTripleQuotes: false,
insideSingleQuotesQuery: false,
insideTripleQuotesQuery: false,
queryIndex: -1,
insideEsqlQuery: false,
esqlQueryIndex: -1,
});
});

it('should handle escaped quotes correctly (not toggling inside state)', () => {
const request = `GET _search\n{\n "query": "FROM test | WHERE KQL(\\"\\"\\")`;
expect(checkForTripleQuotesAndQueries(request)).toEqual({
it('returns false for all flags if triple quote is closed', () => {
const request = `POST _query\n{\n "query": """SELECT * FROM logs""" }`;
expect(checkForTripleQuotesAndEsqlQuery(request)).toEqual({
insideTripleQuotes: false,
insideSingleQuotesQuery: true,
insideTripleQuotesQuery: false,
queryIndex: 26,
insideEsqlQuery: false,
esqlQueryIndex: -1,
});
});
});

it('should reset the state after closing triple quotes', () => {
const request = `GET _search\n{\n "query": """SELECT * FROM logs"""\n}`;
expect(checkForTripleQuotesAndQueries(request)).toEqual({
insideTripleQuotes: false,
insideSingleQuotesQuery: false,
insideTripleQuotesQuery: false,
queryIndex: -1,
});
it('sets insideEsqlQuery for single quoted query after POST _query', () => {
const request = `POST _query\n{\n "query": "FROM test `;
expect(checkForTripleQuotesAndEsqlQuery(request)).toEqual({
insideTripleQuotes: false,
insideEsqlQuery: true,
esqlQueryIndex: request.indexOf('"FROM test ') + 1,
});
});

it('sets insideEsqlQuery for triple quoted query after POST _query (case-insensitive)', () => {
const request = `post _query\n{\n "query": """FROM test `; // lowercase POST should also match
const result = checkForTripleQuotesAndEsqlQuery(request);
expect(result).toEqual({
insideTripleQuotes: true,
insideEsqlQuery: true,
esqlQueryIndex: request.indexOf('"""') + 3,
});
});

it('detects single quoted query after POST _query?pretty suffix', () => {
const request = `POST _query?pretty\n{\n "query": "FROM logs | STATS `;
const result = checkForTripleQuotesAndEsqlQuery(request);
expect(result).toEqual({
insideTripleQuotes: false,
insideEsqlQuery: true,
esqlQueryIndex: request.indexOf('"FROM logs ') + 1,
});
});

it('detects query with /_query endpoint', () => {
const request = `POST /_query\n{\n "query": "FROM logs | STATS `;
const result = checkForTripleQuotesAndEsqlQuery(request);
expect(result).toEqual({
insideTripleQuotes: false,
insideEsqlQuery: true,
esqlQueryIndex: request.indexOf('"FROM logs ') + 1,
});
});

it('detects triple quoted query after POST _query?foo=bar with extra spaces', () => {
const request = `POST _query?foo=bar\n{\n "query": """FROM metrics `;
const result = checkForTripleQuotesAndEsqlQuery(request);
expect(result).toEqual({
insideTripleQuotes: true,
insideEsqlQuery: true,
esqlQueryIndex: request.indexOf('"""') + 3,
});
});

it('does not set ESQL flags for subsequent non-_query request in same buffer', () => {
const request = `POST _query\n{\n "query": "FROM a | STATS "\n}\nGET other_index/_search\n{\n "query": "match_all" }`;
const result = checkForTripleQuotesAndEsqlQuery(request);
expect(result).toEqual({
insideTripleQuotes: false,
insideEsqlQuery: false,
esqlQueryIndex: -1, // single quotes closed in second request
});
});

it('only flags current active _query section in mixed multi-request buffer', () => {
const partial = `POST _query\n{\n "query": "FROM a | STATS "\n}\nPOST _query\n{\n "query": """FROM b | WHERE foo = `; // cursor inside triple quotes of second request
const result = checkForTripleQuotesAndEsqlQuery(partial);
expect(result).toEqual({
insideTripleQuotes: true,
insideEsqlQuery: true,
esqlQueryIndex: partial.lastIndexOf('"""') + 3,
});
});

it('handles request method at end of buffer without trailing newline (regression test)', () => {
const buffer = 'POST _query';
const result = checkForTripleQuotesAndEsqlQuery(buffer);
expect(result).toEqual({
insideTripleQuotes: false,
insideEsqlQuery: false,
esqlQueryIndex: -1,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,31 @@
* and the start index of the current query.
* @param text The text up to the current position
*/
export const checkForTripleQuotesAndQueries = (
export const checkForTripleQuotesAndEsqlQuery = (
text: string
): {
insideTripleQuotes: boolean;
insideSingleQuotesQuery: boolean;
insideTripleQuotesQuery: boolean;
queryIndex: number;
insideEsqlQuery: boolean;
esqlQueryIndex: number;
} => {
let insideSingleQuotes = false;
let insideTripleQuotes = false;

let insideSingleQuotesQuery = false;
let insideTripleQuotesQuery = false;

let insideEsqlQueryRequest = false;

let currentQueryStartIndex = -1;
let i = 0;

while (i < text.length) {
const textBefore = text.slice(0, i);
const textFromIndex = text.slice(i);
if (text.startsWith('"""', i)) {
insideTripleQuotes = !insideTripleQuotes;
if (insideTripleQuotes) {
insideTripleQuotesQuery = /.*"query"\s*:\s*$/.test(text.slice(0, i));
insideTripleQuotesQuery = /.*"query"\s*:\s*$/.test(textBefore);
if (insideTripleQuotesQuery) {
currentQueryStartIndex = i + 3;
}
Expand All @@ -46,7 +49,7 @@ export const checkForTripleQuotesAndQueries = (
} else if (text.at(i) === '"' && text.at(i - 1) !== '\\') {
insideSingleQuotes = !insideSingleQuotes;
if (insideSingleQuotes) {
insideSingleQuotesQuery = /.*"query"\s*:\s*$/.test(text.slice(0, i));
insideSingleQuotesQuery = /.*"query"\s*:\s*$/.test(textBefore);
if (insideSingleQuotesQuery) {
currentQueryStartIndex = i + 1;
}
Expand All @@ -55,16 +58,26 @@ export const checkForTripleQuotesAndQueries = (
currentQueryStartIndex = -1;
}
i++;
} else if (/^(GET|POST|PUT|DELETE|HEAD|PATCH)/i.test(textFromIndex)) {
// If this is the start of a new request, check if it is a _query API request
insideEsqlQueryRequest = /^(P|p)(O|o)(S|s)(T|t)\s+\/?_query(\n|\s|\?)/.test(textFromIndex);
// Move the index past the current line that contains request method and endpoint.
const newlineIndex = text.indexOf('\n', i);
if (newlineIndex === -1) {
// No newline after the request line; advance to end to avoid infinite loop.
i = text.length;
} else {
i = newlineIndex + 1; // Position at start of next line
}
} else {
i++;
}
}

return {
insideTripleQuotes,
insideSingleQuotesQuery,
insideTripleQuotesQuery,
queryIndex: currentQueryStartIndex,
insideEsqlQuery: insideEsqlQueryRequest && (insideSingleQuotesQuery || insideTripleQuotesQuery),
esqlQueryIndex: insideEsqlQueryRequest ? currentQueryStartIndex : -1,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export { checkForTripleQuotesAndQueries, unescapeInvalidChars } from './autocomplete_utils';
export { checkForTripleQuotesAndEsqlQuery, unescapeInvalidChars } from './autocomplete_utils';
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { XJson } from '@kbn/es-ui-shared-plugin/public';
import { ErrorAnnotation } from '@kbn/monaco/src/console/types';
import { checkForTripleQuotesAndQueries } from '@kbn/monaco/src/console/utils';
import { checkForTripleQuotesAndEsqlQuery } from '@kbn/monaco/src/console/utils';
import { isQuotaExceededError } from '../../../services/history';
import { DEFAULT_VARIABLES, KIBANA_API_PREFIX } from '../../../../common/constants';
import { getStorage, StorageKeys } from '../../../services';
Expand Down Expand Up @@ -785,7 +785,7 @@ export class MonacoEditorActionsProvider {
private async isPositionInsideTripleQuotesAndQuery(
model: monaco.editor.ITextModel,
position: monaco.Position
): Promise<{ insideTripleQuotes: boolean; insideQuery: boolean }> {
): Promise<{ insideTripleQuotes: boolean; insideEsqlQuery: boolean }> {
const selectedRequests = await this.getSelectedParsedRequests();

for (const request of selectedRequests) {
Expand All @@ -800,21 +800,21 @@ export class MonacoEditorActionsProvider {
endColumn: position.column,
});

const { insideTripleQuotes, insideSingleQuotesQuery, insideTripleQuotesQuery } =
checkForTripleQuotesAndQueries(requestContentBefore);
const { insideTripleQuotes, insideEsqlQuery } =
checkForTripleQuotesAndEsqlQuery(requestContentBefore);
return {
insideTripleQuotes,
insideQuery: insideSingleQuotesQuery || insideTripleQuotesQuery,
insideEsqlQuery,
};
}
if (request.startLineNumber > position.lineNumber) {
// Stop iteration once we pass the cursor position
return { insideTripleQuotes: false, insideQuery: false };
return { insideTripleQuotes: false, insideEsqlQuery: false };
}
}

// Return false if the position is not inside a request
return { insideTripleQuotes: false, insideQuery: false };
return { insideTripleQuotes: false, insideEsqlQuery: false };
}

private triggerSuggestions() {
Expand All @@ -824,8 +824,8 @@ export class MonacoEditorActionsProvider {
return;
}
this.isPositionInsideTripleQuotesAndQuery(model, position).then(
({ insideTripleQuotes, insideQuery }) => {
if (insideTripleQuotes && !insideQuery) {
({ insideTripleQuotes, insideEsqlQuery }) => {
if (insideTripleQuotes && !insideEsqlQuery) {
// Don't trigger autocomplete suggestions inside scripts and strings
return;
}
Expand All @@ -839,11 +839,11 @@ export class MonacoEditorActionsProvider {
// Trigger suggestions if the line:
// - is empty
// - matches specified regex
// - is inside a query
// - is inside an ESQL query
if (
!lineContentBefore.trim() ||
shouldTriggerSuggestions(lineContentBefore) ||
insideQuery
insideEsqlQuery
) {
this.editor.trigger(TRIGGER_SUGGESTIONS_ACTION_LABEL, TRIGGER_SUGGESTIONS_HANDLER_ID, {});
}
Expand Down