next.js/packages/next/src/next-devtools/dev-overlay/components/code-frame/code-frame.tsx
code-frame.tsx225 lines6.0 KB
import { useMemo } from 'react'
import { HotlinkedText } from '../hot-linked-text'
import { getStackFrameFile, type StackFrame } from '../../../shared/stack-frame'
import { useOpenInEditor } from '../../utils/use-open-in-editor'
import { ExternalIcon } from '../../icons/external'
import { FileIcon } from '../../icons/file'
import {
  formatCodeFrame,
  groupCodeFrameLines,
  parseLineNumberFromCodeFrameLine,
} from './parse-code-frame'

type CodeFrameProps = {
  stackFrame: StackFrame
  codeFrame: string
}

export function CodeFrame({ stackFrame, codeFrame }: CodeFrameProps) {
  const parsedLineStates = useMemo(() => {
    const decodedLines = groupCodeFrameLines(formatCodeFrame(codeFrame))

    return decodedLines.map((line) => {
      return {
        line,
        parsedLine: parseLineNumberFromCodeFrameLine(line, stackFrame),
      }
    })
  }, [codeFrame, stackFrame])

  const open = useOpenInEditor({
    file: stackFrame.file,
    line1: stackFrame.line1 ?? 1,
    column1: stackFrame.column1 ?? 1,
  })

  const fileExtension = stackFrame?.file?.split('.').pop()

  // TODO: make the caret absolute
  return (
    <div data-nextjs-codeframe>
      <div className="code-frame-header">
        {/* TODO: This is <div> in `Terminal` component.
        Changing now will require multiple test snapshots updates.
        Leaving as <div> as is trivial and does not affect the UI.
        Change when the new redbox matcher `toDisplayRedbox` is used.
        */}
        <p className="code-frame-link">
          <span className="code-frame-icon">
            <FileIcon lang={fileExtension} />
          </span>
          <span data-text>
            {getStackFrameFile(stackFrame)} @{' '}
            <HotlinkedText text={stackFrame.methodName} />
          </span>
          <button
            aria-label="Open in editor"
            data-with-open-in-editor-link-source-file
            onClick={open}
          >
            <span className="code-frame-icon" data-icon="right">
              <ExternalIcon width={16} height={16} />
            </span>
          </button>
        </p>
      </div>
      <pre className="code-frame-pre">
        <div className="code-frame-lines">
          {parsedLineStates.map(({ line, parsedLine }, lineIndex) => {
            const { lineNumber, isErroredLine } = parsedLine

            const lineNumberProps: Record<string, string | boolean> = {}
            if (lineNumber) {
              lineNumberProps['data-nextjs-codeframe-line'] = lineNumber
            }
            if (isErroredLine) {
              lineNumberProps['data-nextjs-codeframe-line--errored'] = true
            }

            return (
              <div key={`line-${lineIndex}`} {...lineNumberProps}>
                {line.map((entry, entryIndex) => (
                  <span
                    key={`frame-${entryIndex}`}
                    style={{
                      color: entry.fg ? `var(--color-${entry.fg})` : undefined,
                      ...(entry.decoration === 'bold'
                        ? // TODO(jiwon): This used to be 800, but the symbols like `─┬─` are
                          // having longer width than expected on Geist Mono font-weight
                          // above 600, hence a temporary fix is to use 500 for bold.
                          { fontWeight: 500 }
                        : entry.decoration === 'italic'
                          ? { fontStyle: 'italic' }
                          : undefined),
                    }}
                  >
                    {entry.content}
                  </span>
                ))}
              </div>
            )
          })}
        </div>
      </pre>
    </div>
  )
}

export const CODE_FRAME_STYLES = `
  [data-nextjs-codeframe] {
    --code-frame-padding: 12px;
    --code-frame-line-height: var(--size-16);
    background-color: var(--color-background-200);
    color: var(--color-gray-1000);
    text-overflow: ellipsis;
    border: 1px solid var(--color-gray-400);
    border-radius: 8px;
    font-family: var(--font-stack-monospace);
    font-size: var(--size-12);
    line-height: var(--code-frame-line-height);
    margin: 8px 0;

    svg {
      width: var(--size-16);
      height: var(--size-16);
    }
  }

  .code-frame-link,
  .code-frame-pre {
    padding: var(--code-frame-padding);
  }

  .code-frame-link svg {
    flex-shrink: 0;
  }

  .code-frame-lines {
    min-width: max-content;
  }

  .code-frame-link [data-text] {
    text-align: left;
    margin: auto 6px;
  }

  .code-frame-header {
    width: 100%;
    transition: background 100ms ease-out;
    border-radius: 8px 8px 0 0;
    border-bottom: 1px solid var(--color-gray-400);
  }

  [data-with-open-in-editor-link-source-file] {
    padding: 4px;
    margin: -4px 0 -4px auto;
    border-radius: var(--rounded-full);
    margin-left: auto;

    &:focus-visible {
      outline: var(--focus-ring);
      outline-offset: -2px;
    }

    &:hover {
      background: var(--color-gray-100);
    }
  }

  [data-nextjs-codeframe]::selection,
  [data-nextjs-codeframe] *::selection {
    background-color: var(--color-ansi-selection);
  }

  [data-nextjs-codeframe] *:not(a) {
    color: inherit;
    background-color: transparent;
    font-family: var(--font-stack-monospace);
  }

  [data-nextjs-codeframe-line][data-nextjs-codeframe-line--errored="true"] {
    position: relative;
    isolation: isolate;

    > span { 
      position: relative;
      z-index: 1;
    }

    &::after {
      content: "";
      width: calc(100% + var(--code-frame-padding) * 2);
      height: var(--code-frame-line-height);
      left: calc(-1 * var(--code-frame-padding));
      background: var(--color-red-200);
      box-shadow: 2px 0 0 0 var(--color-red-900) inset;
      position: absolute;
    }
  }


  [data-nextjs-codeframe] > * {
    margin: 0;
  }

  .code-frame-link {
    display: flex;
    margin: 0;
    outline: 0;
  }
  .code-frame-link [data-icon='right'] {
    margin-left: auto;
  }

  .code-frame-pre {
    overflow-x: auto;
    overflow-y: hidden;
    display: block;
    max-width: 100%;
  }

  [data-nextjs-codeframe] svg {
    color: var(--color-gray-900);
  }
`
Quest for Codev2.0.0
/
SIGN IN