PgSQLCompletionProvider.ts231 lines · main
| 1 | import type { RefObject } from 'react' |
| 2 | |
| 3 | import BackwardIterator from './BackwardIterator' |
| 4 | import type { DatabaseFunction } from '@/data/database-functions/database-functions-query' |
| 5 | import type { Schema } from '@/data/database/schemas-query' |
| 6 | import type { TableColumn } from '@/data/database/table-columns-query' |
| 7 | |
| 8 | // [Joshen] Needs to be fixed |
| 9 | |
| 10 | export default function getPgsqlCompletionProvider(monaco: any, pgInfoRef: RefObject<any>) { |
| 11 | return { |
| 12 | triggerCharacters: [' ', '.', '"'], |
| 13 | provideCompletionItems: function (model: any, position: any, context: any) { |
| 14 | try { |
| 15 | // position.column should minus 2 as it returns 2 for first char |
| 16 | // position.lineNumber should minus 1 |
| 17 | let iterator = new BackwardIterator(model, position.column - 2, position.lineNumber - 1) |
| 18 | |
| 19 | if (context.triggerCharacter === '"') { |
| 20 | return startingQuoteScenarioSuggestions(monaco, pgInfoRef, iterator) |
| 21 | } else if (context.triggerCharacter === '.') { |
| 22 | return dotScenarioSuggestions(monaco, pgInfoRef, iterator) |
| 23 | } else { |
| 24 | return defaultScenarioSuggestions(monaco, pgInfoRef) |
| 25 | } |
| 26 | } catch (_) { |
| 27 | // any error, returns empty suggestion |
| 28 | return { suggestions: [] } |
| 29 | } |
| 30 | }, |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | function startingQuoteScenarioSuggestions(monaco: any, pgInfoRef: RefObject<any>, iterator: any) { |
| 35 | const items: any[] = [] |
| 36 | |
| 37 | let startingQuotedIdent = iterator.isFowardDQuote() |
| 38 | if (!startingQuotedIdent) return { suggestions: items } |
| 39 | |
| 40 | iterator.next() // get passed the starting quote |
| 41 | if (iterator.isNextPeriod()) { |
| 42 | // probably a field - get the ident |
| 43 | let ident = iterator.readIdent() |
| 44 | let isQuotedIdent = false |
| 45 | if (ident.match(/^\".*?\"$/)) { |
| 46 | isQuotedIdent = true |
| 47 | ident = fixQuotedIdent(ident) |
| 48 | } |
| 49 | let table = pgInfoRef.current.tableColumns.find((tbl: TableColumn) => { |
| 50 | return ( |
| 51 | (isQuotedIdent && tbl.tablename === ident) || |
| 52 | (!isQuotedIdent && tbl.tablename.toLocaleLowerCase() == ident.toLocaleLowerCase()) |
| 53 | ) |
| 54 | }) |
| 55 | |
| 56 | if (!table) return { suggestions: items } |
| 57 | table.columns.forEach((field: any) => { |
| 58 | items.push({ |
| 59 | label: field.attname, |
| 60 | kind: monaco.languages.CompletionItemKind.Property, |
| 61 | detail: field.data_type, |
| 62 | insertText: field.attname, |
| 63 | }) |
| 64 | }) |
| 65 | } else { |
| 66 | // probably a table - list the tables |
| 67 | pgInfoRef.current.tableColumns.forEach((table: TableColumn) => { |
| 68 | items.push({ |
| 69 | label: table.tablename, |
| 70 | kind: monaco.languages.CompletionItemKind.Class, |
| 71 | insertText: table.tablename, |
| 72 | }) |
| 73 | }) |
| 74 | } |
| 75 | |
| 76 | return { suggestions: items } |
| 77 | } |
| 78 | |
| 79 | function dotScenarioSuggestions(monaco: any, pgInfoRef: RefObject<any>, iterator: any) { |
| 80 | const items: any[] = [] |
| 81 | |
| 82 | let idents = readIdents(iterator, 3) |
| 83 | let pos = 0 |
| 84 | |
| 85 | let schema = pgInfoRef.current.schemas.find((sch: Schema) => { |
| 86 | const _ident = idents && idents.length > pos ? idents[pos] : {} |
| 87 | return ( |
| 88 | (_ident.isQuoted && sch.name === _ident.name) || |
| 89 | (!_ident.isQuoted && sch.name?.toLocaleLowerCase() == _ident.name?.toLocaleLowerCase()) |
| 90 | ) |
| 91 | }) |
| 92 | |
| 93 | if (!schema) { |
| 94 | schema = pgInfoRef.current.schemas.find((sch: Schema) => { |
| 95 | return sch.name == 'public' |
| 96 | }) |
| 97 | } else { |
| 98 | pos++ |
| 99 | } |
| 100 | |
| 101 | if (idents.length == pos) { |
| 102 | pgInfoRef.current.tableColumns.forEach((tbl: TableColumn) => { |
| 103 | if (tbl.schemaname != schema.name) { |
| 104 | return |
| 105 | } |
| 106 | items.push({ |
| 107 | label: tbl.tablename, |
| 108 | kind: monaco.languages.CompletionItemKind.Class, |
| 109 | detail: tbl.schemaname !== 'public' ? tbl.schemaname : null, |
| 110 | insertText: formatInsertText(tbl.tablename), |
| 111 | }) |
| 112 | }) |
| 113 | return { suggestions: items } |
| 114 | } |
| 115 | |
| 116 | let table = pgInfoRef.current.tableColumns.find((tbl: TableColumn) => { |
| 117 | const _ident = idents && idents.length > pos ? idents[pos] : {} |
| 118 | return ( |
| 119 | (tbl.schemaname == schema.name && _ident.isQuoted && tbl.tablename === _ident.name) || |
| 120 | (!_ident.isQuoted && tbl.tablename?.toLocaleLowerCase() == _ident.name?.toLocaleLowerCase()) |
| 121 | ) |
| 122 | }) |
| 123 | |
| 124 | if (table) { |
| 125 | table.columns.forEach((field: any) => { |
| 126 | items.push({ |
| 127 | label: field.attname, |
| 128 | kind: monaco.languages.CompletionItemKind.Property, |
| 129 | detail: field.data_type, |
| 130 | insertText: formatInsertText(field.attname), |
| 131 | }) |
| 132 | }) |
| 133 | } |
| 134 | |
| 135 | return { suggestions: items } |
| 136 | } |
| 137 | |
| 138 | function defaultScenarioSuggestions(monaco: any, pgInfoRef: RefObject<any>) { |
| 139 | const items: any = [] |
| 140 | |
| 141 | if (pgInfoRef.current.keywords?.length > 0) { |
| 142 | pgInfoRef.current.keywords.forEach((x: string) => { |
| 143 | items.push({ |
| 144 | label: x, |
| 145 | kind: monaco.languages.CompletionItemKind.Keyword, |
| 146 | insertText: x, |
| 147 | }) |
| 148 | }) |
| 149 | } |
| 150 | |
| 151 | if (pgInfoRef.current.schemas?.length > 0) { |
| 152 | pgInfoRef.current.schemas.forEach((x: Schema) => { |
| 153 | items.push({ |
| 154 | label: x.name, |
| 155 | kind: monaco.languages.CompletionItemKind.Keyword, |
| 156 | insertText: x.name, |
| 157 | }) |
| 158 | }) |
| 159 | } |
| 160 | |
| 161 | if (pgInfoRef.current.tableColumns?.length > 0) { |
| 162 | pgInfoRef.current.tableColumns.forEach((x: TableColumn) => { |
| 163 | const insertText = x.schemaname == 'public' ? x.tablename : x.schemaname + '.' + x.tablename |
| 164 | items.push({ |
| 165 | label: x.tablename, |
| 166 | detail: x.schemaname !== 'public' ? x.schemaname : null, |
| 167 | kind: x.is_table |
| 168 | ? monaco.languages.CompletionItemKind.Class |
| 169 | : monaco.languages.CompletionItemKind.Interface, |
| 170 | insertText: formatInsertText(insertText), |
| 171 | }) |
| 172 | x.columns.forEach((field: any) => { |
| 173 | if (!field) return |
| 174 | |
| 175 | let foundItem = items.find( |
| 176 | (i: any) => |
| 177 | i.label === field?.attname && |
| 178 | i.kind === monaco.languages.CompletionItemKind.Field && |
| 179 | i.detail === field?.data_type |
| 180 | ) |
| 181 | if (foundItem) { |
| 182 | foundItem.tables.push(x.tablename) |
| 183 | foundItem.tables.sort() |
| 184 | foundItem.documentation = foundItem.tables.join(', ') |
| 185 | } else { |
| 186 | items.push({ |
| 187 | label: field.attname, |
| 188 | kind: monaco.languages.CompletionItemKind.Field, |
| 189 | detail: field.data_type, |
| 190 | documentation: x.tablename, |
| 191 | tables: [x.tablename], |
| 192 | insertText: formatInsertText(field.attname), |
| 193 | }) |
| 194 | } |
| 195 | }) |
| 196 | }) |
| 197 | } |
| 198 | |
| 199 | if (pgInfoRef.current.functions?.length > 0) { |
| 200 | pgInfoRef.current.functions.forEach((x: DatabaseFunction) => { |
| 201 | items.push({ |
| 202 | label: x.name, |
| 203 | kind: monaco.languages.CompletionItemKind.Function, |
| 204 | detail: x.return_type, |
| 205 | insertText: x.name, |
| 206 | }) |
| 207 | }) |
| 208 | } |
| 209 | |
| 210 | return { suggestions: items } |
| 211 | } |
| 212 | |
| 213 | function fixQuotedIdent(str: string) { |
| 214 | return str.replace(/^\"/, '').replace(/\"$/, '').replace(/\"\"/, '"') |
| 215 | } |
| 216 | |
| 217 | function readIdents(iterator: any, maxlvl: number) { |
| 218 | return iterator.readIdents(maxlvl).map((name: string) => { |
| 219 | let isQuoted = false |
| 220 | if (name.match(/^\".*?\"$/)) { |
| 221 | isQuoted = true |
| 222 | name = fixQuotedIdent(name) |
| 223 | } |
| 224 | return { isQuoted: isQuoted, name: name } |
| 225 | }) |
| 226 | } |
| 227 | |
| 228 | function formatInsertText(value: string) { |
| 229 | const hasUpperCase = !(value == value.toLowerCase()) |
| 230 | return hasUpperCase ? `"${value}"` : value |
| 231 | } |