Skip to content
Merged
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
73 changes: 73 additions & 0 deletions src/patches/slashCommands.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';

import {
findSlashCommandListEndPosition,
writeSlashCommandDefinition,
} from './slashCommands';

const anchor =
'var Cmd0={type:"local",name:"clear",description:"Clear the conversation"};';
const items = Array.from({ length: 31 }, (_, i) => `c${i}`).join(',');

const arrowForm = `${anchor}Cmds=memo9(()=>[${items},...Fa?[Fa]:[]])`;
const returnForm = `${anchor}function $tS(){return[${items},...Fa?[Fa]:[]]}`;

describe('findSlashCommandListEndPosition', () => {
it('finds the closing bracket of an arrow-returned command array', () => {
const end = findSlashCommandListEndPosition(arrowForm);

expect(end).not.toBeNull();
expect(arrowForm[end as number]).toBe(']');
expect(arrowForm.slice(end as number)).toBe('])');
});

it('finds the closing bracket of a function-returned command array', () => {
const end = findSlashCommandListEndPosition(returnForm);

expect(end).not.toBeNull();
expect(returnForm[end as number]).toBe(']');
expect(returnForm.slice(end as number)).toBe(']}');
});

it('ignores small arrays and returns null when no command list exists', () => {
expect(
findSlashCommandListEndPosition(`${anchor}function f(){return[a,b,c]}`)
).toBeNull();
});

it('ignores large arrays that are not near command metadata', () => {
expect(
findSlashCommandListEndPosition(`function f(){return[${items}]}`)
).toBeNull();
});

it('picks the largest candidate when several arrays qualify', () => {
const smaller = Array.from({ length: 30 }, (_, i) => `s${i}`).join(',');
const file = `${anchor}function a(){return[${smaller}]}function b(){return[${items}]}`;
const end = findSlashCommandListEndPosition(file);

expect(end).not.toBeNull();
expect(file.slice(end as number)).toBe(']}');
expect(file.slice(0, end as number)).toContain('c30');
});
});

describe('writeSlashCommandDefinition', () => {
it('inserts the definition before the closing bracket of an arrow form', () => {
const result = writeSlashCommandDefinition(arrowForm, ',NEW_CMD');

expect(result).not.toBeNull();
expect(result).toContain(',NEW_CMD])');
});

it('inserts the definition before the closing bracket of a return form', () => {
const result = writeSlashCommandDefinition(returnForm, ',NEW_CMD');

expect(result).not.toBeNull();
expect(result).toContain(',NEW_CMD]}');
});

it('returns null when the command list cannot be located', () => {
expect(writeSlashCommandDefinition('var a=1;', ',NEW_CMD')).toBeNull();
});
});
17 changes: 10 additions & 7 deletions src/patches/slashCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,24 +60,27 @@ const analyzeArrayFromOpenBracket = (
/**
* Find the end position of the slash command array using stack machine.
*
* Supports both pre-2.1.138 form (plain `=>[ID,ID,...]` with 30+ bare
* identifiers) and 2.1.138+ form where the array uses spread operators for
* Supports the pre-2.1.138 form (plain `=>[ID,ID,...]` with 30+ bare
* identifiers), the 2.1.138+ form where the array uses spread operators for
* conditionally-included commands, e.g.:
* =L8(()=>[AUK,pL4,DX4,y64,...gT4?[gT4]:[],Qj4,lI6,vL4,...,W94(),...])
* and the 2.1.227+ form where the list moved into a named function whose body
* returns the array, e.g.:
* function $tS(){return[uOd,Wza,lHf,...t5n("fleetFork"),nEf,...]}
*
* The candidate must also sit in slash-command-specific code. The bundle keeps
* slash-command definitions near command metadata such as name/userFacingName,
* so this rejects unrelated large arrow-return arrays.
* so this rejects unrelated large arrays.
*/
export const findSlashCommandListEndPosition = (
fileContents: string
): number | null => {
// Walk every `=>[` candidate. The slash command array is the (only) array
// following an arrow-return that contains >= 30 top-level items.
const arrowPattern = /=>\s*\[/g;
// Walk every `=>[` and `return[` candidate. The slash command array is the
// (only) array returned from a function that contains >= 30 top-level items.
const candidatePattern = /(?:=>|return)\s*\[/g;
let m: RegExpExecArray | null;
let best: { closing: number; items: number } | null = null;
while ((m = arrowPattern.exec(fileContents)) !== null) {
while ((m = candidatePattern.exec(fileContents)) !== null) {
const bracketIndex = m.index + m[0].length - 1; // position of '['
const anchorWindow = fileContents.slice(
Math.max(0, m.index - 12000),
Expand Down