[Console] Disable autocomplete suggestions inside scripts (#216986)

Fixes https://github.com/elastic/kibana/issues/212904

## Summary

This PR disables autocomplete suggestions if the curser is inside
scripts (triple-quote strings).


**How to test:**

Verify that there are no autocomplete suggestions when you place the
cursor inside the script in the request below:

```
POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "script": {
          "source":
          """
            for (field in params['fields']){
                if (!$(field, '').isEmpty()){
                    def value = $(field, '');
                    def hash = value.sha256();

                    // Now we need to traverse as deep as needed
                    // and write to that field
                    // because we do not have a simple
                    // set operation available
                    
    parts = field.splitOnToken('.');
                    

                }
            }
          """,
          "params": {
            "fields": [
              "user.name",
              "geo.city",
              "does.not.exist",
              "this.is.quite.a.deep.field"
            ]
          }
        }
      }
    ]
  }
}
```

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Elena Stoeva 2025-04-09 14:31:09 +01:00 committed by GitHub
parent f833e09121
commit b5a30054c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 88 additions and 9 deletions

View file

@ -38,6 +38,7 @@ import {
shouldTriggerSuggestions,
trackSentRequests,
getRequestFromEditor,
isInsideTripleQuotes,
} from './utils';
import type { AdjustedParsedRequest } from './types';
@ -783,22 +784,61 @@ export class MonacoEditorActionsProvider {
return this.editor.getPosition() ?? { lineNumber: 1, column: 1 };
}
private async isPositionInsideScript(
model: monaco.editor.ITextModel,
position: monaco.Position
): Promise<boolean> {
const selectedRequests = await this.getSelectedParsedRequests();
for (const request of selectedRequests) {
if (
request.startLineNumber <= position.lineNumber &&
request.endLineNumber >= position.lineNumber
) {
const requestContentBefore = model.getValueInRange({
startLineNumber: request.startLineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
if (isInsideTripleQuotes(requestContentBefore)) {
return true;
}
}
if (request.startLineNumber > position.lineNumber) {
// Stop iteration once we pass the cursor position
return false;
}
}
// Return false if no match
return false;
}
private triggerSuggestions() {
const model = this.editor.getModel();
const position = this.editor.getPosition();
if (!model || !position) {
return;
}
const lineContentBefore = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
this.isPositionInsideScript(model, position).then((isCursorInsideScript) => {
if (isCursorInsideScript) {
// Don't trigger autocomplete suggestions inside scripts
return;
}
const lineContentBefore = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
// if the line is empty or it matches specified regex, trigger suggestions
if (!lineContentBefore.trim() || shouldTriggerSuggestions(lineContentBefore)) {
this.editor.trigger(TRIGGER_SUGGESTIONS_ACTION_LABEL, TRIGGER_SUGGESTIONS_HANDLER_ID, {});
}
});
// if the line is empty or it matches specified regex, trigger suggestions
if (!lineContentBefore.trim() || shouldTriggerSuggestions(lineContentBefore)) {
this.editor.trigger(TRIGGER_SUGGESTIONS_ACTION_LABEL, TRIGGER_SUGGESTIONS_HANDLER_ID, {});
}
}
/*

View file

@ -27,6 +27,7 @@ import {
getUrlPathCompletionItems,
shouldTriggerSuggestions,
getInsertText,
isInsideTripleQuotes,
} from './autocomplete_utils';
describe('autocomplete_utils', () => {
@ -292,4 +293,25 @@ describe('autocomplete_utils', () => {
);
});
});
describe('isInsideTripleQuotes', () => {
it('should return false for an empty string', () => {
expect(isInsideTripleQuotes('')).toBe(false);
});
it('should return false for a request without triple quotes', () => {
const request = `POST _search\n{\n \"query\": {\n \"match\": {\n \"message\": \"hello world\"\n }\n }\n}`;
expect(isInsideTripleQuotes(request)).toBe(false);
});
it('should return true for a request ending inside triple quotes', () => {
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(isInsideTripleQuotes(request)).toBe(true);
});
it('should return false for a properly closed triple-quoted string', () => {
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(isInsideTripleQuotes(request)).toBe(false);
});
});
});

View file

@ -470,3 +470,19 @@ export const hasUnclosedQuote = (lineContent: string): boolean => {
return insideString;
};
export const isInsideTripleQuotes = (text: string) => {
let insideTripleQuote = false;
let i = 0;
while (i < text.length) {
if (text.startsWith('"""', i)) {
insideTripleQuote = !insideTripleQuote;
i += 3; // Skip the triple quotes
} else {
i++;
}
}
return insideTripleQuote;
};

View file

@ -28,6 +28,7 @@ export {
getUrlParamsCompletionItems,
getBodyCompletionItems,
shouldTriggerSuggestions,
isInsideTripleQuotes,
} from './autocomplete_utils';
export { getLineTokens, containsUrlParams } from './tokens_utils';
export { getStatusCodeDecorations } from './status_code_decoration_utils';