Skip to content

Commit e0340e5

Browse files
authored
Merge pull request #5 from deepnote/ss/better-empty-select-candidates
2 parents 79e6052 + 5569e60 commit e0340e5

File tree

5 files changed

+113
-49
lines changed

5 files changed

+113
-49
lines changed

packages/server/src/complete/CompletionItemUtils.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import { CompletionItem, CompletionItemKind } from 'vscode-languageserver-types'
22
import { DbFunction } from '../database_libs/AbstractClient'
33

44
export const ICONS = {
5-
KEYWORD: CompletionItemKind.Text,
6-
COLUMN: CompletionItemKind.Interface,
7-
TABLE: CompletionItemKind.Field,
8-
FUNCTION: CompletionItemKind.Property,
9-
ALIAS: CompletionItemKind.Variable,
5+
KEYWORD: CompletionItemKind.Keyword,
6+
COLUMN: CompletionItemKind.Field,
7+
TABLE: CompletionItemKind.Constant,
8+
DATABASE: CompletionItemKind.Enum,
9+
CATALOG: CompletionItemKind.Folder,
10+
FUNCTION: CompletionItemKind.Event,
11+
ALIAS: CompletionItemKind.Constant,
1012
UTILITY: CompletionItemKind.Event,
1113
}
1214

packages/server/src/complete/Identifier.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
import { CompletionItem, CompletionItemKind } from 'vscode-languageserver-types'
2-
3-
export const ICONS = {
4-
KEYWORD: CompletionItemKind.Text,
5-
COLUMN: CompletionItemKind.Interface,
6-
TABLE: CompletionItemKind.Field,
7-
FUNCTION: CompletionItemKind.Property,
8-
ALIAS: CompletionItemKind.Variable,
9-
UTILITY: CompletionItemKind.Event,
10-
}
2+
import { ICONS } from './CompletionItemUtils'
113

124
type OnClause = 'FROM' | 'ALTER TABLE' | 'OTHERS'
135
export class Identifier {
@@ -44,18 +36,37 @@ export class Identifier {
4436
toCompletionItem(): CompletionItem {
4537
const idx = this.lastToken.lastIndexOf('.')
4638
const label = this.identifier.substring(idx + 1)
47-
let kindName: string
48-
if (this.kind === ICONS.TABLE) {
39+
if (
40+
this.kind === ICONS.TABLE ||
41+
this.kind === ICONS.DATABASE ||
42+
this.kind === ICONS.CATALOG
43+
) {
4944
let tableName = label
5045
const i = tableName.lastIndexOf('.')
5146
if (i > 0) {
5247
tableName = label.substring(i + 1)
5348
}
54-
kindName = 'table'
55-
} else {
56-
kindName = 'column'
5749
}
5850

51+
const kindName = (() => {
52+
switch (this.kind) {
53+
case ICONS.TABLE:
54+
return 'table'
55+
case ICONS.DATABASE:
56+
return 'schema'
57+
case ICONS.CATALOG:
58+
return 'database'
59+
case ICONS.FUNCTION:
60+
return 'function'
61+
case ICONS.ALIAS:
62+
return 'table'
63+
case ICONS.COLUMN:
64+
return 'column'
65+
default:
66+
return 'column'
67+
}
68+
})()
69+
5970
const item: CompletionItem = {
6071
label: label,
6172
detail: `${kindName} ${this.detail}`,

packages/server/src/complete/candidates/createTableCandidates.ts

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,35 +36,51 @@ export function createCatalogDatabaseAndTableCandidates(
3636
}
3737
const qualificationLevelNeeded = qualificationNeeded - qualificationLevel
3838
switch (qualificationLevelNeeded) {
39-
case 0:
40-
return [getFullyQualifiedTableName(table)]
41-
case 1:
42-
if (table.catalog && table.database) {
43-
return [table.catalog + '.' + table.database]
44-
}
45-
if (table.database) {
46-
return [table.database]
39+
case 0: {
40+
const tableIdentifier = new Identifier(
41+
lastToken,
42+
getFullyQualifiedTableName(table),
43+
'',
44+
ICONS.TABLE,
45+
onFromClause ? 'FROM' : 'OTHERS'
46+
)
47+
return [tableIdentifier]
48+
}
49+
case 1: {
50+
const qualifiedDatabaseName =
51+
table.catalog && table.database
52+
? table.catalog + '.' + table.database
53+
: table.database
54+
55+
if (qualifiedDatabaseName !== null) {
56+
const databaseIdentifier = new Identifier(
57+
lastToken,
58+
qualifiedDatabaseName,
59+
'',
60+
ICONS.DATABASE,
61+
onFromClause ? 'FROM' : 'OTHERS'
62+
)
63+
return [databaseIdentifier]
4764
}
4865
break
66+
}
4967
case 2:
5068
if (table.catalog) {
51-
return [table.catalog]
69+
const catalogIdentifier = new Identifier(
70+
lastToken,
71+
table.catalog,
72+
'',
73+
ICONS.CATALOG,
74+
onFromClause ? 'FROM' : 'OTHERS'
75+
)
76+
return [catalogIdentifier]
5277
}
5378
break
5479
}
5580
return []
5681
})
5782

5883
return qualifiedEntities
59-
.map((databaseEntity) => {
60-
return new Identifier(
61-
lastToken,
62-
databaseEntity,
63-
'',
64-
ICONS.TABLE,
65-
onFromClause ? 'FROM' : 'OTHERS'
66-
)
67-
})
6884
.filter((item) => item.matchesLastToken())
6985
.map((item) => item.toCompletionItem())
7086
}

packages/server/src/complete/complete.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,29 @@ class Completer {
244244
this.addCandidatesForSelectStar(fromNodes, schemaAndSubqueries)
245245
const expectedLiteralNodes =
246246
e.expected?.filter(
247-
(v): v is ExpectedLiteralNode => v.type === 'literal'
247+
(v): v is ExpectedLiteralNode =>
248+
v.type === 'literal' && hasAtLeastTwoLetters(v.text)
248249
) || []
249250
this.addCandidatesForExpectedLiterals(expectedLiteralNodes)
250251
this.addCandidatesForFunctions()
251-
this.addCandidatesForScopedColumns(fromNodes, schemaAndSubqueries)
252+
253+
const { addedSome: addedSomeScopedColumnCandidates } =
254+
this.addCandidatesForScopedColumns(fromNodes, schemaAndSubqueries)
255+
if (!addedSomeScopedColumnCandidates) {
256+
this.addCandidatesForUnscopedColumns(fromNodes, schemaAndSubqueries)
257+
}
258+
252259
this.addCandidatesForAliases(fromNodes)
253-
this.addCandidatesForTables(schemaAndSubqueries, true)
260+
261+
const fromNodesContainingCursor = fromNodes.filter((tableNode) =>
262+
isPosInLocation(tableNode.location, this.pos)
263+
)
264+
const isCursorInsideFromClause = fromNodesContainingCursor.length > 0
265+
if (isCursorInsideFromClause) {
266+
// only add table candidates if the cursor is inside a FROM clause or JOIN clause, etc.
267+
this.addCandidatesForTables(schemaAndSubqueries, true)
268+
}
269+
254270
if (logger.isDebugEnabled())
255271
logger.debug(
256272
`candidates for error returns: ${JSON.stringify(this.candidates)}`
@@ -378,14 +394,20 @@ class Completer {
378394
console.timeEnd('addCandidatesForSelectStar')
379395
}
380396

381-
addCandidatesForScopedColumns(fromNodes: FromTableNode[], tables: Table[]) {
397+
addCandidatesForScopedColumns(
398+
fromNodes: FromTableNode[],
399+
tables: Table[]
400+
): { addedSome: boolean } {
382401
console.time('addCandidatesForScopedColumns')
402+
let addedSome = false
383403
createCandidatesForScopedColumns(fromNodes, tables, this.lastToken).forEach(
384404
(v) => {
405+
addedSome = true
385406
this.addCandidate(v)
386407
}
387408
)
388409
console.timeEnd('addCandidatesForScopedColumns')
410+
return { addedSome }
389411
}
390412

391413
addCandidatesForUnscopedColumns(fromNodes: FromTableNode[], tables: Table[]) {
@@ -419,3 +441,7 @@ export function complete(
419441
console.timeEnd('complete')
420442
return { candidates: candidates, error: completer.error }
421443
}
444+
445+
function hasAtLeastTwoLetters(value: string): boolean {
446+
return /[a-zA-Z].*[a-zA-Z]/.test(value)
447+
}

packages/server/test/complete.test.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -174,14 +174,24 @@ describe('on blank space', () => {
174174

175175
test('complete inside SELECT', () => {
176176
const result = complete('SELECT ', { line: 0, column: 7 }, SIMPLE_SCHEMA)
177-
expect(result.candidates.length).toEqual(12) // TODO whare are they?
178177
const expected = [
179178
expect.objectContaining({ label: 'array_concat()' }),
180179
expect.objectContaining({ label: 'array_contains()' }),
181180
]
182181
expect(result.candidates).toEqual(expect.arrayContaining(expected))
183182
})
184183

184+
test('complete after SELECT FROM schema2.table2', () => {
185+
const result = complete(
186+
'SELECT FROM schema2.table2',
187+
{ line: 0, column: 8 },
188+
SIMPLE_NESTED_SCHEMA
189+
)
190+
expect(result.candidates).not.toContainEqual(
191+
expect.objectContaining({ label: 'TABLE1' })
192+
)
193+
})
194+
185195
test('complete function inside WHERE select star', () => {
186196
const result = complete(
187197
'SELECT * FROM tab1 WHERE arr',
@@ -450,7 +460,6 @@ describe('Fully qualified table names', () => {
450460
{ line: 0, column: 31 },
451461
SIMPLE_NESTED_SCHEMA
452462
)
453-
expect(result.candidates.length).toEqual(1)
454463
const expected = [expect.objectContaining({ label: 'table3' })]
455464
expect(result.candidates).toEqual(expect.arrayContaining(expected))
456465
})
@@ -880,18 +889,18 @@ test('complete aliased column inside function', () => {
880889
expect(result.candidates[0].label).toEqual('department_id')
881890
})
882891

883-
test('complete column inside function', () => {
884-
const sql = `SELECT TO_CHAR(empl, 'MM/DD/YYYY') FROM employees x`
892+
test('complete table inside function', () => {
893+
const sql = `SELECT TO_CHAR(empl, 'MM/DD/YYYY') FROM employees`
885894
const result = complete(sql, { line: 0, column: 19 }, COMPLEX_SCHEMA)
886-
expect(result.candidates.length).toEqual(1)
887-
expect(result.candidates[0].label).toEqual('employees')
895+
const expected = [expect.objectContaining({ label: 'employees' })]
896+
expect(result.candidates).toEqual(expect.arrayContaining(expected))
888897
})
889898

890899
test('complete an alias inside function', () => {
891900
const sql = `SELECT TO_CHAR(an_ali, 'MM/DD/YYYY') FROM employees an_alias`
892901
const result = complete(sql, { line: 0, column: 21 }, COMPLEX_SCHEMA)
893-
expect(result.candidates.length).toEqual(1)
894-
expect(result.candidates[0].label).toEqual('an_alias')
902+
const expected = [expect.objectContaining({ label: 'an_alias' })]
903+
expect(result.candidates).toEqual(expect.arrayContaining(expected))
895904
})
896905

897906
describe('From clause subquery', () => {

0 commit comments

Comments
 (0)