Repository: vercel/next.js
Pull request: [dev-overlay] solidate the line number parsing

### What

Enhance the line break solution of dev overlay code frame from #77078

We parse the tokens and group them into lines by detect the line break token. There's a missing case we didn't notice before where the line break token could contain spaces look like below:

![image](https://github.com/user-attachments/assets/20760ca4-46d9-40bf-9a0f-1cb9a8014fd2)

* The content before `\n` should belong to previous line
* The content after `\n` should belong to the next line

Also improved the memoization situation in the code frame to avoid re-parsing the lines and tokens.

Closes NDX-1042

Changed files:
filename: packages/next/src/client/components/react-dev-overlay/ui/components/code-frame/code-frame.tsx
patch: @@ -14,14 +14,16 @@ import {
 export type CodeFrameProps = { stackFrame: StackFrame; codeFrame: string }
 
 export function CodeFrame({ stackFrame, codeFrame }: CodeFrameProps) {
-  const formattedFrame = useMemo<string>(
-    () => formatCodeFrame(codeFrame),
-    [codeFrame]
-  )
-  const decodedLines = useMemo(
-    () => groupCodeFrameLines(formattedFrame),
-    [formattedFrame]
-  )
+  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,
@@ -60,9 +62,8 @@ export function CodeFrame({ stackFrame, codeFrame }: CodeFrameProps) {
         </p>
       </div>
       <pre className="code-frame-pre">
-        {decodedLines.map((line, lineIndex) => {
-          const { lineNumber, isErroredLine } =
-            parseLineNumberFromCodeFrameLine(line, stackFrame)
+        {parsedLineStates.map(({ line, parsedLine }, lineIndex) => {
+          const { lineNumber, isErroredLine } = parsedLine
 
           const lineNumberProps: Record<string, string | boolean> = {}
           if (lineNumber) {

filename: packages/next/src/client/components/react-dev-overlay/ui/components/code-frame/parse-code-frame.test.ts
patch: @@ -49,4 +49,45 @@ describe('parse line numbers', () => {
       isErroredLine: false,
     })
   })
+
+  it('parse line numbers when a token contains both break and spaces', () => {
+    //   4 |   return (
+    //   5 |     <p>
+    // > 6 |       <p>nest</p>
+    //     |       ^
+    //   7 |     </p>
+    //   8 |   )
+    //   9 | }
+    const input = {
+      stackFrame: {
+        file: 'app/page.tsx',
+        lineNumber: 6,
+        column: 7,
+        methodName: 'Page',
+        arguments: [],
+      },
+      codeFrame: `"\u001b[0m \u001b[90m 4 |\u001b[39m   \u001b[36mreturn\u001b[39m (\n \u001b[90m 5 |\u001b[39m     \u001b[33m<\u001b[39m\u001b[33mp\u001b[39m\u001b[33m>\u001b[39m\n\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 6 |\u001b[39m       \u001b[33m<\u001b[39m\u001b[33mp\u001b[39m\u001b[33m>\u001b[39mnest\u001b[33m<\u001b[39m\u001b[33m/\u001b[39m\u001b[33mp\u001b[39m\u001b[33m>\u001b[39m\n \u001b[90m   |\u001b[39m       \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\n \u001b[90m 7 |\u001b[39m     \u001b[33m<\u001b[39m\u001b[33m/\u001b[39m\u001b[33mp\u001b[39m\u001b[33m>\u001b[39m\n \u001b[90m 8 |\u001b[39m   )\n \u001b[90m 9 |\u001b[39m }\u001b[0m"`,
+    }
+    const formattedFrame = formatCodeFrame(input.codeFrame)
+    const decodedLines = groupCodeFrameLines(formattedFrame)
+
+    expect(
+      parseLineNumberFromCodeFrameLine(decodedLines[1], input.stackFrame)
+    ).toEqual({
+      lineNumber: '5',
+      isErroredLine: false,
+    })
+    expect(
+      parseLineNumberFromCodeFrameLine(decodedLines[2], input.stackFrame)
+    ).toEqual({
+      lineNumber: '6',
+      isErroredLine: true,
+    })
+    expect(
+      parseLineNumberFromCodeFrameLine(decodedLines[4], input.stackFrame)
+    ).toEqual({
+      lineNumber: '7',
+      isErroredLine: false,
+    })
+  })
 })

filename: packages/next/src/client/components/react-dev-overlay/ui/components/code-frame/parse-code-frame.ts
patch: @@ -43,9 +43,24 @@ export function groupCodeFrameLines(formattedFrame: string) {
 
   let line: typeof decoded = []
   for (const token of decoded) {
-    if (token.content === '\n') {
-      lines.push(line)
-      line = []
+    // If the token is a new line with only line break "\n",
+    // break here into a new line.
+    // The line could also contain spaces, it's still considered line break if "\n" line has spaces.
+    if (typeof token.content === 'string' && token.content.includes('\n')) {
+      const segments = token.content.split('\n')
+      for (let i = 0; i < segments.length; i++) {
+        const segment = segments[i]
+        if (segment) {
+          line.push({
+            ...token,
+            content: segment,
+          })
+        }
+        if (i < segments.length - 1) {
+          lines.push(line)
+          line = []
+        }
+      }
     } else {
       line.push(token)
     }
@@ -66,7 +81,6 @@ export function parseLineNumberFromCodeFrameLine(
   // parse line number from line first 2 tokens
   // e.g. ` > 1 | const foo = 'bar'` => `1`, first token is `1 |`
   // e.g. `  2 | const foo = 'bar'` => `2`. first 2 tokens are ' ' and ' 2 |'
-  // console.log('line', line)
   if (line[0]?.content === '>' || line[0]?.content === ' ') {
     lineNumberToken = line[1]
     lineNumber = lineNumberToken?.content?.replace('|', '')?.trim()