Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = {
// The tests don't depend on real tokenization, so stub it out.
moduleNameMapper: {
'^tiktoken$': '<rootDir>/tests/ts/__mocks__/tiktoken.ts',
'^strip-ansi$': '<rootDir>/tests/ts/__mocks__/strip-ansi.ts',
'\\.svg$': '<rootDir>/tests/ts/__mocks__/svg.ts',
'^@jupyterlab/apputils$':
'<rootDir>/tests/ts/__mocks__/jupyterlab-apputils.ts',
Expand Down
42 changes: 2 additions & 40 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,12 @@ import { IDocumentManager } from '@jupyterlab/docmanager';
import { FileDialog } from '@jupyterlab/filebrowser';
import { encoding_for_model } from 'tiktoken';
import { NotebookPanel } from '@jupyterlab/notebook';
import stripAnsi from 'strip-ansi';

import { shellSingleQuote } from './shell-utils';

const tiktoken_encoding = encoding_for_model('gpt-4o');

export function removeAnsiChars(str: string): string {
return str.replace(
// eslint-disable-next-line no-control-regex
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
''
);
}

export async function waitForDuration(duration: number): Promise<void> {
return new Promise(resolve => {
setTimeout(() => {
Expand All @@ -28,37 +21,6 @@ export async function waitForDuration(duration: number): Promise<void> {
});
}

export function moveCodeSectionBoundaryMarkersToNewLine(
source: string
): string {
const existingLines = source.split('\n');
const newLines = [];
for (const line of existingLines) {
if (line.length > 3 && line.startsWith('```')) {
newLines.push('```');
let remaining = line.substring(3);
if (remaining.startsWith('python')) {
if (remaining.length === 6) {
continue;
}
remaining = remaining.substring(6);
}
if (remaining.endsWith('```')) {
newLines.push(remaining.substring(0, remaining.length - 3));
newLines.push('```');
} else {
newLines.push(remaining);
}
} else if (line.length > 3 && line.endsWith('```')) {
newLines.push(line.substring(0, line.length - 3));
newLines.push('```');
} else {
newLines.push(line);
}
}
return newLines.join('\n');
}

export function extractLLMGeneratedCode(code: string): string {
// Strip our backend-emitted stream-interruption marker. The Claude inline
// handler pushes it into the same text channel so the diff pane shows
Expand Down Expand Up @@ -122,7 +84,7 @@ export function markdownToComment(source: string): string {
export function formatJupyterError(output: any): string {
const head = `${output.ename ?? 'Error'}: ${output.evalue ?? ''}`.trim();
const tb = Array.isArray(output.traceback)
? output.traceback.map((line: string) => removeAnsiChars(line)).join('\n')
? output.traceback.map((line: string) => stripAnsi(line)).join('\n')
: '';
return tb ? `${head}\n${tb}` : head;
}
Expand Down
13 changes: 13 additions & 0 deletions tests/ts/__mocks__/strip-ansi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>

// strip-ansi ships ESM-only, which jest's CJS test runner can't load
// directly. The real package is a single regex-based string replace, so
// this mock reimplements that regex rather than reworking the jest
// transform pipeline for one dependency.
export default function stripAnsi(str: string): string {
return str.replace(
// eslint-disable-next-line no-control-regex
/[›][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
''
);
}
55 changes: 0 additions & 55 deletions tests/ts/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>

import {
removeAnsiChars,
moveCodeSectionBoundaryMarkersToNewLine,
extractLLMGeneratedCode,
markdownToComment,
compareSelectionPoints,
Expand All @@ -19,59 +17,6 @@ import {
writeTextToClipboard
} from '../../src/utils';

describe('removeAnsiChars', () => {
it('strips colour escape sequences', () => {
const colored = '\u001b[31merror\u001b[0m: oops';
expect(removeAnsiChars(colored)).toBe('error: oops');
});

it('strips cursor-control escape sequences', () => {
expect(removeAnsiChars('hi\u001b[2Athere')).toBe('hithere');
});

it('returns plain strings unchanged', () => {
expect(removeAnsiChars('plain text')).toBe('plain text');
});

it('handles empty input', () => {
expect(removeAnsiChars('')).toBe('');
});
});

describe('moveCodeSectionBoundaryMarkersToNewLine', () => {
it('splits an opening fence that has trailing content', () => {
const input = '```pythonprint("hi")';
expect(moveCodeSectionBoundaryMarkersToNewLine(input)).toBe(
'```\nprint("hi")'
);
});

it('splits a fence that opens and closes on a single line', () => {
const input = '```pythonprint("hi")```';
expect(moveCodeSectionBoundaryMarkersToNewLine(input)).toBe(
'```\nprint("hi")\n```'
);
});

it('drops a redundant language tag when nothing follows it', () => {
expect(moveCodeSectionBoundaryMarkersToNewLine('```python')).toBe('```');
});

it('moves a trailing fence onto its own line', () => {
const input = 'print("hi")```';
expect(moveCodeSectionBoundaryMarkersToNewLine(input)).toBe(
'print("hi")\n```'
);
});

it('strips a redundant python language tag from a well-formed fence', () => {
const input = '```python\nprint("hi")\n```';
expect(moveCodeSectionBoundaryMarkersToNewLine(input)).toBe(
'```\nprint("hi")\n```'
);
});
});

describe('extractLLMGeneratedCode', () => {
it('extracts the body between matched fences', () => {
const wrapped = '```python\nprint("hi")\n```';
Expand Down
Loading