diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx index 3a80742615ad29..09da1876949890 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx +++ b/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx @@ -74,6 +74,10 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { return actionsProvider.current!.getDocumentationLink(docLinkVersion); }, [docLinkVersion]); + const autoIndentCallback = useCallback(async () => { + return actionsProvider.current!.autoIndent(); + }, []); + const sendRequestsCallback = useCallback(async () => { await actionsProvider.current?.sendRequests(toasts, dispatch, trackUiMetric, http); }, [dispatch, http, toasts, trackUiMetric]); @@ -125,7 +129,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { {}} + autoIndent={autoIndentCallback} notifications={notifications} /> 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 38fa543b363230..de9c2289c8a291 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 @@ -34,10 +34,13 @@ import { SELECTED_REQUESTS_CLASSNAME, stringifyRequest, trackSentRequests, + getAutoIndentedRequests, } from './utils'; import type { AdjustedParsedRequest } from './types'; +const AUTO_INDENTATION_ACTION_LABEL = 'Apply indentations'; + export class MonacoEditorActionsProvider { private parsedRequestsProvider: ConsoleParsedRequestsProvider; private highlightedLines: monaco.editor.IEditorDecorationsCollection; @@ -343,4 +346,57 @@ export class MonacoEditorActionsProvider { ): monaco.languages.ProviderResult { return this.getSuggestions(model, position, context); } + + /* + 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) { + return ''; + } + 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(); + } + + /** + * This function applies indentations to the request in the selected text. + */ + public async autoIndent() { + const parsedRequests = await this.getSelectedParsedRequests(); + const selectionStartLineNumber = parsedRequests[0].startLineNumber; + const selectionEndLineNumber = parsedRequests[parsedRequests.length - 1].endLineNumber; + const selectedRange = new monaco.Range( + selectionStartLineNumber, + 1, + selectionEndLineNumber, + this.editor.getModel()?.getLineMaxColumn(selectionEndLineNumber) ?? 1 + ); + + if (parsedRequests.length < 1) { + return; + } + + const selectedText = this.getTextInRange(selectedRange); + const allText = this.getTextInRange(); + + const autoIndentedText = getAutoIndentedRequests(parsedRequests, selectedText, allText); + + this.editor.executeEdits(AUTO_INDENTATION_ACTION_LABEL, [ + { + range: selectedRange, + text: autoIndentedText, + }, + ]); + } } diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts index 2b117bcfd558d3..a0de7b461e99a3 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/index.ts @@ -14,6 +14,7 @@ export { replaceRequestVariables, getCurlRequest, trackSentRequests, + getAutoIndentedRequests, } from './requests_utils'; export { getDocumentationLinkFromAutocomplete, diff --git a/src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.test.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.test.ts index 14235e57c6f016..f07a6db3a68819 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.test.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.test.ts @@ -7,6 +7,7 @@ */ import { + getAutoIndentedRequests, getCurlRequest, replaceRequestVariables, stringifyRequest, @@ -160,4 +161,195 @@ describe('requests_utils', () => { expect(mockMetricsTracker.count).toHaveBeenNthCalledWith(2, 'POST__test'); }); }); + + describe('getAutoIndentedRequests', () => { + 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 = { + method: 'GET', + url: '_search', + data: [{ query: { match_all: {} } }], + // Offsets are with respect to the sample editor text + startLineNumber: 2, + endLineNumber: 7, + startOffset: 1, + endOffset: 36, + }; + + const TEST_REQUEST_2 = { + method: 'GET', + url: '_all', + data: [], + // Offsets are with respect to the sample editor text + startLineNumber: 10, + endLineNumber: 10, + startOffset: 1, + endOffset: 36, + }; + + const TEST_REQUEST_3 = { + method: 'POST', + url: '/_bulk', + // Multi-data + data: [{ index: { _index: 'books' } }, { name: '1984' }, { name: 'Atomic habits' }], + // Offsets are with respect to the sample editor text + startLineNumber: 15, + endLineNumber: 23, + startOffset: 1, + endOffset: 36, + }; + + const TEST_REQUEST_4 = { + method: 'GET', + url: '_search', + data: [{ query: { match_all: {} } }], + // 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], + 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], + 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], + sampleEditorTextLines + .slice(TEST_REQUEST_3.startLineNumber - 1, TEST_REQUEST_3.endLineNumber) + .join('\n'), + sampleEditorTextLines.join('\n') + ); + const expectedResultLines = [ + 'POST /_bulk', + '{', + ' "index": {', + ' "_index": "books"', + ' }', + '}', + '{', + ' "name": "1984"', + '}', + '{', + ' "name": "Atomic habits"', + '}', + ]; + + 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], + sampleEditorTextLines.slice(1, 23).join('\n'), + sampleEditorTextLines.join('\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(expectedResultLines.join('\n')); + }); + + it('does not auto-indent a request with comments', () => { + 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/requests_utils.ts b/src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.ts index 7f9babb333e894..bf9c6074bb20a7 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.ts +++ b/src/plugins/console/public/application/containers/editor/monaco/utils/requests_utils.ts @@ -13,6 +13,7 @@ import type { DevToolsVariable } from '../../../../components'; import type { EditorRequest } from '../types'; import { variableTemplateRegex } from './constants'; import { removeTrailingWhitespaces } from './tokens_utils'; +import { AdjustedParsedRequest } from '../types'; /* * This function stringifies and normalizes the parsed request: @@ -130,3 +131,64 @@ const replaceVariables = (text: string, variables: DevToolsVariable[]): string = } return text; }; + +const containsComments = (text: string) => { + return text.indexOf('//') >= 0 || text.indexOf('/*') >= 0; +}; + +/** + * 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[], + selectedText: string, + allText: string +): string => { + const selectedTextLines = selectedText.split(`\n`); + const allTextLines = allText.split(`\n`); + const formattedTextLines: string[] = []; + + let currentLineIndex = 0; + let currentRequestIndex = 0; + + 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 requestLines = allTextLines.slice(request.startLineNumber - 1, request.endLineNumber); + + if (requestLines.some((line) => containsComments(line))) { + // If request has comments, add it as it is - without formatting + // TODO: Format requests with comments + formattedTextLines.push(...requestLines); + } else { + // If no comments, add stringified parsed request + const stringifiedRequest = stringifyRequest(request); + const firstLine = stringifiedRequest.method + ' ' + stringifiedRequest.url; + formattedTextLines.push(firstLine); + + if (stringifiedRequest.data && stringifiedRequest.data.length > 0) { + formattedTextLines.push(...stringifiedRequest.data); + } + } + + currentLineIndex = currentLineIndex + requestLines.length; + currentRequestIndex++; + } else { + // Current line is a comment or whitespaces + // Trim white spaces and add it to the formatted text + formattedTextLines.push(selectedTextLines[currentLineIndex].trim()); + currentLineIndex++; + } + } + + return formattedTextLines.join('\n'); +};