Skip to content

Commit

Permalink
✨ Add params and types autocompletion to SQL editor
Browse files Browse the repository at this point in the history
  • Loading branch information
antoniovizuete committed Oct 10, 2022
1 parent f3744a1 commit 57cebe0
Show file tree
Hide file tree
Showing 13 changed files with 321 additions and 29 deletions.
43 changes: 26 additions & 17 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { Allotment } from "allotment";
import { useState } from "react";
import { useRef, useState } from "react";

import { QueryResult } from "../lib/peform-query";
import "../styles/App.css";
import Footer from "./Footer";
import QueryForm from "./QueryForm";
import MonacoWrapper from "./MonacoWrapper";
import QueryForm, { useQueryFormContext } from "./QueryForm";
import { EditorRef } from "./QueryForm/components/Editor";
import Result from "./Result";

export default function App() {
const [result, setResult] = useState<QueryResult | undefined>();
const [error, setError] = useState<string | undefined>();
const [loading, setLoading] = useState(false);
const paramsEditorRef = useRef<EditorRef>(null);
const { state } = useQueryFormContext();

const handleOnSuccess = (result?: QueryResult) => {
setResult(result);
Expand All @@ -25,20 +29,25 @@ export default function App() {
};

return (
<Allotment vertical>
<Allotment.Pane minSize={100}>
<QueryForm
onSuccess={handleOnSuccess}
onError={handleOnError}
onPerformQuery={() => setLoading(true)}
/>
</Allotment.Pane>
<Allotment.Pane>
<Result result={result} error={error} loading={loading} />
</Allotment.Pane>
<Allotment.Pane maxSize={24} minSize={24} className="p-0">
<Footer />
</Allotment.Pane>
</Allotment>
<MonacoWrapper
jsonParams={paramsEditorRef.current?.getValue() || state.jsonParams}
>
<Allotment vertical>
<Allotment.Pane minSize={100}>
<QueryForm
onSuccess={handleOnSuccess}
onError={handleOnError}
onPerformQuery={() => setLoading(true)}
paramsEditorRef={paramsEditorRef}
/>
</Allotment.Pane>
<Allotment.Pane>
<Result result={result} error={error} loading={loading} />
</Allotment.Pane>
<Allotment.Pane maxSize={24} minSize={24} className="p-0">
<Footer />
</Allotment.Pane>
</Allotment>
</MonacoWrapper>
);
}
11 changes: 11 additions & 0 deletions src/components/MonacoWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PropsWithChildren } from "react";
import { useMonacoConfigSupplier } from "../hooks/useMonacoConfigSupplier";

type Props = PropsWithChildren<{
jsonParams?: string;
}>;

export default function MonacoWrapper({ children, jsonParams = "{}" }: Props) {
useMonacoConfigSupplier({ jsonParams });
return <>{children}</>;
}
5 changes: 3 additions & 2 deletions src/components/QueryForm/QueryForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@ import { Alignment, Button, InputGroup, Navbar } from "@blueprintjs/core";
import { Allotment } from "allotment";
import { QueryResult } from "../../lib/peform-query";

import Editor from "./components/Editor";
import Editor, { EditorRef } from "./components/Editor";
import { useQueryForm } from "./hooks/useQueryForm";
import { Actions } from "./QueryForm.reducer";

export type QueryFormProps = {
onPerformQuery: () => void;
onSuccess: (result?: QueryResult) => void;
onError: (error?: string) => void;
paramsEditorRef: React.RefObject<EditorRef>;
};

export default function QueryForm(props: QueryFormProps) {
const { paramsEditorRef } = props;
const {
state: { query, password, serverAddress, username, jsonParams },
dispatch,
performQuery,
queryEditorRef,
paramsEditorRef,
openHelpDialog,
HotKeysHelpDialog,
} = useQueryForm(props);
Expand Down
1 change: 0 additions & 1 deletion src/components/QueryForm/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
} from "monaco-editor/esm/vs/editor/editor.api";
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
import { addAction } from "../../../lib/editor-helpers/add-action.editor.helper";
import { QueryFormContextType } from "../QueryForm.context";

type EditorProps = {
defaultValue?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ const HotKeysHelpDialog = forwardRef<HotKeysHelpDialogRef, Props>(
.split(",")
.filter(Boolean)
.filter(filterCombosByOS)
.flatMap((combo) => (
<div className="mx-2 my-1 gap-1 flex flex-row justify-end">
.flatMap((combo, i) => (
<div key={i} className="mx-2 my-1 gap-1 flex flex-row justify-end">
{mapComboToTags(combo.trim())}
</div>
));
Expand Down
2 changes: 1 addition & 1 deletion src/components/QueryForm/hooks/useQueryForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const useQueryForm = ({
description: "Run query",
callback: () =>
performQuery({ query: state.query, jsonParams: state.jsonParams }),
deps: [performQuery],
deps: [performQuery, state.query, state.jsonParams],
},
{
combo: "alt+h, option+h",
Expand Down
81 changes: 81 additions & 0 deletions src/hooks/useMonacoConfigSupplier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useMonaco } from "@monaco-editor/react";
import { IDisposable } from "monaco-editor/esm/vs/editor/editor.api";
import { useEffect, useState } from "react";
import { getParemeterNameSuggetionProvider } from "../lib/editor-suggestions/parameter-name.suggestion";
import { paremeterSnippetSuggetionProvider } from "../lib/editor-suggestions/parameter-snippet.suggestion";
import { paremeterTypeSuggetionProvider } from "../lib/editor-suggestions/parameter-type.suggestion";

type Params = {
jsonParams: string;
};

export const useMonacoConfigSupplier = ({ jsonParams }: Params) => {
const monaco = useMonaco();
const [paramKeys, setParamKeys] = useState<string[]>([]);
const [areSameParamKeys, setAreSameParamKeys] = useState(false);

useEffect(() => {
let subs: IDisposable[] = [];
if (monaco) {
subs.push(
monaco.languages.registerCompletionItemProvider(
paremeterSnippetSuggetionProvider.language,
paremeterSnippetSuggetionProvider.provider
)
);
subs.push(
monaco.languages.registerCompletionItemProvider(
paremeterTypeSuggetionProvider.language,
paremeterTypeSuggetionProvider.provider
)
);
}
return () => {
subs.forEach((sub) => sub.dispose());
};
}, [monaco]);

useEffect(() => {
try {
const parsedParams = Object.keys(JSON.parse(jsonParams));
setParamKeys((prev) => {
if (arraysAreEqual(prev, parsedParams)) {
setAreSameParamKeys(true);
return prev;
}
setAreSameParamKeys(false);
return parsedParams;
});
} catch (e) {}
}, [jsonParams]);

useEffect(() => {
let subs: IDisposable[] = [];
if (monaco) {
const { provider, language } =
getParemeterNameSuggetionProvider(paramKeys);
subs.push(
monaco.languages.registerCompletionItemProvider(
language,
areSameParamKeys
? {
triggerCharacters: provider.triggerCharacters,
provideCompletionItems: function () {
return { suggestions: [] };
},
}
: provider
)
);
}
return () => {
console.log("disposing");
setAreSameParamKeys(false);
subs.forEach((sub) => sub.dispose());
};
}, [monaco, paramKeys, areSameParamKeys]);
};

const arraysAreEqual = (array1: string[], array2: string[]): boolean =>
array1.length === array2.length &&
array1.every((element, index) => element === array2[index]);
46 changes: 46 additions & 0 deletions src/lib/editor-suggestions/parameter-name.suggestion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { languages } from "monaco-editor/esm/vs/editor/editor.api";
import { SuggestionProvider } from "./types";

export const getParemeterNameSuggetionProvider = (
paramKeys: string[] = []
): SuggestionProvider => {
return {
language: "sql",
provider: {
triggerCharacters: ["{"],
provideCompletionItems(model, position) {
const word = model.getWordUntilPosition(position);
const textUntilPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});

const lastWord = textUntilPosition.split(" ").pop();

const match = lastWord?.match(/\{/g);
if (!match) {
return {
suggestions: [],
};
}
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};

return {
suggestions: paramKeys.map((key) => ({
label: key,
kind: languages.CompletionItemKind.Keyword,
insertText: key,
range,
})),
};
},
},
};
};
30 changes: 30 additions & 0 deletions src/lib/editor-suggestions/parameter-snippet.suggestion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { languages } from "monaco-editor/esm/vs/editor/editor.api";
import { SuggestionProvider } from "./types";

export const paremeterSnippetSuggetionProvider: SuggestionProvider = {
language: "sql",
provider: {
provideCompletionItems(model, position, context, token) {
const word = model.getWordUntilPosition(position);

const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};
return {
suggestions: [
{
label: "parameter",
kind: languages.CompletionItemKind.Snippet,
insertText: "{${1:parameterName}:${2:parameterType}}",
insertTextRules:
languages.CompletionItemInsertTextRule.InsertAsSnippet,
range,
},
],
};
},
},
};
101 changes: 101 additions & 0 deletions src/lib/editor-suggestions/parameter-type.suggestion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { languages } from "monaco-editor/esm/vs/editor/editor.api";
import { SuggestionProvider } from "./types";

export const paremeterTypeSuggetionProvider: SuggestionProvider = {
language: "sql",
provider: {
triggerCharacters: [":"],
provideCompletionItems(model, position) {
const word = model.getWordUntilPosition(position);
const textUntilPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
});
const lastWord = textUntilPosition.split(" ").pop();

const match = lastWord?.match(/\{[a-zA-Z0-9]+:/g);
if (!match) {
return {
suggestions: [],
};
}

const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};
return {
suggestions: types.map((type) => ({
label: type,
kind: languages.CompletionItemKind.Keyword,
insertText: type,
range,
})),
};
},
},
};

const types = [
"AggregateFunction",
"Array",
"Bool",
"Date",
"Date32",
"DateTime",
"DateTime32",
"DateTime64",
"Decimal",
"Decimal128",
"Decimal256",
"Decimal32",
"Decimal64",
"Enum",
"Enum16",
"Enum8",
"FixedString",
"Float32",
"Float64",
"IPv4",
"IPv6",
"Int128",
"Int16",
"Int256",
"Int32",
"Int64",
"Int8",
"IntervalDay",
"IntervalHour",
"IntervalMicrosecond",
"IntervalMillisecond",
"IntervalMinute",
"IntervalMonth",
"IntervalNanosecond",
"IntervalQuarter",
"IntervalSecond",
"IntervalWeek",
"IntervalYear",
"LowCardinality",
"Map",
"MultiPolygon",
"Nested",
"Nothing",
"Nullable",
"Object",
"Point",
"Polygon",
"Ring",
"SimpleAggregateFunction",
"String",
"Tuple",
"UInt128",
"UInt16",
"UInt256",
"UInt32",
"UInt64",
"UInt8",
];
6 changes: 6 additions & 0 deletions src/lib/editor-suggestions/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { languages } from "monaco-editor/esm/vs/editor/editor.api";

export type SuggestionProvider = {
language: languages.LanguageSelector;
provider: languages.CompletionItemProvider;
};
Loading

0 comments on commit 57cebe0

Please sign in to comment.