diff --git a/src/components/EditableSyntaxHighlighter/EditableSyntaxHighlighter.module.scss b/src/components/EditableSyntaxHighlighter/EditableSyntaxHighlighter.module.scss index 017b4ff..5045982 100644 --- a/src/components/EditableSyntaxHighlighter/EditableSyntaxHighlighter.module.scss +++ b/src/components/EditableSyntaxHighlighter/EditableSyntaxHighlighter.module.scss @@ -59,15 +59,40 @@ grid-column: 1 /-1; border-radius: inherit; border: 0; - word-break: break-all; - overflow: hidden; + overflow-y: hidden; + overflow-x: auto; padding: $spacing-05; + white-space: pre; + } + + > .textarea { + padding-inline-start: calc(var(--line-number-width) + #{$spacing-05}); } code { display: block; - inline-size: 100%; - white-space: pre-wrap !important; + min-inline-size: 100%; + inline-size: max-content; + white-space: pre !important; + } + + :global(.linenumber) { + min-inline-size: var(--line-number-width) !important; + white-space: nowrap; + text-align: start !important; + padding-inline-end: $spacing-07 !important; + position: sticky; + inset-inline-start: 0; + &::before { + content: ''; + inline-size: 100%; + background-color: var(--highlight-background); + position: absolute; + inset-block: 0; + inset-inline: -$spacing-05; + z-index: -1; + border-inline-end: 1px solid $border-subtle-00; + } } &.invalid { @@ -76,6 +101,13 @@ padding-inline-end: $spacing-10 !important; } } + + &:focus-within :global(.linenumber) { + &::before { + inline-size: calc(100% - 2px); + inset-inline-start: calc(2px - $spacing-05); + } + } } .textarea { diff --git a/src/components/EditableSyntaxHighlighter/EditableSyntaxHighlighter.tsx b/src/components/EditableSyntaxHighlighter/EditableSyntaxHighlighter.tsx index a4c3fbc..48a636a 100644 --- a/src/components/EditableSyntaxHighlighter/EditableSyntaxHighlighter.tsx +++ b/src/components/EditableSyntaxHighlighter/EditableSyntaxHighlighter.tsx @@ -17,10 +17,18 @@ import { FormLabel } from '@carbon/react'; import { WarningFilled } from '@carbon/react/icons'; import clsx from 'clsx'; -import { CSSProperties, ReactNode, TextareaHTMLAttributes } from 'react'; +import { + CSSProperties, + ReactNode, + TextareaHTMLAttributes, + useEffect, + useRef, + useState, +} from 'react'; import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; import python from 'react-syntax-highlighter/dist/cjs/languages/hljs/python'; import defaultStyle from 'react-syntax-highlighter/dist/cjs/styles/hljs/default-style'; +import { useResizeObserver } from 'usehooks-ts'; import classes from './EditableSyntaxHighlighter.module.scss'; const style: { [key: string]: CSSProperties } = { @@ -122,7 +130,7 @@ interface Props onChange?: (value: string) => void; invalid?: boolean; readOnly?: boolean; - // showLineNumbers?: boolean; + showLineNumbers?: boolean; } export function EditableSyntaxHighlighter({ @@ -132,20 +140,84 @@ export function EditableSyntaxHighlighter({ onChange, invalid, readOnly, + showLineNumbers, className, - // showLineNumbers, ...props }: Props) { + const [lineNumberWidth, setLineNumberWidth] = useState(0); + const rootRef = useRef(null); + const preRef = useRef(null); + const textAreaRef = useRef(null); + + const PreTag = (preProps: any) =>
;
+
+  const syncScroll = () => {
+    requestAnimationFrame(() => {
+      const preElement = preRef.current;
+      const textAreaElement = textAreaRef.current;
+
+      if (preElement && textAreaElement) {
+        preElement.scrollLeft = textAreaElement.scrollLeft;
+      }
+    });
+  };
+
+  const checkLineNumberWidth = () => {
+    if (!preRef.current) {
+      return;
+    }
+
+    const lineNumberElement = [
+      ...preRef.current.querySelectorAll('.linenumber'),
+    ].at(-1);
+
+    setLineNumberWidth(
+      lineNumberElement ? (lineNumberElement as HTMLElement).offsetWidth : 0,
+    );
+  };
+
+  useEffect(() => {
+    const textAreaElement = textAreaRef.current;
+
+    if (textAreaElement) {
+      textAreaElement.addEventListener('scroll', syncScroll);
+      textAreaElement.addEventListener('input', syncScroll);
+      textAreaElement.addEventListener('paste', syncScroll);
+    }
+
+    return () => {
+      if (textAreaElement) {
+        textAreaElement.removeEventListener('scroll', syncScroll);
+        textAreaElement.removeEventListener('input', syncScroll);
+        textAreaElement.removeEventListener('paste', syncScroll);
+      }
+    };
+  }, []);
+
+  useEffect(() => {
+    checkLineNumberWidth();
+  }, [value]);
+
+  useResizeObserver({
+    ref: rootRef,
+    onResize: checkLineNumberWidth,
+  });
+
   return (
-    
+
{labelText && {labelText}} -
+
{value.at(-1) === '\n' ? `${value} ` : value} @@ -153,6 +225,7 @@ export function EditableSyntaxHighlighter({ {!readOnly && (