Repository: elastic/kibana
Pull request #213240: [ES|QL] Fixes the suggestion problem in where for multiline queries
Author: login: stratoula
Labels: name: release_note:fix
name: Feature:ES|QL
description: ES|QL related features in Kibana
name: Team:ESQL
description: ES|QL related features in Kibana t//
name: backport:version
description: Backport to applied version labels
name: v9.1.0
name: v8.19.0
Created: 2025-03-05T13:35:24Z | Merged: 2025-03-10T12:02:22Z

## Summary

Closes https://github.com/elastic/kibana/issues/213323

This is the attempt to fix this bug:

```
FROM kibana_sample_data_logs
| WHERE event.dataset == # cursor on this line
| LIMIT 10
```

In main the suggestions do not trigger. The problem is that the range is completely wrong. The lineNumber is 3 while it should be 2 and the start and end columns are also wrong.


This PR attempts to fix it (hopefully).
![meow](https://github.com/user-attachments/assets/2741891e-5186-477b-900f-ef42bb3371da)


### Checklist

- [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios




<!--ONMERGE {"backportTargets":["8.x"]} ONMERGE-->


Diff:
diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.where.test.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.where.test.ts
index 1fdc6fa2ae769..4105a6a097e5a 100644
--- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.where.test.ts
+++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.where.test.ts
@@ -411,7 +411,7 @@ describe('WHERE <expression>', () => {
           detail: 'Click to create',
           command: { id: 'esql.control.values.create', title: 'Click to create' },
           sortText: '11',
-          rangeToReplace: { start: 31, end: 31 },
+          rangeToReplace: { start: 30, end: 30 },
         });
       });
 
@@ -439,7 +439,7 @@ describe('WHERE <expression>', () => {
           detail: 'Named parameter',
           command: undefined,
           sortText: '11A',
-          rangeToReplace: { start: 31, end: 31 },
+          rangeToReplace: { start: 30, end: 30 },
         });
       });
     });
diff --git a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts
index 8fc421bda3751..a7be67ca460bf 100644
--- a/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts
+++ b/src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts
@@ -608,7 +608,7 @@ export async function getSuggestionsToRightOfOperatorExpression({
       ) {
         suggestions.push(listCompleteItem);
       } else {
-        const finalType = leftArgType || leftArgType || 'any';
+        const finalType = leftArgType || 'any';
         const supportedTypes = getSupportedTypesForBinaryOperators(fnDef, finalType as string);
 
         // this is a special case with AND/OR
@@ -671,12 +671,11 @@ export async function getSuggestionsToRightOfOperatorExpression({
   }
   return suggestions.map<SuggestionRawDefinition>((s) => {
     const overlap = getOverlapRange(queryText, s.text);
-    const offset = overlap.start === overlap.end ? 1 : 0;
     return {
       ...s,
       rangeToReplace: {
-        start: overlap.start + offset,
-        end: overlap.end + offset,
+        start: overlap.start,
+        end: overlap.end,
       },
     };
   });
diff --git a/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.test.ts b/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.test.ts
new file mode 100644
index 0000000000000..0a703771d4dd6
--- /dev/null
+++ b/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.test.ts
@@ -0,0 +1,63 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+import { offsetRangeToMonacoRange } from './utils';
+
+describe('offsetRangeToMonacoRange', () => {
+  test('should convert offset range to monaco range when the cursor is not at the end', () => {
+    const expression = 'FROM test | WHERE test == | LIMIT 1';
+    const range = { start: 26, end: 26 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 26,
+      endColumn: 26,
+      startLineNumber: 1,
+      endLineNumber: 1,
+    });
+  });
+
+  test('should convert offset range to monaco range when the cursor is at the end', () => {
+    const expression = 'FROM test | WHERE test == 1 | LIMIT 1';
+    const range = { start: 37, end: 37 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 37,
+      endColumn: 37,
+      startLineNumber: 1,
+      endLineNumber: 1,
+    });
+  });
+
+  test('should convert offset range to monaco range for multiple lines query when the cursor is not at the end', () => {
+    const expression = 'FROM test \n| WHERE test == \n| LIMIT 1';
+    const range = { start: 27, end: 27 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 16,
+      endColumn: 16,
+      startLineNumber: 2,
+      endLineNumber: 2,
+    });
+  });
+
+  test('should convert offset range to monaco range for multiple lines query when the cursor is at the end', () => {
+    const expression = 'FROM test \n| WHERE test == \n| LIMIT 1';
+    const range = { start: 35, end: 35 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 7,
+      endColumn: 7,
+      startLineNumber: 3,
+      endLineNumber: 3,
+    });
+  });
+});
diff --git a/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.ts b/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.ts
index be51b00bcde81..ad5e0ff50d272 100644
--- a/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.ts
+++ b/src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.ts
@@ -47,22 +47,25 @@ export const offsetRangeToMonacoRange = (
   let endLineNumber = 1;
   let currentLine = 1;
 
-  for (let i = 0; i < expression.length; i++) {
-    if (expression[i] === '\n') {
-      currentLine++;
-      currentOffset = i + 1;
-    }
+  const hasMultipleLines = expression.includes('\n');
+  const offset = hasMultipleLines ? 1 : 0;
 
-    if (i === range.start) {
+  for (let i = 0; i < expression.length; i++) {
+    if (i === range.start - offset) {
       startLineNumber = currentLine;
       startColumn = i - currentOffset;
     }
 
-    if (i === range.end) {
+    if (i === range.end - offset) {
       endLineNumber = currentLine;
       endColumn = i - currentOffset;
       break; // No need to continue once we find the end position
     }
+
+    if (expression[i] === '\n') {
+      currentLine++;
+      currentOffset = i;
+    }
   }
 
   // Handle the case where the start offset is past the end of the string


Reviews:
user: login: stratoula
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T13:35:45Z
user: login: stratoula
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T13:47:46Z
user: login: stratoula
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T13:48:18Z
user: login: stratoula
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T13:59:09Z
user: login: drewdaemon
body: I've confirmed it fixes the problem. This whole issue makes my head hurt. I left some comments, including one about the tests. Hopefully they make sense...
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T18:48:01Z
user: login: drewdaemon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T18:50:54Z
user: login: stratoula
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T07:12:23Z
user: login: stratoula
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T07:13:17Z
user: login: stratoula
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T07:33:37Z
user: login: drewdaemon
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T17:20:29Z
user: login: drewdaemon
body: Thank you for this fix!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T17:21:10Z
user: login: stratoula
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T08:07:43Z
user: login: eokoneyo
body: Tested locally, suggestions are presented as expected
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T12:02:20Z

Review comments:
diff_hunk: @@ -608,7 +608,7 @@ export async function getSuggestionsToRightOfOperatorExpression({
       ) {
         suggestions.push(listCompleteItem);
       } else {
-        const finalType = leftArgType || leftArgType || 'any';
path: src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts
user: login: stratoula
body: irrelevant but I saw it and decided to fix here
created_at: 2025-03-05T13:35:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -671,12 +671,11 @@ export async function getSuggestionsToRightOfOperatorExpression({
   }
   return suggestions.map<SuggestionRawDefinition>((s) => {
     const overlap = getOverlapRange(queryText, s.text);
-    const offset = overlap.start === overlap.end ? 1 : 0;
path: src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts
user: login: stratoula
body: I dont think that this is needed unless I am missing something
created_at: 2025-03-05T13:47:46Z
author_association: CONTRIBUTOR
path: src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.test.ts
user: login: stratoula
body: I added a test because it took me some time to understand how this function works
created_at: 2025-03-05T13:48:18Z
author_association: CONTRIBUTOR
path: src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.ts
user: login: stratoula
body: The problem here is that the range.start is 2 positions after the /n and this creates the problem. So if the /n is at position 10, the start comes at 12 which causes the bug and is wrong as the range start should be either 1 position or at the same with the newline character.

- By removing this 
> const offset = overlap.start === overlap.end ? 1 : 0;
the difference becomes 1 position

- By moving the line counter in the end we ensure that the line won't increase before we end the loop if we find the position

- I am introducing an offset here to be sure that we catch the position correctly for multilines (as I explain above the position difference is 1 here for multilines

created_at: 2025-03-05T13:59:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -671,12 +671,11 @@ export async function getSuggestionsToRightOfOperatorExpression({
   }
   return suggestions.map<SuggestionRawDefinition>((s) => {
     const overlap = getOverlapRange(queryText, s.text);
-    const offset = overlap.start === overlap.end ? 1 : 0;
path: src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts
user: login: drewdaemon
body: So, you basically moved this shift to the translation step?
created_at: 2025-03-06T18:37:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+import { offsetRangeToMonacoRange } from './utils';
+
+describe('offsetRangeToMonacoRange', () => {
+  test('should convert offset range to monaco range when the cursor is not at the end', () => {
+    const expression = 'FROM test | WHERE test == | LIMIT 1';
+    const range = { start: 26, end: 26 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 26,
+      endColumn: 26,
+      startLineNumber: 1,
+      endLineNumber: 1,
+    });
+  });
+
+  test('should convert offset range to monaco range when the cursor is at the end', () => {
+    const expression = 'FROM test | WHERE test == 1 | LIMIT 1';
+    const range = { start: 37, end: 37 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 37,
+      endColumn: 37,
+      startLineNumber: 1,
+      endLineNumber: 1,
+    });
+  });
+
+  test('should convert offset range to monaco range for multiple lines query when the cursor is in not at the end', () => {
+    const expression = 'FROM test \n| WHERE test == \n| LIMIT 1';
+    const range = { start: 27, end: 27 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 16,
+      endColumn: 16,
+      startLineNumber: 2,
+      endLineNumber: 2,
+    });
+  });
path: src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.test.ts
user: login: drewdaemon
body: I don't think this test case is catching regressions. I was able to break the suggestions again and this still passed. Here's the case I built that detected the regression

```ts
    const expression = `FROM kibana_sample_data_logs
| WHERE event.dataset == 
| LIMIT 1`;
    const range = { start: 55, end: 55 };
    const monacoRange = offsetRangeToMonacoRange(expression, range);

    expect(monacoRange).toEqual({
      startColumn: 26,
      endColumn: 26,
      startLineNumber: 2,
      endLineNumber: 2,
    });
```
created_at: 2025-03-06T18:38:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,22 +47,25 @@ export const offsetRangeToMonacoRange = (
   let endLineNumber = 1;
   let currentLine = 1;
 
-  for (let i = 0; i < expression.length; i++) {
-    if (expression[i] === '\n') {
-      currentLine++;
-      currentOffset = i + 1;
-    }
+  const hasMultipleLines = expression.includes('\n');
+  const offset = hasMultipleLines ? 1 : 0;
 
-    if (i === range.start) {
+  for (let i = 0; i < expression.length; i++) {
+    if (i === range.start - offset) {
       startLineNumber = currentLine;
       startColumn = i - currentOffset;
     }
 
-    if (i === range.end) {
+    if (i === range.end - offset) {
       endLineNumber = currentLine;
       endColumn = i - currentOffset;
       break; // No need to continue once we find the end position
     }
+
+    if (expression[i] === '\n') {
+      currentLine++;
+      currentOffset = i;
+    }
path: src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.ts
user: login: drewdaemon
body: I'm not sure I understand everything... but I think that...

Given the following query

```
FROM kibana_sample_data_logs
| WHERE event.dataset == /
| LIMIT 1
```

The requested range is `(55,55)` which is supposed to be one column past the cursor (line 2, column 26) (denoted by /).

So, column 26 is actually one column past the end of the second line (column 25—these are 1-indexed), which works fine as far as Monaco is concerned. The problem comes in this case because our code is picking up the newline character instead of letting the column number run off the current line.

Your changes prevent this by stopping the loop one index before the newline character via an offset. 

A potential problem is that it will do this shift for all positions in all multiline queries, not just handling the end-of-line case. Or maybe it's a fix? 😓 

Whatever we do here, I would like to clean things up with https://github.com/elastic/kibana/issues/209905.


created_at: 2025-03-06T18:46:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+import { offsetRangeToMonacoRange } from './utils';
+
+describe('offsetRangeToMonacoRange', () => {
+  test('should convert offset range to monaco range when the cursor is not at the end', () => {
+    const expression = 'FROM test | WHERE test == | LIMIT 1';
+    const range = { start: 26, end: 26 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 26,
+      endColumn: 26,
+      startLineNumber: 1,
+      endLineNumber: 1,
+    });
+  });
+
+  test('should convert offset range to monaco range when the cursor is at the end', () => {
+    const expression = 'FROM test | WHERE test == 1 | LIMIT 1';
+    const range = { start: 37, end: 37 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 37,
+      endColumn: 37,
+      startLineNumber: 1,
+      endLineNumber: 1,
+    });
+  });
+
+  test('should convert offset range to monaco range for multiple lines query when the cursor is in not at the end', () => {
+    const expression = 'FROM test \n| WHERE test == \n| LIMIT 1';
+    const range = { start: 27, end: 27 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 16,
+      endColumn: 16,
+      startLineNumber: 2,
+      endLineNumber: 2,
+    });
+  });
path: src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.test.ts
user: login: drewdaemon
body: Specifically I broke it by removing your offset and the tests still passed....
created_at: 2025-03-06T18:50:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -47,22 +47,25 @@ export const offsetRangeToMonacoRange = (
   let endLineNumber = 1;
   let currentLine = 1;
 
-  for (let i = 0; i < expression.length; i++) {
-    if (expression[i] === '\n') {
-      currentLine++;
-      currentOffset = i + 1;
-    }
+  const hasMultipleLines = expression.includes('\n');
+  const offset = hasMultipleLines ? 1 : 0;
 
-    if (i === range.start) {
+  for (let i = 0; i < expression.length; i++) {
+    if (i === range.start - offset) {
       startLineNumber = currentLine;
       startColumn = i - currentOffset;
     }
 
-    if (i === range.end) {
+    if (i === range.end - offset) {
       endLineNumber = currentLine;
       endColumn = i - currentOffset;
       break; // No need to continue once we find the end position
     }
+
+    if (expression[i] === '\n') {
+      currentLine++;
+      currentOffset = i;
+    }
path: src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.ts
user: login: stratoula
body: > A potential problem is that it will do this shift for all positions in all multiline queries

I think it is a fix 😅 (or at least with the way I understand it the multiline is messing the positions so this is a fix for everything)
created_at: 2025-03-07T07:12:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -671,12 +671,11 @@ export async function getSuggestionsToRightOfOperatorExpression({
   }
   return suggestions.map<SuggestionRawDefinition>((s) => {
     const overlap = getOverlapRange(queryText, s.text);
-    const offset = overlap.start === overlap.end ? 1 : 0;
path: src/platform/packages/shared/kbn-esql-validation-autocomplete/src/autocomplete/helper.ts
user: login: stratoula
body: No I removed this offset gaining one position closet to the real position (it was by 2 positions wrong due to this)
created_at: 2025-03-07T07:13:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+import { offsetRangeToMonacoRange } from './utils';
+
+describe('offsetRangeToMonacoRange', () => {
+  test('should convert offset range to monaco range when the cursor is not at the end', () => {
+    const expression = 'FROM test | WHERE test == | LIMIT 1';
+    const range = { start: 26, end: 26 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 26,
+      endColumn: 26,
+      startLineNumber: 1,
+      endLineNumber: 1,
+    });
+  });
+
+  test('should convert offset range to monaco range when the cursor is at the end', () => {
+    const expression = 'FROM test | WHERE test == 1 | LIMIT 1';
+    const range = { start: 37, end: 37 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 37,
+      endColumn: 37,
+      startLineNumber: 1,
+      endLineNumber: 1,
+    });
+  });
+
+  test('should convert offset range to monaco range for multiple lines query when the cursor is in not at the end', () => {
+    const expression = 'FROM test \n| WHERE test == \n| LIMIT 1';
+    const range = { start: 27, end: 27 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 16,
+      endColumn: 16,
+      startLineNumber: 2,
+      endLineNumber: 2,
+    });
+  });
path: src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.test.ts
user: login: stratoula
body:  It fails when I remove the offset and is the exact same scenario with yours 🤔 You do exactly the same, you just use another index. 😄 (maybe I am missing something)
created_at: 2025-03-07T07:33:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+import { offsetRangeToMonacoRange } from './utils';
+
+describe('offsetRangeToMonacoRange', () => {
+  test('should convert offset range to monaco range when the cursor is not at the end', () => {
+    const expression = 'FROM test | WHERE test == | LIMIT 1';
+    const range = { start: 26, end: 26 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 26,
+      endColumn: 26,
+      startLineNumber: 1,
+      endLineNumber: 1,
+    });
+  });
+
+  test('should convert offset range to monaco range when the cursor is at the end', () => {
+    const expression = 'FROM test | WHERE test == 1 | LIMIT 1';
+    const range = { start: 37, end: 37 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 37,
+      endColumn: 37,
+      startLineNumber: 1,
+      endLineNumber: 1,
+    });
+  });
+
+  test('should convert offset range to monaco range for multiple lines query when the cursor is in not at the end', () => {
+    const expression = 'FROM test \n| WHERE test == \n| LIMIT 1';
+    const range = { start: 27, end: 27 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 16,
+      endColumn: 16,
+      startLineNumber: 2,
+      endLineNumber: 2,
+    });
+  });
path: src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.test.ts
user: login: drewdaemon
body: WTH... it works. No idea what was happening on my end.
created_at: 2025-03-07T17:20:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+import { offsetRangeToMonacoRange } from './utils';
+
+describe('offsetRangeToMonacoRange', () => {
+  test('should convert offset range to monaco range when the cursor is not at the end', () => {
+    const expression = 'FROM test | WHERE test == | LIMIT 1';
+    const range = { start: 26, end: 26 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 26,
+      endColumn: 26,
+      startLineNumber: 1,
+      endLineNumber: 1,
+    });
+  });
+
+  test('should convert offset range to monaco range when the cursor is at the end', () => {
+    const expression = 'FROM test | WHERE test == 1 | LIMIT 1';
+    const range = { start: 37, end: 37 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 37,
+      endColumn: 37,
+      startLineNumber: 1,
+      endLineNumber: 1,
+    });
+  });
+
+  test('should convert offset range to monaco range for multiple lines query when the cursor is in not at the end', () => {
+    const expression = 'FROM test \n| WHERE test == \n| LIMIT 1';
+    const range = { start: 27, end: 27 };
+    const monacoRange = offsetRangeToMonacoRange(expression, range);
+
+    expect(monacoRange).toEqual({
+      startColumn: 16,
+      endColumn: 16,
+      startLineNumber: 2,
+      endLineNumber: 2,
+    });
+  });
path: src/platform/packages/shared/kbn-monaco/src/languages/esql/lib/shared/utils.test.ts
user: login: stratoula
body: No prob 🙌 
created_at: 2025-03-10T08:07:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: elasticmachine
created_at: 2025-03-06T07:04:40Z
body: Pinging @elastic/kibana-esql (Team:ESQL)
author_association: CONTRIBUTOR
user: login: elasticmachine
created_at: 2025-03-10T09:34:55Z
body: ## :green_heart: Build Succeeded
* [Buildkite Build](https://buildkite.com/elastic/kibana-pull-request/builds/282029)
* Commit: a91227a7811eb158423c8be5e69a7426898a92f6

### Metrics [[docs](https://www.elastic.co/guide/en/kibana/master/ci-metrics.html)]
#### Page load bundle
> Size of the bundles that are downloaded on every page load. Target size is below 100kb


| id | [before](https://github.com/elastic/kibana/commit/2c93a349f31fede1dbfa9470e0730547e5c6d552) | [after](https://github.com/elastic/kibana/commit/a91227a7811eb158423c8be5e69a7426898a92f6) | diff |
| --- | --- | --- | --- |
| `kbnUiSharedDeps-srcJs` | 3.6MB | 3.6MB | +3.0B |



### History
* :yellow_heart: [Build #281679](https://buildkite.com/elastic/kibana-pull-request/builds/281679) was flaky a46bc7e6ba00b763d0d9f5b8b978c453a6dc37d6
* :green_heart: [Build #281148](https://buildkite.com/elastic/kibana-pull-request/builds/281148) succeeded 67294176f58fd4bb7b4caa45491f4764dd1dbbaa
* :broken_heart: [Build #281094](https://buildkite.com/elastic/kibana-pull-request/builds/281094) failed aa3572df6d4fbfe945541a7abdb58722fdcfde9a

<!--buildkite-pr-comment-kibana-pull-request
{"builds":[{"buildStatus":{"state":"passed","success":true,"hasRetries":false,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/282029","number":282029,"commit":"a91227a7811eb158423c8be5e69a7426898a92f6"},{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/281679","number":281679,"commit":"a46bc7e6ba00b763d0d9f5b8b978c453a6dc37d6"},{"buildStatus":{"state":"passed","success":true,"hasRetries":false,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/281148","number":281148,"commit":"67294176f58fd4bb7b4caa45491f4764dd1dbbaa"},{"buildStatus":{"state":"failing","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/281094","number":281094,"commit":"aa3572df6d4fbfe945541a7abdb58722fdcfde9a"}],"number":282029}
buildkite-pr-comment-->
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-10T12:02:45Z
body: Starting backport for target branches: 8.x

https://github.com/elastic/kibana/actions/runs/13764133332
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-03-10T12:08:21Z
body: ## 💔 All backports failed

| Status | Branch | Result |
|:------:|:------:|:------|
|❌|8.x|**Backport failed because of merge conflicts**<br><br>You might need to backport the following PRs to 8.x:<br> - [[Security Solution] Test plan for prebuilt rule upgrade notifications (#212817)](https://github.com/elastic/kibana/pull/212817)<br> - [[ResponseOps][Cases] Fix flaky Cases action Cypress test (#213529)](https://github.com/elastic/kibana/pull/213529)<br> - [[ES\|QL] Small refactoring (#213439)](https://github.com/elastic/kibana/pull/213439)<br> - [[Discover] Enable `consistent-type-imports` eslint rule (#212293)](https://github.com/elastic/kibana/pull/212293)<br> - [[Response Ops] [Rule Form] Remove V1 Rule Form Flyout (#209171)](https://github.com/elastic/kibana/pull/209171)<br> - [[ResponseOps][MW] Use date format from settings in MW UI (#211576)](https://github.com/elastic/kibana/pull/211576)<br> - [Criticality badge medium impact bug / typo fixed (#213456)](https://github.com/elastic/kibana/pull/213456)|

### Manual backport
To create the backport manually run:
```
node scripts/backport --pr 213240
```

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR
user: login: stratoula
created_at: 2025-03-10T13:50:59Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|8.x|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/213744">](https://github.com/elastic/kibana/pull/213744)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR