From 50363cc7a497d97823e9fe92cded2727a299fcc6 Mon Sep 17 00:00:00 2001 From: Elena Stoeva Date: Tue, 14 May 2024 16:20:54 +0100 Subject: [PATCH] Address feedback --- packages/kbn-monaco/src/console/parser.js | 2 - packages/kbn-monaco/src/console/types.ts | 1 - .../monaco/monaco_editor_actions_provider.ts | 49 ++-- .../editor/monaco/use_register_commands.ts | 35 +++ .../containers/editor/monaco/utils.test.ts | 226 +++++++++++------- .../containers/editor/monaco/utils.ts | 55 ++--- 6 files changed, 235 insertions(+), 133 deletions(-) create mode 100644 src/plugins/console/public/application/containers/editor/monaco/use_register_commands.ts diff --git a/packages/kbn-monaco/src/console/parser.js b/packages/kbn-monaco/src/console/parser.js index f89de01885031c..e7369444ab85a5 100644 --- a/packages/kbn-monaco/src/console/parser.js +++ b/packages/kbn-monaco/src/console/parser.js @@ -59,9 +59,7 @@ export const createParser = () => { }, addRequestEnd = function() { const lastRequest = getLastRequest(); - const requestText = text.substring(requestStartOffset, requestEndOffset); lastRequest.endOffset = requestEndOffset; - lastRequest.text = requestText; requests.push(lastRequest); }, error = function (m) { diff --git a/packages/kbn-monaco/src/console/types.ts b/packages/kbn-monaco/src/console/types.ts index e9faecd1383ba7..986ffd67045088 100644 --- a/packages/kbn-monaco/src/console/types.ts +++ b/packages/kbn-monaco/src/console/types.ts @@ -17,7 +17,6 @@ export interface ParsedRequest { method: string; url: string; data?: Array>; - text: string; } export interface ConsoleParserResult { errors: ErrorAnnotation[]; diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts index ed9668a5ba0aba..802eed50483c44 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts @@ -44,7 +44,6 @@ export interface EditorRequest { method: string; url: string; data: string[]; - text: string; } export interface AdjustedParsedRequest extends ParsedRequest { @@ -343,23 +342,29 @@ export class MonacoEditorActionsProvider { return this.getSuggestions(model, position); } - private getTextInRange(selectionRange: monaco.IRange): string { + /* + This function returns the text in the provided range. + If no range is provided, it returns all text in the editor. + */ + private getTextInRange(selectionRange?: monaco.IRange): string { const model = this.editor.getModel(); - if (!model || !selectionRange) { + if (!model) { return ''; } - const { startLineNumber, startColumn, endLineNumber, endColumn } = selectionRange; - const text = model.getValueInRange({ - startLineNumber, - startColumn, - endLineNumber, - endColumn, - }); - return text; + if (selectionRange) { + const { startLineNumber, startColumn, endLineNumber, endColumn } = selectionRange; + return model.getValueInRange({ + startLineNumber, + startColumn, + endLineNumber, + endColumn, + }); + } + // If no range is provided, return all text in the editor + return model.getValue(); } public async autoIndent(event: React.MouseEvent) { - event.preventDefault(); const parsedRequests = await this.getSelectedParsedRequests(); const selectionStartLineNumber = parsedRequests[0].startLineNumber; const selectionEndLineNumber = parsedRequests[parsedRequests.length - 1].endLineNumber; @@ -375,14 +380,20 @@ export class MonacoEditorActionsProvider { } const selectedText = this.getTextInRange(selectedRange); + const allText = this.getTextInRange(); - const autoIndentedText = getAutoIndentedRequests(parsedRequests, selectedText); + const autoIndentedText = getAutoIndentedRequests(parsedRequests, selectedText, allText); - this.editor.executeEdits('', [ - { - range: selectedRange, - text: autoIndentedText, - }, - ]); + this.editor.executeEdits( + i18n.translate('console.monaco.applyIndentationCall', { + defaultMessage: 'Apply indentations.', + }), + [ + { + range: selectedRange, + text: autoIndentedText, + }, + ] + ); } } diff --git a/src/plugins/console/public/application/containers/editor/monaco/use_register_commands.ts b/src/plugins/console/public/application/containers/editor/monaco/use_register_commands.ts new file mode 100644 index 00000000000000..26959cde1f98c9 --- /dev/null +++ b/src/plugins/console/public/application/containers/editor/monaco/use_register_commands.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { monaco } from '@kbn/monaco'; + +interface RegisterCommandsParams { + editor: monaco.editor.IStandaloneCodeEditor; + getDocumentationLink: () => Promise; +} + +/** + * Hook that sets up the autocomplete polling for Console editor. + * + * @param params The {@link RegisterCommandsParams} to use. + */ +export const useRegisterCommands = () => { + return (params: RegisterCommandsParams) => { + const { editor, getDocumentationLink } = params; + + const openDocs = async () => { + const documentation = await getDocumentationLink(); + if (!documentation) { + return; + } + window.open(documentation, '_blank'); + }; + + editor.addCommand(monaco.KeyCode.Enter, openDocs); + }; +}; diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils.test.ts b/src/plugins/console/public/application/containers/editor/monaco/utils.test.ts index 60dc6ce236685b..96bbea0cc54492 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/utils.test.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/utils.test.ts @@ -14,7 +14,6 @@ import { replaceRequestVariables, stringifyRequest, trackSentRequests, - isStartOfRequest, } from './utils'; import { MetricsTracker } from '../../../../types'; import { AutoCompleteContext } from '../../../../lib/autocomplete/types'; @@ -67,7 +66,6 @@ describe('monaco editor utils', () => { endOffset: 11, method: 'get', url: '_search some_text', - text: 'get _search some_text', }; it('calls the "removeTrailingWhitespaces" on the url', () => { const stringifiedRequest = stringifyRequest(request); @@ -107,7 +105,6 @@ describe('monaco editor utils', () => { method: 'GET', url: '${variable1}', data: [], - text: 'GET ${variable1}', }; it('when there is no other text', () => { const result = replaceRequestVariables(request, variables); @@ -134,7 +131,6 @@ describe('monaco editor utils', () => { method: 'GET', url: '${variable1}', data: [JSON.stringify({ '${variable1}': '${variable2}' }, null, 2)], - text: '', }; it('works with several variables', () => { const result = replaceRequestVariables(request, variables); @@ -145,7 +141,7 @@ describe('monaco editor utils', () => { describe('getCurlRequest', () => { it('works without a request body', () => { - const request = { method: 'GET', url: '_search', data: [], text: '' }; + const request = { method: 'GET', url: '_search', data: [] }; const result = getCurlRequest(request, 'http://test.com'); expect(result).toBe('curl -XGET "http://test.com/_search" -H "kbn-xsrf: reporting"'); }); @@ -154,7 +150,6 @@ describe('monaco editor utils', () => { method: 'GET', url: '_search', data: [JSON.stringify(dataObjects[0], null, 2)], - text: '', }; const result = getCurlRequest(request, 'http://test.com'); expect(result).toBe( @@ -171,7 +166,6 @@ describe('monaco editor utils', () => { method: 'GET', url: '_search', data: [JSON.stringify(dataObjects[0], null, 2), JSON.stringify(dataObjects[1], null, 2)], - text: '', }; const result = getCurlRequest(request, 'http://test.com'); expect(result).toBe( @@ -191,8 +185,8 @@ describe('monaco editor utils', () => { describe('trackSentRequests', () => { it('tracks each request correctly', () => { const requests = [ - { method: 'GET', url: '_search', data: [], text: '' }, - { method: 'POST', url: '_test', data: [], text: '' }, + { method: 'GET', url: '_search', data: [] }, + { method: 'POST', url: '_test', data: [] }, ]; const mockMetricsTracker: jest.Mocked = { count: jest.fn(), load: jest.fn() }; trackSentRequests(requests, mockMetricsTracker); @@ -203,7 +197,7 @@ describe('monaco editor utils', () => { }); describe('getDocumentationLink', () => { - const mockRequest = { method: 'GET', url: '_search', data: [], text: '' }; + const mockRequest = { method: 'GET', url: '_search', data: [] }; const version = '8.13'; const expectedLink = 'http://elastic.co/8.13/_search'; @@ -247,130 +241,194 @@ describe('monaco editor utils', () => { }); }); - describe('isStartOfRequest', () => { - it('correctly matches first lines of requests', () => { - expect(isStartOfRequest('GET _all')).toBe(true); - expect(isStartOfRequest(' get _search ')).toBe(true); - expect(isStartOfRequest('POST _all')).toBe(true); - expect(isStartOfRequest(' post _search ')).toBe(true); - expect(isStartOfRequest('PUT _all')).toBe(true); - expect(isStartOfRequest(' put _search ')).toBe(true); - expect(isStartOfRequest('DELETE _all')).toBe(true); - expect(isStartOfRequest(' delete _search ')).toBe(true); - expect(isStartOfRequest('HEAD _all')).toBe(true); - expect(isStartOfRequest(' head _search ')).toBe(true); - expect(isStartOfRequest('PATCH _all')).toBe(true); - expect(isStartOfRequest(' patch _search ')).toBe(true); - }); - - it('does not match any other lines', () => { - expect(isStartOfRequest('// comment')).toBe(false); - expect(isStartOfRequest('/*')).toBe(false); - expect(isStartOfRequest(' { ')).toBe(false); - expect(isStartOfRequest('"jdks": 4')).toBe(false); - expect(isStartOfRequest(' ')).toBe(false); - expect(isStartOfRequest('')).toBe(false); - expect(isStartOfRequest(' }')).toBe(false); - }); - }); - describe('getAutoIndentedRequests', () => { - const mockRequestParams = { - // Required properties in the AdjustedParsedRequest type - startLineNumber: 1, - endLineNumber: 1, - startOffset: 1, - endOffset: 1, - }; + const sampleEditorTextLines = [ + ' ', // line 1 + 'GET _search ', // line 2 + '{ ', // line 3 + ' "query": { ', // line 4 + ' "match_all": { } ', // line 5 + ' } ', // line 6 + ' } ', // line 7 + ' ', // line 8 + '// single comment before Request 2 ', // line 9 + ' GET _all ', // line 10 + ' ', // line 11 + '/* ', // line 12 + ' multi-line comment before Request 3', // line 13 + '*/ ', // line 14 + 'POST /_bulk ', // line 15 + '{ ', // line 16 + ' "index":{ ', // line 17 + ' "_index":"books" ', // line 18 + ' } ', // line 19 + ' } ', // line 20 + '{ ', // line 21 + '"name":"1984" ', // line 22 + '}{"name":"Atomic habits"} ', // line 23 + ' ', // line 24 + 'GET _search // test comment ', // line 25 + '{ ', // line 26 + ' "query": { ', // line 27 + ' "match_all": { } // comment', // line 28 + ' } ', // line 29 + '} ', // line 30 + ' // some comment ', // line 31 + ' ', // line 32 + ]; const TEST_REQUEST_1 = { - ...mockRequestParams, method: 'GET', url: '_search', data: [{ query: { match_all: {} } }], - // Non-formatted text - text: 'GET _search \n{ \n "query": {\n "match_all": { }\n }\n}', + // Offsets are with respect to the sample editor text + startLineNumber: 2, + endLineNumber: 7, + startOffset: 1, + endOffset: 36, }; const TEST_REQUEST_2 = { - ...mockRequestParams, method: 'GET', url: '_all', data: [], - // Non-formatted text - text: ' GET _all ', + // Offsets are with respect to the sample editor text + startLineNumber: 10, + endLineNumber: 10, + startOffset: 1, + endOffset: 36, }; const TEST_REQUEST_3 = { - ...mockRequestParams, method: 'POST', url: '/_bulk', // Multi-data data: [{ index: { _index: 'books' } }, { name: '1984' }, { name: 'Atomic habits' }], - // Non-formatted text - text: 'POST /_bulk\n{\n"index":{\n"_index":"books"\n}\n}\n{\n"name":"1984"\n}{"name":"Atomic habits"}', + // Offsets are with respect to the sample editor text + startLineNumber: 15, + endLineNumber: 23, + startOffset: 1, + endOffset: 36, }; const TEST_REQUEST_4 = { - ...mockRequestParams, method: 'GET', url: '_search', data: [{ query: { match_all: {} } }], - // Non-formatted text with comments - text: 'GET _search // test comment \n{ \n "query": {\n "match_all": { } // comment\n }\n}', + // Offsets are with respect to the sample editor text + startLineNumber: 24, + endLineNumber: 30, + startOffset: 1, + endOffset: 36, }; it('correctly auto-indents a single request with data', () => { - const formattedData = getAutoIndentedRequests([TEST_REQUEST_1], TEST_REQUEST_1.text); - const expectedResult = 'GET _search\n{\n "query": {\n "match_all": {}\n }\n}'; - expect(formattedData).toBe(expectedResult); + const formattedData = getAutoIndentedRequests( + [TEST_REQUEST_1], + sampleEditorTextLines + .slice(TEST_REQUEST_1.startLineNumber - 1, TEST_REQUEST_1.endLineNumber) + .join('\n'), + sampleEditorTextLines.join('\n') + ); + const expectedResultLines = [ + 'GET _search', + '{', + ' "query": {', + ' "match_all": {}', + ' }', + '}', + ]; + + expect(formattedData).toBe(expectedResultLines.join('\n')); }); it('correctly auto-indents a single request with no data', () => { - const formattedData = getAutoIndentedRequests([TEST_REQUEST_2], TEST_REQUEST_2.text); + const formattedData = getAutoIndentedRequests( + [TEST_REQUEST_2], + sampleEditorTextLines + .slice(TEST_REQUEST_2.startLineNumber - 1, TEST_REQUEST_2.endLineNumber) + .join('\n'), + sampleEditorTextLines.join('\n') + ); const expectedResult = 'GET _all'; expect(formattedData).toBe(expectedResult); }); it('correctly auto-indents a single request with multiple data', () => { - const formattedData = getAutoIndentedRequests([TEST_REQUEST_3], TEST_REQUEST_3.text); - const expectedResult = - 'POST /_bulk\n{\n "index": {\n "_index": "books"\n }\n}\n{\n "name": "1984"\n}\n{\n "name": "Atomic habits"\n}'; - - expect(formattedData).toBe(expectedResult); - }); - - it('correctly auto-indents multiple request', () => { const formattedData = getAutoIndentedRequests( - [TEST_REQUEST_1, TEST_REQUEST_2, TEST_REQUEST_3], - TEST_REQUEST_1.text + '\n\n' + TEST_REQUEST_2.text + '\n\n' + TEST_REQUEST_3.text + [TEST_REQUEST_3], + sampleEditorTextLines + .slice(TEST_REQUEST_3.startLineNumber - 1, TEST_REQUEST_3.endLineNumber) + .join('\n'), + sampleEditorTextLines.join('\n') ); - const expectedResult = - 'GET _search\n{\n "query": {\n "match_all": {}\n }\n}\n\nGET _all\n\nPOST /_bulk\n{\n "index": {\n "_index": "books"\n }\n}\n{\n "name": "1984"\n}\n{\n "name": "Atomic habits"\n}'; + const expectedResultLines = [ + 'POST /_bulk', + '{', + ' "index": {', + ' "_index": "books"', + ' }', + '}', + '{', + ' "name": "1984"', + '}', + '{', + ' "name": "Atomic habits"', + '}', + ]; - expect(formattedData).toBe(expectedResult); + expect(formattedData).toBe(expectedResultLines.join('\n')); }); it('auto-indents multiple request with comments in between', () => { const formattedData = getAutoIndentedRequests( [TEST_REQUEST_1, TEST_REQUEST_2, TEST_REQUEST_3], - TEST_REQUEST_1.text + - '\n\n// single comment\n' + - TEST_REQUEST_2.text + - '\n\n/*\n multi-line comment\n*/\n' + - TEST_REQUEST_3.text + sampleEditorTextLines.slice(1, 23).join('\n'), + sampleEditorTextLines.join('\n') ); - const expectedResult = - 'GET _search\n{\n "query": {\n "match_all": {}\n }\n}\n\n// single comment\nGET _all\n\n/*\n multi-line comment\n*/\nPOST /_bulk\n{\n "index": {\n "_index": "books"\n }\n}\n{\n "name": "1984"\n}\n{\n "name": "Atomic habits"\n}'; + const expectedResultLines = [ + 'GET _search', + '{', + ' "query": {', + ' "match_all": {}', + ' }', + '}', + '', + '// single comment before Request 2', + 'GET _all', + '', + '/*', + 'multi-line comment before Request 3', + '*/', + 'POST /_bulk', + '{', + ' "index": {', + ' "_index": "books"', + ' }', + '}', + '{', + ' "name": "1984"', + '}', + '{', + ' "name": "Atomic habits"', + '}', + ]; - expect(formattedData).toBe(expectedResult); + expect(formattedData).toBe(expectedResultLines.join('\n')); }); it('does not auto-indent a request with comments', () => { - const formattedData = getAutoIndentedRequests([TEST_REQUEST_4], TEST_REQUEST_4.text); - const expectedResult = TEST_REQUEST_4.text; - expect(formattedData).toBe(expectedResult); + const requestText = sampleEditorTextLines + .slice(TEST_REQUEST_4.startLineNumber - 1, TEST_REQUEST_4.endLineNumber) + .join('\n'); + const formattedData = getAutoIndentedRequests( + [TEST_REQUEST_4], + requestText, + sampleEditorTextLines.join('\n') + ); + + expect(formattedData).toBe(requestText); }); }); }); diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils.ts b/src/plugins/console/public/application/containers/editor/monaco/utils.ts index 3a10802b1064ae..c08f5c80e03def 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/utils.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/utils.ts @@ -61,7 +61,7 @@ export const stringifyRequest = (parsedRequest: ParsedRequest): EditorRequest => const url = removeTrailingWhitespaces(parsedRequest.url); const method = parsedRequest.method.toUpperCase(); const data = parsedRequest.data?.map((parsedData) => JSON.stringify(parsedData, null, 2)); - return { url, method, data: data ?? [], text: parsedRequest.text }; + return { url, method, data: data ?? [] }; }; const replaceVariables = (text: string, variables: DevToolsVariable[]): string => { @@ -76,14 +76,13 @@ const replaceVariables = (text: string, variables: DevToolsVariable[]): string = }; export const replaceRequestVariables = ( - { method, url, data, text }: EditorRequest, + { method, url, data }: EditorRequest, variables: DevToolsVariable[] ): EditorRequest => { return { method, url: replaceVariables(url, variables), data: data.map((dataObject) => replaceVariables(dataObject, variables)), - text, }; }; @@ -135,49 +134,51 @@ export const getDocumentationLink = (request: EditorRequest, docLinkVersion: str return null; }; -export const isStartOfRequest = (line: string) => { - const regex = /\s*(GET|POST|PUT|DELETE|HEAD|PATCH|get|post|put|delete|head|patch)\s+.*/g; - return regex.test(line); -}; - const containsComments = (text: string) => { return text.indexOf('//') >= 0 || text.indexOf('/*') >= 0; }; -/* - * This function takes a list of parsed requests and a string representing the unformatted - * text from the editor that contains these requests, and returns a text in which - * the requests are auto-indented. +/** + * This function takes a string containing unformatted Console requests and + * returns a text in which the requests are auto-indented. + * @param requests The list of {@link AdjustedParsedRequest} that are in the selected text in the editor. + * @param selectedText The selected text in the editor. + * @param allText The whole text input in the editor. */ export const getAutoIndentedRequests = ( requests: AdjustedParsedRequest[], - text: string + selectedText: string, + allText: string ): string => { - const textLines = text.split(`\n`); - const formattedText: string[] = []; + const selectedTextLines = selectedText.split(`\n`); + const allTextLines = allText.split(`\n`); + const formattedTextLines: string[] = []; let currentLineIndex = 0; let currentRequestIndex = 0; - while (currentLineIndex < textLines.length) { - if (isStartOfRequest(textLines[currentLineIndex])) { + while (currentLineIndex < selectedTextLines.length) { + const request = requests[currentRequestIndex]; + // Check if the current line is the start of the next request + if ( + request && + selectedTextLines[currentLineIndex] === allTextLines[request.startLineNumber - 1] + ) { // Start of a request - const request = requests[currentRequestIndex]; - const requestText = request.text; - const requestLines = requestText.split('\n'); + const requestLines = allTextLines.slice(request.startLineNumber - 1, request.endLineNumber); - if (containsComments(requestText)) { + if (requestLines.some((line) => containsComments(line))) { // If request has comments, add it as it is - without formatting // TODO: Format requests with comments - formattedText.push(...requestLines); + formattedTextLines.push(...requestLines); } else { // If no comments, add stringified parsed request const stringifiedRequest = stringifyRequest(request); const firstLine = stringifiedRequest.method + ' ' + stringifiedRequest.url; - formattedText.push(firstLine); + formattedTextLines.push(firstLine); if (stringifiedRequest.data && stringifiedRequest.data.length > 0) { - formattedText.push(...stringifiedRequest.data); + formattedTextLines.push(...stringifiedRequest.data); } } @@ -185,13 +186,13 @@ export const getAutoIndentedRequests = ( currentRequestIndex++; } else { // Current line is a comment or whitespaces - // Add it to the formatted text as it is - formattedText.push(textLines[currentLineIndex]); + // Trim white spaces and add it to the formatted text + formattedTextLines.push(selectedTextLines[currentLineIndex].trim()); currentLineIndex++; } } - return formattedText.join('\n'); + return formattedTextLines.join('\n'); }; /*