Skip to content

Commit

Permalink
feat: cql autocompleter with syntax highlighter (#46)
Browse files Browse the repository at this point in the history
![image](https://github.com/user-attachments/assets/70c3f723-5746-4ef7-b556-0d5993608007)

This Pull Requests aims to add Syntax Highlight for 'CQL' and bring
Autocompletion for the Editor based on the Keyspace informations
(fields, tables, keyspaces etc).

- [x] Syntax Highlight with CQL
- [ ] Keyspace Autocompletion

At the CQL Editor you will find these lines and everything must be
around it.
```ts
monaco.languages.register({
  id: "cql",
  extensions: [".cql"],
  aliases: ["CQL", "cql"],
  mimetypes: ["text/cql"],
});

monaco.languages.setMonarchTokensProvider("cql", cql_language);
// monaco.languages.registerCompletionItemProvider("cql", {}); // 
```

Repositories used as reference:

*
[https://github.com/DTStack/monaco-sql-languages](https://github.com/DTStack/monaco-sql-languages)
*
[https://github.com/brijeshb42/monaco-ace-tokenizer](https://github.com/brijeshb42/monaco-ace-tokenizer)

> [!WARNING]
> There is an "compilation error" poppin so far when you try to add any
completion. Most of the other projects currently uses **Webpack** plugin
to handle it. I'm not sure how to solve it in NextJS.

Closes #16 

----

### Autocompletion and Syntax Highlighting Enhancements:

*
[`src/app/(main)/query-runner/_components/cql-autocompleter.ts`](diffhunk://#diff-5feb6defb1e3d8fa5b864f5eb133be4704cd4a41d6d808d688c8d4c0eb4fb579R1-R283):
Implemented a comprehensive autocompletion feature for CQL keywords,
functions, data types, and operators using the `monaco-editor` library.
*
[`src/app/(main)/query-runner/_components/cql-editor.tsx`](diffhunk://#diff-c3d008cee84b65d82023278c89d53a325b14f0df0e41dfe05a6b97bc57e63f16R5-R6):
Integrated the CQL autocompletion provider and registered the CQL
language with the Monaco editor. Updated the editor configuration to use
CQL instead of SQL.
[[1]](diffhunk://#diff-c3d008cee84b65d82023278c89d53a325b14f0df0e41dfe05a6b97bc57e63f16R5-R6)
[[2]](diffhunk://#diff-c3d008cee84b65d82023278c89d53a325b14f0df0e41dfe05a6b97bc57e63f16L24-R32)
[[3]](diffhunk://#diff-c3d008cee84b65d82023278c89d53a325b14f0df0e41dfe05a6b97bc57e63f16R71)
[[4]](diffhunk://#diff-c3d008cee84b65d82023278c89d53a325b14f0df0e41dfe05a6b97bc57e63f16R180-R223)
[[5]](diffhunk://#diff-c3d008cee84b65d82023278c89d53a325b14f0df0e41dfe05a6b97bc57e63f16L281-R334)
*
[`src/app/(main)/query-runner/_components/cql-language.ts`](diffhunk://#diff-ec0c7dc84c86fb5fde17b29d073ce9eaefb505aa1e5cdc15d9cd6feb7c50de47R1-R341):
Defined the CQL language configuration and tokenizer for syntax
highlighting in the Monaco editor.

### Minor Adjustments:

*
[`src/app/(main)/query-runner/_components/utils.tsx`](diffhunk://#diff-dc47942100a9e6a96dfd00903f3918d3286160928d04636e290421ecb58464feR24):
Minor formatting adjustment to improve code readability.

---------

Co-authored-by: Gabriel do Carmo Vieira <[email protected]>
  • Loading branch information
danielhe4rt and gvieira18 authored Nov 3, 2024
1 parent f40020a commit d5029e3
Show file tree
Hide file tree
Showing 4 changed files with 756 additions and 2 deletions.
372 changes: 372 additions & 0 deletions src/app/(main)/query-runner/_components/cql-autocompleter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,372 @@
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 { 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<ReturnType<typeof useMonaco>>;

// 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",
];

const defaultRange = {
startLineNumber: 0,
startColumn: 0,
endLineNumber: 0,
endColumn: 0,
};

function getDefaultSuggestions(
monacoInstance: MonacoInstance,
): languages.CompletionItem[] {
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",
},
};

return Object.entries(cqlSyntax).flatMap(([syntaxType, item]) =>
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
],
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,
);
}

// Helper function to create completion items
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() || "";

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)) {
const keyspaces = [
{
// 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<string, number>(),
},
durableWrites: true,
tablets: true,
tables: new Map<string, TableDefinition>(),
},
] as KeyspaceDefinition[];

return prepareUseSuggestions(monacoInstance, keyspaces);
} else if (fromTableRegex.test(textUntilPosition)) {
const tables = [
{
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);
} 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: "Column",
description: " - table x ",
},
kind: monacoInstance.languages.CompletionItemKind.Keyword,
documentation: "Column from table x",
range: defaultRange,
insertText: "field1",
insertTextRules:
monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet,
},
];
}

return getDefaultSuggestions(monacoInstance);
};
Loading

0 comments on commit d5029e3

Please sign in to comment.