PgSQLCompletionProvider.ts 7.0 KB

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