/* Copyright 2026 Marimo. All rights reserved. */ import type { Completion, CompletionSource } from "@codemirror/autocomplete"; import { keywordCompletionSource, schemaCompletionSource, } from "@codemirror/lang-sql"; import type { EditorState } from "@codemirror/state"; import { DefaultSqlTooltipRenders } from "@marimo-team/codemirror-sql"; import { once } from "@/utils/once"; import { languageMetadataField } from "../../metadata"; import { SCHEMA_CACHE } from "./completion-store"; import type { SQLLanguageAdapterMetadata } from "./sql"; function getSQLMetadata(state: EditorState): SQLLanguageAdapterMetadata { return state.field(languageMetadataField) as SQLLanguageAdapterMetadata; } /** * Custom schema completion source that dynamically gets the Dialect and SQL tables. */ export function tablesCompletionSource(): CompletionSource { return (ctx) => { const metadata = getSQLMetadata(ctx.state); const connectionName = metadata.engine; const config = SCHEMA_CACHE.getCompletionSource(connectionName); if (!config) { return null; } const completions = schemaCompletionSource(config)(ctx); if (!completions) { return null; } return completions; }; } /** * Custom keyword completion source that dynamically gets the Dialect. * This also ignores keyword completions on table columns. */ export function customKeywordCompletionSource(): CompletionSource { return (ctx) => { const metadata = getSQLMetadata(ctx.state); const connectionName = metadata.engine; const dialect = SCHEMA_CACHE.getDialect(connectionName); // We want to ignore keyword completions on something like // `WHERE my_table.col` // ^cursor const textBefore = ctx.matchBefore(/\.\w*/); if (textBefore) { // If there is a match, we are typing after a dot, // so we don't want to trigger SQL keyword completion return null; } const keywordRenderer = (label: string, type: string): Completion => { return { label, type, info: async () => { const keywordDocs = await getKeywordDocs(); const keywordInfo = keywordDocs[label.toLocaleLowerCase()]; if (!keywordInfo) { return null; } const dom = document.createElement("div"); dom.innerHTML = DefaultSqlTooltipRenders.keyword({ keyword: label, info: keywordInfo, }); return dom; }, }; }; const uppercaseKeywords = true; const result = keywordCompletionSource( dialect, uppercaseKeywords, keywordRenderer, )(ctx); return result; }; } // e.g. lazily load keyword docs const getKeywordDocs = once(async (): Promise> => { const keywords = await import("@marimo-team/codemirror-sql/data/common-keywords.json"); // Include DuckDB for now, but we can remove this once we have a better way to handle dialect-specific keywords const duckdbKeywords = await import("@marimo-team/codemirror-sql/data/duckdb-keywords.json"); return { ...keywords.default.keywords, ...duckdbKeywords.default.keywords, }; });