From fdebab01e8c7576154c91e07c76f4cf9aefa9b3f Mon Sep 17 00:00:00 2001 From: danielhe4rt Date: Thu, 31 Oct 2024 16:05:06 -0300 Subject: [PATCH 1/4] feat: cql autocompleter with syntax highlighter --- .../_components/cql-autocompleter.ts | 283 +++++++++++++++ .../query-runner/_components/cql-editor.tsx | 57 ++- .../query-runner/_components/cql-language.ts | 341 ++++++++++++++++++ .../(main)/query-runner/_components/utils.tsx | 1 + 4 files changed, 680 insertions(+), 2 deletions(-) create mode 100644 src/app/(main)/query-runner/_components/cql-autocompleter.ts create mode 100644 src/app/(main)/query-runner/_components/cql-language.ts diff --git a/src/app/(main)/query-runner/_components/cql-autocompleter.ts b/src/app/(main)/query-runner/_components/cql-autocompleter.ts new file mode 100644 index 0000000..1c8f1bd --- /dev/null +++ b/src/app/(main)/query-runner/_components/cql-autocompleter.ts @@ -0,0 +1,283 @@ +import * as monaco from "monaco-editor"; + +// Define CQL keywords, functions, data types, and operators +const keywords = [ + "ADD", + "AGGREGATE", + "ALL", + "ALLOW", + "ALTER", + "AND", + "ANY", + "APPLY", + "AS", + "ASC", + "AUTHORIZE", + "BATCH", + "BEGIN", + "BY", + "CAST", + "COLUMNFAMILY", + "CREATE", + "CUSTOM", + "DELETE", + "DESC", + "DISTINCT", + "DROP", + "ENTRIES", + "EXISTS", + "FILTERING", + "FROM", + "FULL", + "FUNCTION", + "GRANT", + "IF", + "IN", + "INDEX", + "INFINITY", + "INSERT", + "INTO", + "IS", + "JSON", + "KEY", + "KEYSPACE", + "KEYSPACES", + "LANGUAGE", + "LIMIT", + "LOGIN", + "MATERIALIZED", + "MODIFY", + "NAN", + "NAMESPACE", + "NORECURSIVE", + "NOT", + "NULL", + "OF", + "ON", + "OPTIONS", + "OR", + "ORDER", + "PARTITION", + "PASSWORD", + "PER", + "PERMISSION", + "PERMISSIONS", + "PRIMARY", + "REVOKE", + "SCHEMA", + "SELECT", + "SET", + "SFUNCTION", + "STATIC", + "STORAGE", + "SUPERUSER", + "TABLE", + "TOKEN", + "TRUNCATE", + "TTL", + "TYPE", + "UNLOGGED", + "UPDATE", + "USE", + "USER", + "USERS", + "USING", + "VALUES", + "VIEW", + "WHERE", + "WITH", + "WRITETIME", +]; + +const functions = [ + "blobAsBigint", + "blobAsBoolean", + "blobAsDecimal", + "blobAsDouble", + "blobAsFloat", + "blobAsInt", + "blobAsText", + "blobAsTimestamp", + "blobAsUUID", + "blobAsVarchar", + "blobAsVarint", + "dateOf", + "now", + "unixTimestampOf", + "uuid", + "timeuuid", + "minTimeuuid", + "maxTimeuuid", + "toDate", + "toTimestamp", + "toUnixTimestamp", + "writetime", + "ttl", + "token", + "bigintAsBlob", + "booleanAsBlob", + "dateAsBlob", + "decimalAsBlob", + "doubleAsBlob", + "floatAsBlob", + "inetAsBlob", + "intAsBlob", + "textAsBlob", + "timestampAsBlob", + "timeuuidAsBlob", + "uuidAsBlob", + "varcharAsBlob", + "varintAsBlob", +]; + +const dataTypes = [ + "ascii", + "bigint", + "blob", + "boolean", + "counter", + "date", + "decimal", + "double", + "float", + "frozen", + "inet", + "int", + "list", + "map", + "set", + "smallint", + "text", + "time", + "timestamp", + "timeuuid", + "tinyint", + "tuple", + "uuid", + "varchar", + "varint", +]; + +const operators = [ + "=", + ">", + "<", + ">=", + "<=", + "!=", + "+", + "-", + "*", + "/", + "%", + "IN", + "CONTAINS", + "CONTAINS KEY", + "AND", + "OR", + "NOT", + "IS", + "NULL", + "IS NOT NULL", + "IS NULL", +]; + +// Helper function to create completion items +function createCompletionItem( + label: string, + kind: monaco.languages.CompletionItemKind, + documentation?: string, + insertText?: string, +): monaco.languages.CompletionItem { + return { + label, + kind, + documentation, + range: { + startLineNumber: 0, + startColumn: 0, + endLineNumber: 0, + endColumn: 0, + }, + insertText: insertText || label, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + }; +} + +// Build completion items +const completionItems: monaco.languages.CompletionItem[] = []; + +// Add keywords to completion items +keywords.forEach((keyword) => { + completionItems.push( + createCompletionItem( + keyword, + monaco.languages.CompletionItemKind.Keyword, + "Keyword", + ), + ); +}); + +// Add functions to completion items +functions.forEach((func) => { + completionItems.push( + createCompletionItem( + func, + monaco.languages.CompletionItemKind.Function, + "Function", + func + "($0)", + ), + ); +}); + +// Add data types to completion items +dataTypes.forEach((type) => { + completionItems.push( + createCompletionItem( + type, + monaco.languages.CompletionItemKind.TypeParameter, + "Data Type", + ), + ); +}); + +// Add operators to completion items +operators.forEach((operator) => { + completionItems.push( + createCompletionItem( + operator, + monaco.languages.CompletionItemKind.Operator, + "Operator", + ), + ); +}); + +// Implement the CompletionItemProvider +export const cqlCompletionItemProvider: monaco.languages.CompletionItemProvider = + { + triggerCharacters: [" ", ".", "(", ")"], + provideCompletionItems: ( + model: monaco.editor.ITextModel, + position: monaco.Position, + context: monaco.languages.CompletionContext, + token: monaco.CancellationToken, + ) => { + const word = model.getWordUntilPosition(position); + const range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn, + }; + + // Filter suggestions based on the current word + const suggestions = completionItems + .filter((item) => + item.label.toLowerCase().startsWith(word.word.toLowerCase()), + ) + .map((item) => ({ ...item, range })); + + return { suggestions }; + }, + }; diff --git a/src/app/(main)/query-runner/_components/cql-editor.tsx b/src/app/(main)/query-runner/_components/cql-editor.tsx index 34686e5..d26c5d6 100644 --- a/src/app/(main)/query-runner/_components/cql-editor.tsx +++ b/src/app/(main)/query-runner/_components/cql-editor.tsx @@ -2,6 +2,8 @@ import Editor, { type Monaco, useMonaco } from "@monaco-editor/react"; import { executeQueryAction } from "@scylla-studio/actions/execute-query"; +import { cqlCompletionItemProvider } from "@scylla-studio/app/(main)/query-runner/_components/cql-autocompleter"; +import { cql_language } from "@scylla-studio/app/(main)/query-runner/_components/cql-language"; import { DropdownMenu, DropdownMenuContent, @@ -21,7 +23,13 @@ import type { TracingResult } from "@scylla-studio/lib/execute-query"; import { cn } from "@scylla-studio/lib/utils"; import debounce from "lodash.debounce"; import { Braces, ChartArea, Play, SearchCode } from "lucide-react"; -import type { editor } from "monaco-editor"; +import { + CancellationToken, + CompletionItem, + Position, + editor, + languages, +} from "monaco-editor"; import { useAction } from "next-safe-action/hooks"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; @@ -60,6 +68,7 @@ export function CqlEditor() { const fetchSizeReference = useRef(null); const monaco = useMonaco(); + const editorReference = useRef(null); const decorationsReference = useRef(null); @@ -168,6 +177,50 @@ export function CqlEditor() { editor: editor.IStandaloneCodeEditor, monaco: Monaco, ) => { + monaco.languages.register({ + id: "cql", + extensions: [".cql"], + aliases: ["CQL", "cql"], + mimetypes: ["text/cql"], + }); + + monaco.languages.setMonarchTokensProvider("cql", cql_language); + // monaco.languages.registerCompletionItemProvider("cql", { + // triggerCharacters: [' ', '.', '(', ')'], + // provideCompletionItems(model: editor.ITextModel, position: Position, context: languages.CompletionContext, token: CancellationToken): languages.ProviderResult { + // + // const completionItems: CompletionItem[] = [ + // { + // label: 'SELECT', + // kind: languages.CompletionItemKind.Keyword, + // documentation: 'Select data from a table', + // range: { + // startLineNumber: 0, + // startColumn: 0, + // endLineNumber: 0, + // endColumn: 0 + // }, + // insertText: 'SELECT', + // insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet + // } + // + // ]; + // return {suggestions: completionItems}; + // + // const word = model.getWordUntilPosition(position); + // const range = { + // startLineNumber: position.lineNumber, + // endLineNumber: position.lineNumber, + // startColumn: word.startColumn, + // endColumn: word.endColumn + // }; + // const suggestions = completionItems + // .filter((item) => item.label.toLowerCase().startsWith(word.word.toLowerCase())) + // .map((item) => ({ ...item, range })); + // return { suggestions }; + // } + // }); + editorReference.current = editor; // Add event listener to highlight the query whenever the cursor position changes @@ -278,7 +331,7 @@ export function CqlEditor() { { + defaultToken: "", + tokenPostfix: ".cql", + ignoreCase: true, + + brackets: [ + { open: "[", close: "]", token: TokenClassConsts.DELIMITER_SQUARE }, + { open: "(", close: ")", token: TokenClassConsts.DELIMITER_PAREN }, + { open: "{", close: "}", token: TokenClassConsts.DELIMITER_CURLY }, + ], + + keywords: [ + "ADD", + "AGGREGATE", + "ALL", + "ALLOW", + "ALTER", + "AND", + "ANY", + "APPLY", + "AS", + "ASC", + "AUTHORIZE", + "BATCH", + "BEGIN", + "BY", + "CAST", + "COLUMNFAMILY", + "CREATE", + "CUSTOM", + "DELETE", + "DESC", + "DISTINCT", + "DROP", + "ENTRIES", + "EXISTS", + "FILTERING", + "FROM", + "FULL", + "FUNCTION", + "GRANT", + "IF", + "IN", + "INDEX", + "INFINITY", + "INSERT", + "INTO", + "IS", + "JSON", + "KEY", + "KEYSPACE", + "KEYSPACES", + "LANGUAGE", + "LIMIT", + "LOGIN", + "MATERIALIZED", + "MODIFY", + "NAN", + "NAMESPACE", + "NORECURSIVE", + "NOT", + "NULL", + "OF", + "ON", + "OPTIONS", + "OR", + "ORDER", + "PARTITION", + "PASSWORD", + "PER", + "PERMISSION", + "PERMISSIONS", + "PRIMARY", + "REVOKE", + "SCHEMA", + "SELECT", + "SET", + "SFUNCTION", + "STATIC", + "STORAGE", + "SUPERUSER", + "TABLE", + "TOKEN", + "TRUNCATE", + "TTL", + "TYPE", + "UNLOGGED", + "UPDATE", + "USE", + "USER", + "USERS", + "USING", + "VALUES", + "VIEW", + "WHERE", + "WITH", + "WRITETIME", + ], + + operators: [ + "=", + ">", + "<", + ">=", + "<=", + "!=", + "+", + "-", + "*", + "/", + "%", + "IN", + "CONTAINS", + "CONTAINS KEY", + "AND", + "OR", + "NOT", + "IS", + "NULL", + "IS NOT NULL", + "IS NULL", + ], + + builtinFunctions: [ + "blobAsBigint", + "blobAsBoolean", + "blobAsDecimal", + "blobAsDouble", + "blobAsFloat", + "blobAsInt", + "blobAsText", + "blobAsTimestamp", + "blobAsUUID", + "blobAsVarchar", + "blobAsVarint", + "dateOf", + "now", + "unixTimestampOf", + "uuid", + "timeuuid", + "minTimeuuid", + "maxTimeuuid", + "toDate", + "toTimestamp", + "toUnixTimestamp", + "writetime", + "ttl", + "token", + "bigintAsBlob", + "booleanAsBlob", + "dateAsBlob", + "decimalAsBlob", + "doubleAsBlob", + "floatAsBlob", + "inetAsBlob", + "intAsBlob", + "textAsBlob", + "timestampAsBlob", + "timeuuidAsBlob", + "uuidAsBlob", + "varcharAsBlob", + "varintAsBlob", + ], + + builtinVariables: [], + + typeKeywords: [ + "ascii", + "bigint", + "blob", + "boolean", + "counter", + "date", + "decimal", + "double", + "float", + "frozen", + "inet", + "int", + "list", + "map", + "set", + "smallint", + "text", + "time", + "timestamp", + "timeuuid", + "tinyint", + "tuple", + "uuid", + "varchar", + "varint", + ], + + scopeKeywords: ["BEGIN", "APPLY", "BATCH", "IF", "ELSE"], + + pseudoColumns: [], + + tokenizer: { + root: [ + { include: "@comments" }, + { include: "@whitespace" }, + { include: "@numbers" }, + { include: "@strings" }, + { include: "@complexIdentifiers" }, + { include: "@scopes" }, + { include: "@complexOperators" }, + [/[;,.]/, TokenClassConsts.DELIMITER], + [/[\(\)\[\]\{\}]/, "@brackets"], + [ + /[\w@]+/, + { + cases: { + "@scopeKeywords": TokenClassConsts.KEYWORD_SCOPE, + "@operators": TokenClassConsts.OPERATOR_KEYWORD, + "@typeKeywords": TokenClassConsts.TYPE, + "@builtinVariables": TokenClassConsts.VARIABLE, + "@builtinFunctions": TokenClassConsts.PREDEFINED, + "@keywords": TokenClassConsts.KEYWORD, + "@default": TokenClassConsts.IDENTIFIER, + }, + }, + ], + [/[<>=!%&+\-*/|~^]/, TokenClassConsts.OPERATOR_SYMBOL], + ], + comments: [ + [/--+.*/, TokenClassConsts.COMMENT], + [/\/\*/, { token: TokenClassConsts.COMMENT_QUOTE, next: "@comment" }], + ], + whitespace: [[/\s+/, TokenClassConsts.WHITE]], + comment: [ + [/[^*/]+/, TokenClassConsts.COMMENT], + [/\*\//, { token: TokenClassConsts.COMMENT_QUOTE, next: "@pop" }], + [/./, TokenClassConsts.COMMENT], + ], + numbers: [ + [/0[xX][0-9a-fA-F]+/, TokenClassConsts.NUMBER_HEX], + [/[$][+-]*\d*(\.\d*)?/, TokenClassConsts.NUMBER], + [/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/, TokenClassConsts.NUMBER], + ], + strings: [ + [/'/, { token: TokenClassConsts.STRING, next: "@stringSingle" }], + [/"/, { token: TokenClassConsts.STRING, next: "@stringDouble" }], + ], + stringSingle: [ + [/\\'/, TokenClassConsts.STRING_ESCAPE], + [/[^']+/, TokenClassConsts.STRING], + [/''/, TokenClassConsts.STRING], + [/'/, { token: TokenClassConsts.STRING, next: "@pop" }], + ], + stringDouble: [ + [/[^"]+/, TokenClassConsts.STRING], + [/""/, TokenClassConsts.STRING], + [/"/, { token: TokenClassConsts.STRING, next: "@pop" }], + ], + complexIdentifiers: [ + [ + /"/, + { + token: TokenClassConsts.IDENTIFIER_QUOTE, + next: "@quotedIdentifierDouble", + }, + ], + ], + quotedIdentifierDouble: [ + [/[^"]+/, TokenClassConsts.IDENTIFIER], + [/""/, TokenClassConsts.IDENTIFIER], + [/"/, { token: TokenClassConsts.IDENTIFIER_QUOTE, next: "@pop" }], + ], + scopes: [], + complexOperators: [ + [/IS\s+NOT\s+NULL\b/i, { token: TokenClassConsts.OPERATOR_KEYWORD }], + [/IS\s+NULL\b/i, { token: TokenClassConsts.OPERATOR_KEYWORD }], + [ + /NOT\s+(IN|CONTAINS|CONTAINS\s+KEY)\b/i, + { token: TokenClassConsts.OPERATOR_KEYWORD }, + ], + [/CONTAINS\s+KEY\b/i, { token: TokenClassConsts.OPERATOR_KEYWORD }], + [/CONTAINS\b/i, { token: TokenClassConsts.OPERATOR_KEYWORD }], + [/ALLOW\s+FILTERING\b/i, { token: TokenClassConsts.KEYWORD }], + ], + }, +}; diff --git a/src/app/(main)/query-runner/_components/utils.tsx b/src/app/(main)/query-runner/_components/utils.tsx index 8a73525..e2c610a 100644 --- a/src/app/(main)/query-runner/_components/utils.tsx +++ b/src/app/(main)/query-runner/_components/utils.tsx @@ -21,6 +21,7 @@ export const getFullQueryAtCursor = ( let endLineIndex = position.lineNumber; while (model.getLineCount() >= endLineIndex) { const currentLine = model.getLineContent(endLineIndex); + if (currentLine.includes(";")) { found = true; break; From 6ca3a253ab2289fcb2fac259b355314fe7a3977d Mon Sep 17 00:00:00 2001 From: Gabriel do Carmo Vieira <48625433+gvieira18@users.noreply.github.com> Date: Thu, 31 Oct 2024 19:19:44 -0300 Subject: [PATCH 2/4] fix: generate cql autocomplete --- .../_components/cql-autocompleter.ts | 134 +++++------------- .../query-runner/_components/cql-editor.tsx | 70 ++++----- 2 files changed, 67 insertions(+), 137 deletions(-) diff --git a/src/app/(main)/query-runner/_components/cql-autocompleter.ts b/src/app/(main)/query-runner/_components/cql-autocompleter.ts index 1c8f1bd..cf84983 100644 --- a/src/app/(main)/query-runner/_components/cql-autocompleter.ts +++ b/src/app/(main)/query-runner/_components/cql-autocompleter.ts @@ -1,4 +1,6 @@ -import * as monaco from "monaco-editor"; +import { useMonaco } from "@monaco-editor/react"; + +export type MonacoInstance = NonNullable>; // Define CQL keywords, functions, data types, and operators const keywords = [ @@ -183,101 +185,43 @@ const operators = [ ]; // Helper function to create completion items -function createCompletionItem( - label: string, - kind: monaco.languages.CompletionItemKind, - documentation?: string, - insertText?: string, -): monaco.languages.CompletionItem { - return { - label, - kind, - documentation, - range: { - startLineNumber: 0, - startColumn: 0, - endLineNumber: 0, - endColumn: 0, +export const cqlCompletionItemProvider = (monacoInstance: MonacoInstance) => { + const defaultRange = { + startLineNumber: 0, + startColumn: 0, + endLineNumber: 0, + endColumn: 0, + }; + const cqlSyntax = { + [monacoInstance.languages.CompletionItemKind.Keyword]: { + data: keywords, + description: "Keyword", + }, + [monacoInstance.languages.CompletionItemKind.Function]: { + data: functions, + description: 'func + "($0)"', + }, + [monacoInstance.languages.CompletionItemKind.TypeParameter]: { + data: dataTypes, + description: "Data Type", + }, + [monacoInstance.languages.CompletionItemKind.Operator]: { + data: operators, + description: "Operator", }, - insertText: insertText || label, - insertTextRules: - monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, }; -} -// Build completion items -const completionItems: monaco.languages.CompletionItem[] = []; - -// Add keywords to completion items -keywords.forEach((keyword) => { - completionItems.push( - createCompletionItem( - keyword, - monaco.languages.CompletionItemKind.Keyword, - "Keyword", - ), - ); -}); - -// Add functions to completion items -functions.forEach((func) => { - completionItems.push( - createCompletionItem( - func, - monaco.languages.CompletionItemKind.Function, - "Function", - func + "($0)", - ), + return Object.entries(cqlSyntax).flatMap(([syntaxType, item]) => + item.data.map((value) => ({ + label: value, + kind: monacoInstance.languages.CompletionItemKind[ + syntaxType as keyof typeof monacoInstance.languages.CompletionItemKind + ], + documentation: item.description, + range: defaultRange, + insertText: value, + insertTextRules: + monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet, + })), ); -}); - -// Add data types to completion items -dataTypes.forEach((type) => { - completionItems.push( - createCompletionItem( - type, - monaco.languages.CompletionItemKind.TypeParameter, - "Data Type", - ), - ); -}); - -// Add operators to completion items -operators.forEach((operator) => { - completionItems.push( - createCompletionItem( - operator, - monaco.languages.CompletionItemKind.Operator, - "Operator", - ), - ); -}); - -// Implement the CompletionItemProvider -export const cqlCompletionItemProvider: monaco.languages.CompletionItemProvider = - { - triggerCharacters: [" ", ".", "(", ")"], - provideCompletionItems: ( - model: monaco.editor.ITextModel, - position: monaco.Position, - context: monaco.languages.CompletionContext, - token: monaco.CancellationToken, - ) => { - const word = model.getWordUntilPosition(position); - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: word.startColumn, - endColumn: word.endColumn, - }; - - // Filter suggestions based on the current word - const suggestions = completionItems - .filter((item) => - item.label.toLowerCase().startsWith(word.word.toLowerCase()), - ) - .map((item) => ({ ...item, range })); - - return { suggestions }; - }, - }; +}; diff --git a/src/app/(main)/query-runner/_components/cql-editor.tsx b/src/app/(main)/query-runner/_components/cql-editor.tsx index d26c5d6..bf10a3a 100644 --- a/src/app/(main)/query-runner/_components/cql-editor.tsx +++ b/src/app/(main)/query-runner/_components/cql-editor.tsx @@ -23,13 +23,7 @@ import type { TracingResult } from "@scylla-studio/lib/execute-query"; import { cn } from "@scylla-studio/lib/utils"; import debounce from "lodash.debounce"; import { Braces, ChartArea, Play, SearchCode } from "lucide-react"; -import { - CancellationToken, - CompletionItem, - Position, - editor, - languages, -} from "monaco-editor"; +import { CancellationToken, Position, editor, languages } from "monaco-editor"; import { useAction } from "next-safe-action/hooks"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; @@ -185,41 +179,33 @@ export function CqlEditor() { }); monaco.languages.setMonarchTokensProvider("cql", cql_language); - // monaco.languages.registerCompletionItemProvider("cql", { - // triggerCharacters: [' ', '.', '(', ')'], - // provideCompletionItems(model: editor.ITextModel, position: Position, context: languages.CompletionContext, token: CancellationToken): languages.ProviderResult { - // - // const completionItems: CompletionItem[] = [ - // { - // label: 'SELECT', - // kind: languages.CompletionItemKind.Keyword, - // documentation: 'Select data from a table', - // range: { - // startLineNumber: 0, - // startColumn: 0, - // endLineNumber: 0, - // endColumn: 0 - // }, - // insertText: 'SELECT', - // insertTextRules: languages.CompletionItemInsertTextRule.InsertAsSnippet - // } - // - // ]; - // return {suggestions: completionItems}; - // - // const word = model.getWordUntilPosition(position); - // const range = { - // startLineNumber: position.lineNumber, - // endLineNumber: position.lineNumber, - // startColumn: word.startColumn, - // endColumn: word.endColumn - // }; - // const suggestions = completionItems - // .filter((item) => item.label.toLowerCase().startsWith(word.word.toLowerCase())) - // .map((item) => ({ ...item, range })); - // return { suggestions }; - // } - // }); + + monaco.languages.registerCompletionItemProvider("cql", { + triggerCharacters: [" ", ".", "(", ")"], + provideCompletionItems( + model: editor.ITextModel, + position: Position, + _context: languages.CompletionContext, + _token: CancellationToken, + ): languages.ProviderResult { + const word = model.getWordUntilPosition(position); + + const range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn, + }; + + const suggestions = cqlCompletionItemProvider(monaco) + .filter((item) => + item.label.toLowerCase().startsWith(word.word.toLowerCase()), + ) + .map((item) => ({ ...item, range })); + + return { suggestions }; + }, + }); editorReference.current = editor; From e2acc7384e186d420ebb25da24aad95310c326cc Mon Sep 17 00:00:00 2001 From: danielhe4rt Date: Fri, 1 Nov 2024 14:28:47 -0300 Subject: [PATCH 3/4] wip: autocompleter with defined cases --- .../_components/cql-autocompleter.ts | 117 ++++++++++++++++-- .../query-runner/_components/cql-editor.tsx | 5 +- 2 files changed, 111 insertions(+), 11 deletions(-) diff --git a/src/app/(main)/query-runner/_components/cql-autocompleter.ts b/src/app/(main)/query-runner/_components/cql-autocompleter.ts index cf84983..0f979e8 100644 --- a/src/app/(main)/query-runner/_components/cql-autocompleter.ts +++ b/src/app/(main)/query-runner/_components/cql-autocompleter.ts @@ -1,4 +1,7 @@ import { useMonaco } from "@monaco-editor/react"; +import { getFullQueryAtCursor } from "@scylla-studio/app/(main)/query-runner/_components/utils"; +import { useCqlFilters } from "@scylla-studio/hooks/use-cql-filters"; +import { editor } from "monaco-editor"; export type MonacoInstance = NonNullable>; @@ -184,14 +187,14 @@ const operators = [ "IS NULL", ]; -// Helper function to create completion items -export const cqlCompletionItemProvider = (monacoInstance: MonacoInstance) => { - const defaultRange = { - startLineNumber: 0, - startColumn: 0, - endLineNumber: 0, - endColumn: 0, - }; +const defaultRange = { + startLineNumber: 0, + startColumn: 0, + endLineNumber: 0, + endColumn: 0, +}; + +function getDefaultSuggestions(monacoInstance: MonacoInstance) { const cqlSyntax = { [monacoInstance.languages.CompletionItemKind.Keyword]: { data: keywords, @@ -213,7 +216,14 @@ export const cqlCompletionItemProvider = (monacoInstance: MonacoInstance) => { return Object.entries(cqlSyntax).flatMap(([syntaxType, item]) => item.data.map((value) => ({ - label: value, + label: { + label: value, + detail: null, + description: + monacoInstance.languages.CompletionItemKind[ + syntaxType as keyof typeof monacoInstance.languages.CompletionItemKind + ], + }, kind: monacoInstance.languages.CompletionItemKind[ syntaxType as keyof typeof monacoInstance.languages.CompletionItemKind ], @@ -224,4 +234,93 @@ export const cqlCompletionItemProvider = (monacoInstance: MonacoInstance) => { monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet, })), ); +} + +// Helper function to create completion items +export const cqlCompletionItemProvider = ( + monacoInstance: MonacoInstance, + editor: editor.IStandaloneCodeEditor, +) => { + const cursor = getFullQueryAtCursor(editor, monacoInstance); + + if (!cursor) return getDefaultSuggestions(monacoInstance); + + const textUntilPosition = cursor.query.split("\n\n").shift() || ""; + + console.log(textUntilPosition); + + const useKeyspaceRegex = /USE\s+(\w*)$/i; + const fromTableRegex = /FROM\s+(\w*)$/i; + const selectColumnRegex = /SELECT\s+([\w,\s]*)$/i; + const whereColumnRegex = /WHERE\s+(\w*)$/i; + + if (useKeyspaceRegex.test(textUntilPosition)) { + return [ + { + label: { + label: "my_keyspace", + detail: "lalalala", + description: "Keyspace", + }, + kind: monacoInstance.languages.CompletionItemKind.Keyword, + documentation: "Keyspace belongs to the current cluster {cluster_name}", + range: defaultRange, + insertText: "my_keyspace", + insertTextRules: + monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet, + }, + ]; + } else if (fromTableRegex.test(textUntilPosition)) { + return [ + { + label: { + label: "my_keyspace.messages", + detail: "lalalala", + description: "Table", // materialized views? + }, + kind: monacoInstance.languages.CompletionItemKind.Class, + documentation: "Table with stuff trust me", + range: defaultRange, + insertText: "monstrously_long_keyspace_name.messages", + insertTextRules: + monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet, + }, + ]; + } else if (selectColumnRegex.test(textUntilPosition)) { + // TODO: or we display everything related to all tables or we display nothing + return getDefaultSuggestions(monacoInstance); + + // return [ + // { + // label: { + // label: "monstrously_long_keyspace_name.messages", + // detail: "lalalala", + // description: "that's definitely something" + // }, + // kind: monacoInstance.languages.CompletionItemKind.Keyword, + // documentation: "Keyword", + // range: defaultRange, + // insertText: "monstrously_long_keyspace_name.messages", + // insertTextRules: monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet, + // }, + // ]; + } else if (whereColumnRegex.test(textUntilPosition)) { + return [ + { + label: { + label: "field1", + detail: "(from table x)", + description: "Column", + }, + kind: monacoInstance.languages.CompletionItemKind.Keyword, + documentation: "Column from table x", + range: defaultRange, + insertText: "field1", + insertTextRules: + monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet, + }, + ]; + } + + return getDefaultSuggestions(monacoInstance); }; diff --git a/src/app/(main)/query-runner/_components/cql-editor.tsx b/src/app/(main)/query-runner/_components/cql-editor.tsx index bf10a3a..3932991 100644 --- a/src/app/(main)/query-runner/_components/cql-editor.tsx +++ b/src/app/(main)/query-runner/_components/cql-editor.tsx @@ -17,6 +17,7 @@ import { } from "@scylla-studio/components/ui/resizable"; import { Skeleton } from "@scylla-studio/components/ui/skeleton"; import { Tabs, TabsList, TabsTrigger } from "@scylla-studio/components/ui/tabs"; +import { useLayout } from "@scylla-studio/contexts/layout"; import { useCqlFilters } from "@scylla-studio/hooks/use-cql-filters"; import type { AvailableConnections } from "@scylla-studio/lib/connections"; import type { TracingResult } from "@scylla-studio/lib/execute-query"; @@ -197,9 +198,9 @@ export function CqlEditor() { endColumn: word.endColumn, }; - const suggestions = cqlCompletionItemProvider(monaco) + const suggestions = cqlCompletionItemProvider(monaco, editor) .filter((item) => - item.label.toLowerCase().startsWith(word.word.toLowerCase()), + item.label.label.toLowerCase().startsWith(word.word.toLowerCase()), ) .map((item) => ({ ...item, range })); From 969279dc04786f9073a6d8947a7daba7b23fcd69 Mon Sep 17 00:00:00 2001 From: danielhe4rt Date: Sat, 2 Nov 2024 17:10:27 -0300 Subject: [PATCH 4/4] fix: context to autocompleter --- .../_components/cql-autocompleter.ts | 172 +++++++++++------- 1 file changed, 109 insertions(+), 63 deletions(-) diff --git a/src/app/(main)/query-runner/_components/cql-autocompleter.ts b/src/app/(main)/query-runner/_components/cql-autocompleter.ts index 0f979e8..998de9c 100644 --- a/src/app/(main)/query-runner/_components/cql-autocompleter.ts +++ b/src/app/(main)/query-runner/_components/cql-autocompleter.ts @@ -1,7 +1,15 @@ import { useMonaco } from "@monaco-editor/react"; +import KeyspaceDefinitions from "@scylla-studio/app/(main)/keyspace/[keyspace]/_components/keyspace-tables"; import { getFullQueryAtCursor } from "@scylla-studio/app/(main)/query-runner/_components/utils"; import { useCqlFilters } from "@scylla-studio/hooks/use-cql-filters"; -import { editor } from "monaco-editor"; +import { KeyspaceDefinition } from "@scylla-studio/lib/cql-parser/keyspace-parser"; +import { TableDefinition } from "@scylla-studio/lib/cql-parser/table-parser"; +import { + CompletionItem, + CompletionItemLabel, + editor, + languages, +} from "monaco-editor"; export type MonacoInstance = NonNullable>; @@ -194,7 +202,9 @@ const defaultRange = { endColumn: 0, }; -function getDefaultSuggestions(monacoInstance: MonacoInstance) { +function getDefaultSuggestions( + monacoInstance: MonacoInstance, +): languages.CompletionItem[] { const cqlSyntax = { [monacoInstance.languages.CompletionItemKind.Keyword]: { data: keywords, @@ -215,24 +225,77 @@ function getDefaultSuggestions(monacoInstance: MonacoInstance) { }; return Object.entries(cqlSyntax).flatMap(([syntaxType, item]) => - item.data.map((value) => ({ - label: { - label: value, - detail: null, - description: - monacoInstance.languages.CompletionItemKind[ + item.data.map( + (value) => + ({ + label: { + label: value, + detail: + monacoInstance.languages.CompletionItemKind[ + syntaxType as keyof typeof monacoInstance.languages.CompletionItemKind + ].toString(), + description: item.description, + } as languages.CompletionItemLabel, + kind: monacoInstance.languages.CompletionItemKind[ syntaxType as keyof typeof monacoInstance.languages.CompletionItemKind ], - }, - kind: monacoInstance.languages.CompletionItemKind[ - syntaxType as keyof typeof monacoInstance.languages.CompletionItemKind - ], - documentation: item.description, - range: defaultRange, - insertText: value, - insertTextRules: - monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet, - })), + documentation: item.description, + range: defaultRange, + insertText: value, + insertTextRules: + monacoInstance.languages.CompletionItemInsertTextRule + .InsertAsSnippet, + }) as languages.CompletionItem, + ), + ); +} + +function prepareUseSuggestions( + monacoInstance: MonacoInstance, + keyspaces: KeyspaceDefinition[], +): languages.CompletionItem[] { + return keyspaces.map( + (keyspace) => + ({ + label: { + label: keyspace.name, + detail: "", + description: "Keyspace", + } as languages.CompletionItemLabel, + kind: monacoInstance.languages.CompletionItemKind.Class, + documentation: "TODO: build documentation with the Keyspace Object", + range: defaultRange, + insertText: keyspace.name, + insertTextRules: + monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet, + }) as languages.CompletionItem, + ); +} + +type TableCompletion = { + keyspace: string; + table: string; +}; + +function prepareTablesSuggestions( + monacoInstance: MonacoInstance, + tables: TableCompletion[], +): languages.CompletionItem[] { + return tables.map( + (table) => + ({ + label: { + label: `${table.keyspace}.${table.table}`, + detail: "", + description: "Table", + } as languages.CompletionItemLabel, + kind: monacoInstance.languages.CompletionItemKind.Class, + documentation: "TODO: build documentation with the Keyspace Object", + range: defaultRange, + insertText: `${table.keyspace}.${table.table}`, + insertTextRules: + monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet, + }) as languages.CompletionItem, ); } @@ -240,77 +303,60 @@ function getDefaultSuggestions(monacoInstance: MonacoInstance) { export const cqlCompletionItemProvider = ( monacoInstance: MonacoInstance, editor: editor.IStandaloneCodeEditor, -) => { +): languages.CompletionItem[] => { const cursor = getFullQueryAtCursor(editor, monacoInstance); if (!cursor) return getDefaultSuggestions(monacoInstance); const textUntilPosition = cursor.query.split("\n\n").shift() || ""; - console.log(textUntilPosition); - const useKeyspaceRegex = /USE\s+(\w*)$/i; const fromTableRegex = /FROM\s+(\w*)$/i; const selectColumnRegex = /SELECT\s+([\w,\s]*)$/i; const whereColumnRegex = /WHERE\s+(\w*)$/i; + // Check if you're typing a keyspace if (useKeyspaceRegex.test(textUntilPosition)) { - return [ + const keyspaces = [ { - label: { - label: "my_keyspace", - detail: "lalalala", - description: "Keyspace", + // TODO: this should be a real keyspace + name: "my_keyspace", + entitiesCount: 1337, // Ignore this until I figure out what it is + replication: { + class: "NetworkTopologyStrategy", + datacenters: new Map(), }, - kind: monacoInstance.languages.CompletionItemKind.Keyword, - documentation: "Keyspace belongs to the current cluster {cluster_name}", - range: defaultRange, - insertText: "my_keyspace", - insertTextRules: - monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet, + durableWrites: true, + tablets: true, + tables: new Map(), }, - ]; + ] as KeyspaceDefinition[]; + + return prepareUseSuggestions(monacoInstance, keyspaces); } else if (fromTableRegex.test(textUntilPosition)) { - return [ + const tables = [ { - label: { - label: "my_keyspace.messages", - detail: "lalalala", - description: "Table", // materialized views? - }, - kind: monacoInstance.languages.CompletionItemKind.Class, - documentation: "Table with stuff trust me", - range: defaultRange, - insertText: "monstrously_long_keyspace_name.messages", - insertTextRules: - monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet, + keyspace: "my_keyspace", + table: "my_table", }, - ]; + { + keyspace: "my_keyspace", + table: "my_table2", + }, + ] as TableCompletion[]; + + return prepareTablesSuggestions(monacoInstance, tables); } else if (selectColumnRegex.test(textUntilPosition)) { // TODO: or we display everything related to all tables or we display nothing return getDefaultSuggestions(monacoInstance); - - // return [ - // { - // label: { - // label: "monstrously_long_keyspace_name.messages", - // detail: "lalalala", - // description: "that's definitely something" - // }, - // kind: monacoInstance.languages.CompletionItemKind.Keyword, - // documentation: "Keyword", - // range: defaultRange, - // insertText: "monstrously_long_keyspace_name.messages", - // insertTextRules: monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet, - // }, - // ]; } else if (whereColumnRegex.test(textUntilPosition)) { + // TODO: here we would need to fetch all related columns from the specific table mentioned previously return [ { label: { label: "field1", - detail: "(from table x)", - description: "Column", + detail: "Column", + description: " - table x ", }, kind: monacoInstance.languages.CompletionItemKind.Keyword, documentation: "Column from table x",