Skip to content

Commit 56cd3a1

Browse files
committed
feat: implement centralized SQL formatting providers for editor component
1 parent 611d897 commit 56cd3a1

3 files changed

Lines changed: 91 additions & 70 deletions

File tree

ui/src/components/editor.tsx

Lines changed: 6 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { FunctionComponent, useEffect, useRef } from "react";
33
import type { IDisposable } from "monaco-editor";
44
import { vsPlusTheme } from "monaco-sql-languages";
55
import EditorComponent, { useMonaco } from "@monaco-editor/react";
6-
import { format as formatSql } from "sql-formatter";
76

87
import {
98
COMMAND_CONFIG,
@@ -15,6 +14,7 @@ import { fetchAutocomplete } from "@/api";
1514
import { Card } from "@/components/ui/card";
1615
import { useQuery } from "@tanstack/react-query";
1716
import { useTheme } from "@/provider/theme.provider";
17+
import { useSqlFormattingProviders } from "@/lib/monaco";
1818

1919
type Props = {
2020
value: string;
@@ -25,7 +25,7 @@ export const Editor: FunctionComponent<Props> = ({ value, onChange }) => {
2525
const currentTheme = useTheme();
2626
const monacoInstance = useMonaco();
2727
const providerRef = useRef<IDisposable | null>(null);
28-
const formatProviderRefs = useRef<IDisposable[]>([]);
28+
2929

3030
const { data: autoCompleteData } = useQuery({
3131
queryKey: ["autocomplete"],
@@ -124,74 +124,10 @@ export const Editor: FunctionComponent<Props> = ({ value, onChange }) => {
124124
}, [monacoInstance, autoCompleteData]);
125125

126126
// Register formatting providers (document and range)
127-
useEffect(() => {
128-
if (!monacoInstance) return;
129-
130-
// Dispose previous providers if any
131-
formatProviderRefs.current.forEach((d) => d.dispose());
132-
formatProviderRefs.current = [];
133-
134-
const getOptions = () => ({
135-
language: "sqlite" as const,
136-
keywordCase: "upper" as const,
137-
indent: " ",
138-
});
139-
140-
const documentProvider =
141-
monacoInstance.languages.registerDocumentFormattingEditProvider(
142-
ID_LANGUAGE_SQL,
143-
{
144-
provideDocumentFormattingEdits: (model) => {
145-
const fullRange = model.getFullModelRange();
146-
const text = model.getValue();
147-
try {
148-
const formatted = formatSql(text, getOptions());
149-
return [
150-
{
151-
range: fullRange,
152-
text: formatted,
153-
},
154-
];
155-
} catch (err) {
156-
// Keep UX stable if formatting fails
157-
// eslint-disable-next-line no-console
158-
console.error("SQL formatting error (document)", err);
159-
return [];
160-
}
161-
},
162-
}
163-
);
164-
165-
const rangeProvider =
166-
monacoInstance.languages.registerDocumentRangeFormattingEditProvider(
167-
ID_LANGUAGE_SQL,
168-
{
169-
provideDocumentRangeFormattingEdits: (model, range) => {
170-
const text = model.getValueInRange(range);
171-
try {
172-
const formatted = formatSql(text, getOptions());
173-
return [
174-
{
175-
range,
176-
text: formatted,
177-
},
178-
];
179-
} catch (err) {
180-
// eslint-disable-next-line no-console
181-
console.error("SQL formatting error (range)", err);
182-
return [];
183-
}
184-
},
185-
}
186-
);
187-
188-
formatProviderRefs.current = [documentProvider, rangeProvider];
189-
190-
return () => {
191-
formatProviderRefs.current.forEach((d) => d.dispose());
192-
formatProviderRefs.current = [];
193-
};
194-
}, [monacoInstance]);
127+
// Use centralized formatting providers
128+
// Moved into lib to keep this component lean
129+
useSqlFormattingProviders(monacoInstance, { languageId: ID_LANGUAGE_SQL });
130+
195131

196132
// Avoid rendering until theme is known
197133
if (!currentTheme) return null;

ui/src/lib/monaco/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { useSqlFormattingProviders } from "./sql-formatting"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { useEffect, useRef } from "react";
2+
import type { IDisposable } from "monaco-editor";
3+
import type * as Monaco from "monaco-editor";
4+
import { format as formatSql } from "sql-formatter";
5+
import type { FormatOptionsWithLanguage } from "sql-formatter";
6+
7+
type UseSqlFormattingProvidersOptions = {
8+
languageId: string;
9+
getOptions?: () => FormatOptionsWithLanguage;
10+
};
11+
12+
const getDefaultSqlFormatOptions = (): FormatOptionsWithLanguage => ({
13+
language: "sqlite",
14+
keywordCase: "upper",
15+
});
16+
17+
export function useSqlFormattingProviders(
18+
monaco: typeof Monaco | null,
19+
{ languageId, getOptions }: UseSqlFormattingProvidersOptions
20+
) {
21+
const disposablesRef = useRef<IDisposable[]>([]);
22+
23+
useEffect(() => {
24+
if (!monaco) return;
25+
26+
// Dispose any previous providers
27+
disposablesRef.current.forEach((d) => d.dispose());
28+
disposablesRef.current = [];
29+
30+
const getOpts = getOptions ?? getDefaultSqlFormatOptions;
31+
32+
const documentProvider =
33+
monaco.languages.registerDocumentFormattingEditProvider(languageId, {
34+
provideDocumentFormattingEdits: (model) => {
35+
const fullRange = model.getFullModelRange();
36+
const text = model.getValue();
37+
try {
38+
const formatted = formatSql(text, getOpts());
39+
return [
40+
{
41+
range: fullRange,
42+
text: formatted,
43+
},
44+
];
45+
} catch (err) {
46+
// Keep UX stable if formatting fails
47+
// eslint-disable-next-line no-console
48+
console.error("SQL formatting error (document)", err);
49+
return [];
50+
}
51+
},
52+
});
53+
54+
const rangeProvider =
55+
monaco.languages.registerDocumentRangeFormattingEditProvider(
56+
languageId,
57+
{
58+
provideDocumentRangeFormattingEdits: (model, range) => {
59+
const text = model.getValueInRange(range);
60+
try {
61+
const formatted = formatSql(text, getOpts());
62+
return [
63+
{
64+
range,
65+
text: formatted,
66+
},
67+
];
68+
} catch (err) {
69+
// eslint-disable-next-line no-console
70+
console.error("SQL formatting error (range)", err);
71+
return [];
72+
}
73+
},
74+
}
75+
);
76+
77+
disposablesRef.current = [documentProvider, rangeProvider];
78+
79+
return () => {
80+
disposablesRef.current.forEach((d) => d.dispose());
81+
disposablesRef.current = [];
82+
};
83+
}, [monaco, languageId, getOptions]);
84+
}

0 commit comments

Comments
 (0)