diff --git a/extensions/notebook-renderers/src/stackTraceHelper.ts b/extensions/notebook-renderers/src/stackTraceHelper.ts index 9238f74222e..e72c3b8a88c 100644 --- a/extensions/notebook-renderers/src/stackTraceHelper.ts +++ b/extensions/notebook-renderers/src/stackTraceHelper.ts @@ -32,9 +32,9 @@ export function formatStackTrace(stack: string) { const formatSequence = /\u001b\[.+?m/g; const fileRegex = /File\s+(?:\u001b\[.+?m)?(.+):(\d+)/; const lineNumberRegex = /((?:\u001b\[.+?m)?[ ->]*?)(\d+)(.*)/; -const cellRegex = /(Cell\s+(?:\u001b\[.+?m)?In\s*\[(\d+)\])(,\s*line \d+)/; +const cellRegex = /(?Cell\s+(?:\u001b\[.+?m)?In\s*\[(?\d+)\],\s*)(?line (?\d+)).*/; // older versions of IPython ~8.3.0 -const inputRegex = /(Input\s+?(?:\u001b\[.+?m)In\s*\[(\d+)\])(.*)/; +const inputRegex = /(?Input\s+?(?:\u001b\[.+?m)(?In\s*\[(?\d+)\]))(?.*)/; function isIpythonStackTrace(stack: string) { return cellRegex.test(stack) || inputRegex.test(stack) || fileRegex.test(stack); @@ -63,25 +63,19 @@ function linkifyStack(stack: string) { continue; } else if (cellRegex.test(original)) { - lines[i] = original.replace(cellRegex, (_s, cellLabel, executionCount, suffix) => { - fileOrCell = { kind: 'cell', path: `vscode-notebook-cell:?execution_count=${stripFormatting(executionCount)}` }; - const lineNumberMatch = /line (\d+)/i.exec(suffix); - if (lineNumberMatch) { - suffix = `, line ${lineNumberMatch[1]}`; - } - return `${stripFormatting(cellLabel)}${suffix}`; - }); + fileOrCell = { + kind: 'cell', + path: stripFormatting(original.replace(cellRegex, 'vscode-notebook-cell:?execution_count=$')) + }; + lines[i] = original.replace(cellRegex, `$\'>line $`); continue; } else if (inputRegex.test(original)) { - lines[i] = original.replace(inputRegex, (_s, cellLabel, executionCount, suffix) => { - fileOrCell = { kind: 'cell', path: `vscode-notebook-cell:?execution_count=${stripFormatting(executionCount)}` }; - const lineNumberMatch = //i.exec(suffix); - if (lineNumberMatch) { - suffix = `, line ${lineNumberMatch[1]}`; - } - return `${stripFormatting(cellLabel)}${suffix}`; - }); + fileOrCell = { + kind: 'cell', + path: stripFormatting(original.replace(inputRegex, 'vscode-notebook-cell:?execution_count=$')) + }; + lines[i] = original.replace(inputRegex, `Input \'>$$`); continue; } else if (!fileOrCell || original.trim() === '') { diff --git a/extensions/notebook-renderers/src/test/stackTraceHelper.test.ts b/extensions/notebook-renderers/src/test/stackTraceHelper.test.ts index cdf0af4f05d..e65efe3ee29 100644 --- a/extensions/notebook-renderers/src/test/stackTraceHelper.test.ts +++ b/extensions/notebook-renderers/src/test/stackTraceHelper.test.ts @@ -6,6 +6,7 @@ import { formatStackTrace } from '../stackTraceHelper'; import * as assert from 'assert'; +// The stack frames for these tests can be retreived by using the raw json for a notebook with an error suite('StackTraceHelper', () => { test('Non Ipython stack trace is left alone', () => { @@ -18,6 +19,11 @@ suite('StackTraceHelper', () => { assert.equal(formatStackTrace(stack), stack); }); + const formatSequence = /\u001b\[.+?m/g; + function stripAsciiFormatting(text: string) { + return text.replace(formatSequence, ''); + } + test('IPython stack line numbers are linkified', () => { const stack = '\u001b[1;31m---------------------------------------------------------------------------\u001b[0m\n' + @@ -31,25 +37,40 @@ suite('StackTraceHelper', () => { '\u001b[1;32m----> 2\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m\n\n' + '\u001b[1;31mException\u001b[0m\n:'; - const formatted = formatStackTrace(stack); - assert.ok(formatted.indexOf('Cell In[3]') > 0, formatted); - assert.ok(formatted.indexOf('line 2') > 0, formatted); - assert.ok(formatted.indexOf('2') > 0, formatted); - assert.ok(formatted.indexOf('2') > 0, formatted); + const formatted = stripAsciiFormatting(formatStackTrace(stack)); + assert.ok(formatted.indexOf('Cell In[3], line 2') > 0, 'Missing line link in ' + formatted); + assert.ok(formatted.indexOf('2') > 0, 'Missing frame link in ' + formatted); + assert.ok(formatted.indexOf('2') > 0, 'Missing frame link in ' + formatted); }); + + test('IPython stack line numbers are linkified for IPython 8.3', () => { + // stack frames within functions do not list the line number, i.e. + // 'Input In [1], in myfunc()' vs + // 'Input In [2], in ()' const stack = '\u001b[1;31m---------------------------------------------------------------------------\u001b[0m\n' + '\u001b[1;31mException\u001b[0m Traceback (most recent call last)\n' + 'Input \u001b[1;32mIn [2]\u001b[0m, in \u001b[0;36m\u001b[1;34m()\u001b[0m\n' + - '\u001b[0;32m 4\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mmyLib\u001b[39;00m\n' + - '\u001b[1;32m----> 5\u001b[0m \u001b[43mmyLib\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mthrowEx\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n'; + '\u001b[0;32m 3\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\'\u001b[39m\u001b[38;5;124mipykernel\u001b[39m\u001b[38;5;124m\'\u001b[39m, ipykernel\u001b[38;5;241m.\u001b[39m__version__)\n' + + '\u001b[0;32m 4\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\'\u001b[39m\u001b[38;5;124mipython\u001b[39m\u001b[38;5;124m\'\u001b[39m, IPython\u001b[38;5;241m.\u001b[39m__version__)\n' + + '\u001b[1;32m----> 5\u001b[0m \u001b[43mmyfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n' + + '\n\n' + + 'Input \u001b[1;32mIn [1]\u001b[0m, in \u001b[0;36mmyfunc\u001b[1;34m()\u001b[0m\n' + + '\u001b[0;32m 3\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mmyfunc\u001b[39m():\n' + + '\u001b[1;32m----> 4\u001b[0m \u001b[43mmyLib\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mthrowEx\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n' + + '\n\n' + + 'File \u001b[1;32mC:\\venvs\\myLib.py:2\u001b[0m, in \u001b[0;36mthrowEx\u001b[1;34m()\u001b[0m\n' + + '\u001b[0;32m 1\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mthrowEx\u001b[39m():\n' + + '\u001b[1;32m----> 2\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m\n' + + '\n' + + '\u001b[1;31mException\u001b[0m:\n'; - const formatted = formatStackTrace(stack); - assert.ok(formatted.indexOf('Input In [2]') > 0, formatted); - assert.ok(formatted.indexOf('line 5') > 0, formatted); - assert.ok(formatted.indexOf('5') > 0, formatted); + const formatted = stripAsciiFormatting(formatStackTrace(stack)); + assert.ok(formatted.indexOf('Input \'>In [2], in ') > 0, 'Missing cell link in ' + formatted); + assert.ok(formatted.indexOf('Input \'>In [1], in myfunc()') > 0, 'Missing cell link in ' + formatted); + assert.ok(formatted.indexOf('5') > 0, 'Missing frame link in ' + formatted); }); test('IPython stack trace lines without associated location are not linkified', () => {