Skip to content

Commit 5b2411a

Browse files
committed
fix: Make auto-complete replace existing suggestion suffix
1 parent 92fbb5a commit 5b2411a

File tree

3 files changed

+68
-5
lines changed

3 files changed

+68
-5
lines changed

.changeset/smooth-poems-film.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
fix: Improve auto-complete behavior for aliases and maps

packages/app/src/components/SQLEditor/__tests__/utils.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,23 @@ function getSuggestionLabels(
3939
.map(o => o.label);
4040
}
4141

42+
/**
43+
* Returns the range [from, to] that CodeMirror would replace when the user
44+
* accepts a suggestion. This lets us verify that the entire identifier
45+
* (including any trailing characters after the cursor) gets replaced.
46+
*/
47+
function getReplacementRange(
48+
doc: string,
49+
pos: number,
50+
): { from: number; to: number } | null {
51+
const source = createIdentifierCompletionSource(TEST_COMPLETIONS);
52+
const state = EditorState.create({ doc });
53+
const context = new CompletionContext(state, pos, false);
54+
const result = source(context);
55+
if (result == null) return null;
56+
return { from: result.from, to: result.to ?? pos };
57+
}
58+
4259
describe('Auto-Complete source', () => {
4360
it.each([
4461
{ doc: 'SELECT col', expected: ['column1', 'column2'] },
@@ -102,4 +119,32 @@ describe('Auto-Complete source', () => {
102119
expect(getSuggestionLabels('SELECT CAST')).toEqual([]);
103120
});
104121
});
122+
123+
describe('mid-identifier completion', () => {
124+
it.each([
125+
{
126+
name: "cursor before trailing ']",
127+
doc: "ResourceAttributes['host.']",
128+
pos: 25, // after 'host.'
129+
expectedRange: { from: 0, to: 27 },
130+
},
131+
{
132+
name: 'cursor in middle of a word',
133+
doc: 'SELECT column1',
134+
pos: 10, // after 'col'
135+
expectedRange: { from: 7, to: 14 },
136+
},
137+
{
138+
name: 'cursor at end of identifier (no trailing chars)',
139+
doc: 'SELECT column1',
140+
pos: 14,
141+
expectedRange: { from: 7, to: 14 },
142+
},
143+
])(
144+
'replacement range covers full identifier when $name',
145+
({ doc, pos, expectedRange }) => {
146+
expect(getReplacementRange(doc, pos)).toEqual(expectedRange);
147+
},
148+
);
149+
});
105150
});

packages/app/src/components/SQLEditor/utils.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,21 @@ export type SQLCompletion = {
1919
type?: string;
2020
};
2121

22+
// Characters that form SQL identifiers in our editor: word chars, dots,
23+
// single quotes, brackets, $, {, }, and : — to support expressions like
24+
// `ResourceAttributes['service.name']`, `$__dateFilter`, `{name:Type}`.
25+
const IDENTIFIER_CHAR = "[\\w.'[\\]${}:]";
26+
const IDENTIFIER_BEFORE = new RegExp(`${IDENTIFIER_CHAR}+`);
27+
const IDENTIFIER_AFTER = new RegExp(`^${IDENTIFIER_CHAR}+`);
28+
const IDENTIFIER_VALID_FOR = new RegExp(`^${IDENTIFIER_CHAR}*$`);
29+
2230
/**
2331
* Creates a custom CodeMirror completion source for SQL identifiers (column names, table
2432
* names, functions, etc.) that inserts them verbatim, without quoting.
2533
*/
2634
export function createIdentifierCompletionSource(completions: Completion[]) {
2735
return (context: CompletionContext) => {
28-
// Match word characters, dots, single quotes, brackets, $, {, }, and :
29-
// to support identifiers like `ResourceAttributes['service.name']`,
30-
// macros like `$__dateFilter`, and query params like `{name:Type}`
31-
const prefix = context.matchBefore(/[\w.'[\]${}:]+/);
36+
const prefix = context.matchBefore(IDENTIFIER_BEFORE);
3237
if (!prefix && !context.explicit) return null;
3338

3439
// Suppress suggestions after AS keyword since the user is typing a custom alias
@@ -37,10 +42,18 @@ export function createIdentifierCompletionSource(completions: Completion[]) {
3742
.trimEnd();
3843
if (/\bAS$/i.test(textBefore)) return null;
3944

45+
// Look forward from cursor to include trailing identifier characters
46+
// (e.g. the `']` in `ResourceAttributes['host.']`) so accepting a
47+
// suggestion replaces the entire identifier, not just up to the cursor.
48+
const docText = context.state.doc.sliceString(context.pos);
49+
const suffix = docText.match(IDENTIFIER_AFTER);
50+
const to = suffix ? context.pos + suffix[0].length : context.pos;
51+
4052
return {
4153
from: prefix?.from ?? context.pos,
54+
to,
4255
options: completions,
43-
validFor: /^[\w.'[\]${}:]*$/,
56+
validFor: IDENTIFIER_VALID_FOR,
4457
};
4558
};
4659
}

0 commit comments

Comments
 (0)