Skip to content

Commit 4f01b12

Browse files
authored
fix: autocomplete table names after FROM/JOIN keywords (#20)
Table names were not being suggested when the cursor was positioned right after FROM or JOIN keywords with no table typed yet. This adds detection for cursor position after these keywords using the parsed FROM clause's `after` field. Supports all JOIN types: INNER, LEFT, RIGHT, FULL, FULL OUTER, CROSS, NATURAL, and plain JOIN.
1 parent eb6ba0f commit 4f01b12

File tree

2 files changed

+100
-4
lines changed

2 files changed

+100
-4
lines changed

packages/server/src/complete/complete.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class Completer {
111111
// Incomplete sub query 'SELECT sub FROM (SELECT e. FROM employees e) sub'
112112
this.addCandidatesForIncompleteSubquery(fromNodeOnCursor)
113113
} else {
114-
this.addCandidatesForSelectQuery(e, fromNodes)
114+
this.addCandidatesForSelectQuery(e, fromNodes, parsedFromClause)
115115
const expectedLiteralNodes =
116116
e.expected?.filter(
117117
(v): v is ExpectedLiteralNode => v.type === 'literal'
@@ -239,7 +239,11 @@ class Completer {
239239
this.addCandidatesForTables(this.schema.tables, false)
240240
}
241241

242-
addCandidatesForSelectQuery(e: ParseError, fromNodes: FromTableNode[]) {
242+
addCandidatesForSelectQuery(
243+
e: ParseError,
244+
fromNodes: FromTableNode[],
245+
parsedFromClause: FromClauseParserResult
246+
) {
243247
const subqueryTables = createTablesFromFromNodes(fromNodes)
244248
const schemaAndSubqueries = this.schema.tables.concat(subqueryTables)
245249
this.addCandidatesForSelectStar(fromNodes, schemaAndSubqueries)
@@ -263,8 +267,19 @@ class Completer {
263267
isPosInLocation(tableNode.location, this.pos)
264268
)
265269
const isCursorInsideFromClause = fromNodesContainingCursor.length > 0
266-
if (isCursorInsideFromClause) {
267-
// only add table candidates if the cursor is inside a FROM clause or JOIN clause, etc.
270+
271+
// Check if cursor is right after FROM keyword (no table typed yet)
272+
const afterFromClause = parsedFromClause.after?.trim().toUpperCase() || ''
273+
const isCursorAfterFromKeyword =
274+
afterFromClause === 'FROM' ||
275+
afterFromClause.startsWith('FROM ') ||
276+
/^(INNER |LEFT |RIGHT |FULL |FULL OUTER |CROSS |NATURAL |OUTER )?JOIN( |$)/.test(
277+
afterFromClause
278+
)
279+
280+
if (isCursorInsideFromClause || isCursorAfterFromKeyword) {
281+
// add table candidates if the cursor is inside a FROM clause, JOIN clause,
282+
// or right after FROM/JOIN keyword waiting for a table name
268283
this.addCandidatesForTables(schemaAndSubqueries, true)
269284
}
270285

packages/server/test/complete.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,24 @@ describe('From clause', () => {
241241
expect(result.candidates[0].insertText).toEqual('TABLE1')
242242
})
243243

244+
test('from clause: complete TableName after FROM keyword with space', () => {
245+
const result = complete(
246+
'SELECT * FROM ',
247+
{ line: 0, column: 14 },
248+
SIMPLE_SCHEMA
249+
)
250+
expect(result.candidates.map((v) => v.label)).toContain('TABLE1')
251+
})
252+
253+
test('from clause: complete TableName after FROM keyword with space:multi line', () => {
254+
const result = complete(
255+
'SELECT *\nFROM ',
256+
{ line: 1, column: 5 },
257+
SIMPLE_SCHEMA
258+
)
259+
expect(result.candidates.map((v) => v.label)).toContain('TABLE1')
260+
})
261+
244262
test('from clause: complete TableName:multi lines', () => {
245263
const result = complete(
246264
'SELECT TABLE1.COLUMN1\nFROM T',
@@ -277,6 +295,69 @@ describe('From clause', () => {
277295
)
278296
expect(result.candidates.map((v) => v.label)).toContain('ON')
279297
})
298+
299+
test('from clause: complete TableName after JOIN keyword with space', () => {
300+
const result = complete(
301+
'SELECT * FROM TABLE1 JOIN ',
302+
{ line: 0, column: 26 },
303+
SIMPLE_SCHEMA
304+
)
305+
expect(result.candidates.map((v) => v.label)).toContain('TABLE1')
306+
})
307+
308+
test('from clause: complete TableName after INNER JOIN keyword with space', () => {
309+
const result = complete(
310+
'SELECT * FROM TABLE1 INNER JOIN ',
311+
{ line: 0, column: 32 },
312+
SIMPLE_SCHEMA
313+
)
314+
expect(result.candidates.map((v) => v.label)).toContain('TABLE1')
315+
})
316+
317+
test('from clause: complete TableName after LEFT JOIN keyword with space', () => {
318+
const result = complete(
319+
'SELECT * FROM TABLE1 LEFT JOIN ',
320+
{ line: 0, column: 31 },
321+
SIMPLE_SCHEMA
322+
)
323+
expect(result.candidates.map((v) => v.label)).toContain('TABLE1')
324+
})
325+
326+
test('from clause: complete TableName after RIGHT JOIN keyword with space', () => {
327+
const result = complete(
328+
'SELECT * FROM TABLE1 RIGHT JOIN ',
329+
{ line: 0, column: 32 },
330+
SIMPLE_SCHEMA
331+
)
332+
expect(result.candidates.map((v) => v.label)).toContain('TABLE1')
333+
})
334+
335+
test('from clause: complete TableName after CROSS JOIN keyword with space', () => {
336+
const result = complete(
337+
'SELECT * FROM TABLE1 CROSS JOIN ',
338+
{ line: 0, column: 32 },
339+
SIMPLE_SCHEMA
340+
)
341+
expect(result.candidates.map((v) => v.label)).toContain('TABLE1')
342+
})
343+
344+
test('from clause: complete TableName after FULL OUTER JOIN keyword with space', () => {
345+
const result = complete(
346+
'SELECT * FROM TABLE1 FULL OUTER JOIN ',
347+
{ line: 0, column: 37 },
348+
SIMPLE_SCHEMA
349+
)
350+
expect(result.candidates.map((v) => v.label)).toContain('TABLE1')
351+
})
352+
353+
test('from clause: complete TableName after NATURAL JOIN keyword with space', () => {
354+
const result = complete(
355+
'SELECT * FROM TABLE1 NATURAL JOIN ',
356+
{ line: 0, column: 34 },
357+
SIMPLE_SCHEMA
358+
)
359+
expect(result.candidates.map((v) => v.label)).toContain('TABLE1')
360+
})
280361
})
281362

282363
describe('Where clause', () => {

0 commit comments

Comments
 (0)