Skip to content

Commit

Permalink
Automatically quote identifiers with non-word characters in completions
Browse files Browse the repository at this point in the history
FIX: `schemaCompletionSource` now adds quotes around non-word identifiers
even if the user didn't type a starting quote.

See https://discuss.codemirror.net/t/quoting-table-names-when-using-schemacompletionsource/6721
  • Loading branch information
marijnh committed Jun 21, 2023
1 parent a7ce220 commit ca951ee
Show file tree
Hide file tree
Showing 3 changed files with 24 additions and 8 deletions.
22 changes: 15 additions & 7 deletions src/complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {EditorState, Text} from "@codemirror/state"
import {syntaxTree} from "@codemirror/language"
import {SyntaxNode} from "@lezer/common"
import {Type, Keyword} from "./sql.grammar.terms"
import {type SQLDialect} from "./sql"

function tokenBefore(tree: SyntaxNode) {
let cursor = tree.cursor().moveTo(tree.from, -1)
Expand Down Expand Up @@ -101,29 +102,36 @@ class CompletionLevel {
return children[name] || (children[name] = new CompletionLevel)
}

childCompletions(type: string) {
return this.children ? Object.keys(this.children).filter(x => x).map(name => ({label: name, type} as Completion)) : []
childCompletions(type: string, idQuote: string) {
return this.children ? Object.keys(this.children).filter(x => x).map(name => nameCompletion(name, type, idQuote)) : []
}
}

function nameCompletion(label: string, type: string, idQuote: string): Completion {
if (!/[^\w\xb5-\uffff]/.test(label)) return {label, type}
return {label, type, apply: idQuote + label + idQuote}
}

export function completeFromSchema(schema: {[table: string]: readonly (string | Completion)[]},
tables?: readonly Completion[], schemas?: readonly Completion[],
defaultTableName?: string, defaultSchemaName?: string): CompletionSource {
defaultTableName?: string, defaultSchemaName?: string,
dialect?: SQLDialect): CompletionSource {
let top = new CompletionLevel
let defaultSchema = top.child(defaultSchemaName || "")
let idQuote = dialect?.spec.identifierQuotes?.[0] || '"'
for (let table in schema) {
let dot = table.indexOf(".")
let schemaCompletions = dot > -1 ? top.child(table.slice(0, dot)) : defaultSchema
let tableCompletions = schemaCompletions.child(dot > -1 ? table.slice(dot + 1) : table)
tableCompletions.list = schema[table].map(val => typeof val == "string" ? {label: val, type: "property"} : val)
tableCompletions.list = schema[table].map(val => typeof val == "string" ? nameCompletion(val, "property", idQuote) : val)
}
defaultSchema.list = (tables || defaultSchema.childCompletions("type"))
defaultSchema.list = (tables || defaultSchema.childCompletions("type", idQuote))
.concat(defaultTableName ? defaultSchema.child(defaultTableName).list : [])
for (let sName in top.children) {
let schema = top.child(sName)
if (!schema.list.length) schema.list = schema.childCompletions("type")
if (!schema.list.length) schema.list = schema.childCompletions("type", idQuote)
}
top.list = defaultSchema.list.concat(schemas || top.childCompletions("type"))
top.list = defaultSchema.list.concat(schemas || top.childCompletions("type", idQuote))

return (context: CompletionContext) => {
let {parents, from, quoted, empty, aliases} = sourceContext(context.state, context.pos)
Expand Down
3 changes: 2 additions & 1 deletion src/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ export function keywordCompletion(dialect: SQLDialect, upperCase = false): Exten
/// for the given configuration.
export function schemaCompletionSource(config: SQLConfig): CompletionSource {
return config.schema ? completeFromSchema(config.schema, config.tables, config.schemas,
config.defaultTable, config.defaultSchema)
config.defaultTable, config.defaultSchema,
config.dialect || StandardSQL)
: () => null
}

Expand Down
7 changes: 7 additions & 0 deletions test/test-complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,11 @@ describe("SQL completion", () => {
it("does complete explicitly without identifier", () => {
ist(str(get("select |", {schema: schema1, explicit: true})), "products, users")
})

it("adds identifiers for non-word completions", () => {
ist(get("foo.b|", {schema: {foo: ["b c", "b-c", "bup"]}, dialect: PostgreSQL})!
.options.map(o => o.apply || o.label).join(), '"b c","b-c",bup')
ist(get("foo.b|", {schema: {foo: ["b c", "b-c", "bup"]}, dialect: MySQL})!
.options.map(o => o.apply || o.label).join(), '`b c`,`b-c`,bup')
})
})

0 comments on commit ca951ee

Please sign in to comment.