import SyntaxHighlighter from "react-syntax-highlighter"; import { ClipboardIcon, PlayIcon, BookmarkIcon as BookmarkIconOutline, QuestionMarkCircleIcon, MinusIcon, } from "@heroicons/react/24/outline"; import { CustomTooltip } from "../Library/Tooltip"; import { monokai } from "react-syntax-highlighter/dist/esm/styles/hljs"; import { format } from "prettier-sql"; import { useEffect, useRef, useState } from "react"; import { useRunSqlInConversation, useUpdateSqlQuery, useGetUserProfile, } from "@/hooks"; import { Alert, AlertActions, AlertDescription, AlertTitle, } from "../Catalyst/alert"; import { Button } from "../Catalyst/button"; import { Dialect } from "../Library/types"; import Minimizer from "../Minimizer/Minimizer"; function copyToClipboard(text: string) { navigator.clipboard.writeText(text); } function classNames(...classes: string[]) { return classes.filter(Boolean).join(" "); } const SPACES = ["Enter", "Tab", "Space", "Backspace"]; const SPACE_CHARACTERS = [" ", "\n", "\t", "\r"]; // Helper function to get cursor position without considering spaces function getCursorPositionWithoutSpaces( text: string, originalCursorPosition: number ) { const textBeforeCursor = text.substring(0, originalCursorPosition); const nonSpaceCharacters = textBeforeCursor.replace(/\s/g, ""); return nonSpaceCharacters.length; } // Helper function to get new cursor position after formatting function getNewCursorPosition( formattedText: string, lastCharacterPosition: number ) { let nonSpaceCharacterCount = 0; let i = 0; // Iterate through formatted text until we reach the same non-space character // count as the original text's character count for (; i < formattedText.length; i++) { if (nonSpaceCharacterCount === lastCharacterPosition) { break; } if (!SPACE_CHARACTERS.includes(formattedText[i])) { nonSpaceCharacterCount++; } } return i; } type SupportedFormatters = | "bigquery" | "db2" | "hive" | "mariadb" | "mysql" | "n1ql" | "plsql" | "postgresql" | "redshift" | "spark" | "sql" | "tsql"; const formattedCodeOrInitial = (code: string, dialect: SupportedFormatters) => { try { return format(code, { language: dialect || Dialect.Postgres }); } catch { return code; } }; export const CodeBlock = ({ code, dialect, resultId, onUpdateSQLRunResult, onSaveSQLStringResult, forChart = false, minimize, }: { code: string; resultId: string; dialect?: string; onUpdateSQLRunResult: (sql_string_result_id: string, arg: string) => void; onSaveSQLStringResult: ( data?: { created_at: string; chartjs_json: string } | void ) => void; forChart: boolean; minimize?: boolean; }) => { const { data: profile } = useGetUserProfile(); const [savedCode, setSavedCode] = useState(() => formattedCodeOrInitial(code, dialect as SupportedFormatters) ); const [formattedCode, setFormattedCode] = useState(() => formattedCodeOrInitial(code, dialect as SupportedFormatters) ); // Determine if SQL should be minimized by default based on user preference const shouldHideSql = profile?.hide_sql_preference; const [minimized, setMinimized] = useState( minimize || shouldHideSql || false ); const textareaRef = useRef(null); const syntaxHighlighterId = `syntax-highlighter-${resultId}`; const [lastChar, setLastChar] = useState(""); // let BookmarkIcon = isSaved ? BookmarkIconSolid : BookmarkIconOutline; const BookmarkIcon = BookmarkIconOutline; const extraSpace = ""; const { isPending: isPendingRunSql, mutate: runSql } = useRunSqlInConversation( { sql: savedCode, resultId: resultId, }, { onSettled: (data, error) => { if (error) { console.error("onsettled error in: ", error); } else { if (data?.content) { onUpdateSQLRunResult(resultId, data.content as string); } } }, } ); const { isPending: isPendingSaveSql, mutate: updateSQL } = useUpdateSqlQuery({ onSettled: (data, error) => { if (error) { console.error("onsettled error in: ", error); } else { onSaveSQLStringResult(data); } }, }); function saveNewSQLString() { if (!resultId) return; updateSQL({ sqlStringResultId: resultId, code: savedCode, forChart: forChart, }); } useEffect(() => { try { // Do not format if whitespace characters are being typed if (SPACES.includes(lastChar)) { setFormattedCode(savedCode + extraSpace); return; } // If no characters are different from the saved code, don't format (ignoring spaces) const savedCodeWithoutSpaces = savedCode.replace(/\s/g, ""); const formattedCodeWithoutSpaces = formattedCode.replace(/\s/g, ""); if ( lastChar != "" && savedCodeWithoutSpaces === formattedCodeWithoutSpaces ) { return; } const formatted = format(savedCode, { language: (dialect as SupportedFormatters) || Dialect.Postgres, }); setFormattedCode(formatted + extraSpace); if (textareaRef.current !== null) { // Calculate old cursor position without considering spaces const oldCursorPosition = getCursorPositionWithoutSpaces( savedCode, textareaRef.current.selectionStart ); textareaRef.current.value = formatted; // Calculate new cursor position after formatting const newCursorPosition = getNewCursorPosition( formatted, oldCursorPosition ); textareaRef.current.setSelectionRange( newCursorPosition, newCursorPosition, "forward" ); } } catch (e) { setFormattedCode(savedCode); } }, [savedCode, formattedCode, lastChar, dialect]); useEffect(() => { if (!minimized && textareaRef.current !== null) { textareaRef.current.value = formattedCode; } }, [minimized, formattedCode, textareaRef]); const handleTextUpdate = (e: React.ChangeEvent) => { // If the user is typing a space, don't update and reformat the saved code setSavedCode(e.target.value); }; const handleKeyboardInput = (e: React.KeyboardEvent) => { setLastChar(e.code || e.key); // Special condition: handle tab key if (e.key === "Tab") { e.preventDefault(); const { selectionStart, selectionEnd } = e.currentTarget; // Modify current textarea value by adding 2 spaces at the cursor position e.currentTarget.value = e.currentTarget.value.substring(0, selectionStart) + " " + e.currentTarget.value.substring(selectionEnd); e.currentTarget.selectionStart = selectionStart + 2; e.currentTarget.selectionEnd = selectionEnd + 2; // Handle non-letter keys } else { setSavedCode(textareaRef.current?.value || ""); } }; // Mirror textarea horizontal and vertical scroll to syntax highlighter const mirrorScroll = () => { if (textareaRef.current !== null) { const { scrollLeft, scrollTop } = textareaRef.current; const syntaxHighlighter = document.getElementById(syntaxHighlighterId); if (syntaxHighlighter !== null) { syntaxHighlighter.scrollLeft = scrollLeft; syntaxHighlighter.scrollTop = scrollTop; } } }; const [isHelpOpen, setIsHelpOpen] = useState(false); const openSQLForChartHelp = () => { setIsHelpOpen(true); }; return (
textareaRef.current?.focus()} onClick={() => textareaRef.current?.focus()} >