Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selection text wrapping in query input #28

Merged
merged 12 commits into from
Dec 15, 2024
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
23 changes: 20 additions & 3 deletions src/content-script/helpres.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import { throws } from './helpres';
import { describe, test } from '@jest/globals';
import { isNotNil, throws } from './helpres';

describe('helpers', () => {
it('should work', () => {
test('should work', () => {
expect(() => throws()).toThrow('Unexpected value');
});

it('should work with a value', () => {
test('should work with a value', () => {
expect(() => throws('Custom message')).toThrow('Custom message');
});
});

describe('isNotNil', () => {
const cases = [
{ value: null, expected: false },
{ value: undefined, expected: false },
{ value: 0, expected: true },
{ value: '', expected: true },
{ value: 'string', expected: true },
{ value: [], expected: true },
{ value: {}, expected: true },
];

test.each(cases)(`should return $value for $expected`, ({ value, expected }) => {
expect(isNotNil(value)).toBe(expected);
});
})
4 changes: 4 additions & 0 deletions src/content-script/helpres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ export function assetTabType(s?: string | null): asserts s is TabType {
throw new Error(`Invalid tab type '${s}'`);
}
}

export const isNotNil = <T>(value: T | null | undefined): value is T => {
return value !== null && value !== undefined;
};
48 changes: 48 additions & 0 deletions src/content-script/ui/query-input/history-manager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from "@jest/globals";
import { HistoryManager } from './history-manager';

describe('HistoryManager', () => {
describe('undo', () => {
it('should undo the last change and return the previous state', () => {
const historyManager = new HistoryManager<number>();
historyManager.save(1);
historyManager.save(2);
expect(historyManager.undo()).toBe(1);
});

it('should return null if there are no previous states', () => {
const historyManager = new HistoryManager<number>();
expect(historyManager.undo()).toBeNull();
});
});

describe('redo', () => {
it('should redo the last undone change and return the restored state', () => {
const historyManager = new HistoryManager<number>();
historyManager.save(1);
historyManager.save(2);
historyManager.undo();
expect(historyManager.redo()).toBe(2);
});

it('should return null if there are no future states', () => {
const historyManager = new HistoryManager<number>();
historyManager.save(1);
historyManager.undo();
expect(historyManager.redo()).toBeNull();
});
});

describe('save', () => {
it('should save a new item in the history and clear any undone changes', () => {
const historyManager = new HistoryManager<number>();
historyManager.save(1);
historyManager.save(2);
historyManager.save(3);
historyManager.undo();
historyManager.undo();
historyManager.save(10)
expect(historyManager.redo()).toEqual(null);
});
});
});
55 changes: 55 additions & 0 deletions src/content-script/ui/query-input/history-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* A class responsible for managing the history of changes.
*
* @template T The type of items being managed.
*/
export class HistoryManager<T> {
private readonly history: T[] = [];
private readonly future: T[] = [];
private current: T | null = null;

/**
* Undoes the last change, returning the previous state.
*
* @returns {T | null} The previous state of the item, or null if there are no previous states.
*/
public undo(): T | null {
return this.swap(this.history, this.future);
}

/**
* Redoes the last undone change, returning the restored state.
*
* @returns {T | null} The restored state of the item, or null if there are no future states.
*/
public redo(): T | null {
return this.swap(this.future, this.history);
}

/**
* Saves a new item in the history and clears any undone changes.
*
* @param {T} item The item to save.
*/
public save(item: T) {
if (this.current) {
this.history.push(this.current);
}
this.current = item;
this.future.length = 0;
}

private swap(a: T[], b: T[]): T | null {
if (!a.length) {
return null;
}

if (this.current) {
b.push(this.current);
}

this.current = a.pop() ?? null;

return this.current;
}
}
59 changes: 59 additions & 0 deletions src/content-script/ui/query-input/query-input.helpres.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, expect, test } from '@jest/globals';
import { isRedoEvent, isSubmitEvent, isUndoEvent, isWrapEvent } from './query-input.helpres';

interface TestCase { event: KeyboardEventInit; expected: boolean }

describe('isUndoEvent', () => {
const cases: TestCase[] = [
{ event: { key: 'z', metaKey: true, shiftKey: false }, expected: true },
{ event: { key: 'z', ctrlKey: true, shiftKey: false }, expected: true },
{ event: { key: 'a', metaKey: true, shiftKey: false }, expected: false },
{ event: { key: 'a', ctrlKey: true, shiftKey: false }, expected: false },
{ event: { key: 'z', metaKey: false, shiftKey: false }, expected: false },
{ event: { key: 'z', ctrlKey: false, shiftKey: false }, expected: false },
];

test.each(cases)('should return $expected for event $event', ({ event, expected }) => {
expect(isUndoEvent(new KeyboardEvent('keydown', event))).toBe(expected);
});
});

describe('isRedoEvent', () => {
const cases: TestCase[] = [
{ event: { key: 'z', metaKey: true, shiftKey: true }, expected: true },
{ event: { key: 'z', ctrlKey: true, shiftKey: true }, expected: true },
{ event: { key: 'z', metaKey: true, shiftKey: false }, expected: false },
{ event: { key: 'a', metaKey: true, shiftKey: true }, expected: false },
];

test.each(cases)('should return $expected for event $event', ({ event, expected }) => {
expect(isRedoEvent(new KeyboardEvent('keydown', event))).toBe(expected);
});
});

describe('isWrapEvent', () => {
const brackets = { '(': ')', '[': ']', '{': '}' };

const cases: TestCase[] = [
{ event: { key: '(' }, expected: true },
{ event: { key: '[' }, expected: true },
{ event: { key: 'a' }, expected: false },
{ event: { key: ')' }, expected: false },
];

test.each(cases)('should return $expected for event $event', ({ event, expected }) => {
expect(isWrapEvent(new KeyboardEvent('keydown', event), brackets)).toBe(expected);
});
});

describe('isSubmitEvent', () => {
const cases: TestCase[] = [
{ event: { key: 'Enter' }, expected: true },
{ event: { key: 'a' }, expected: false },
{ event: { key: 'Tab' }, expected: false },
];

test.each(cases)('should return $expected for event $event', ({ event, expected }) => {
expect(isSubmitEvent(new KeyboardEvent('keydown', event))).toBe(expected);
});
});
23 changes: 23 additions & 0 deletions src/content-script/ui/query-input/query-input.helpres.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const isUndoEvent = (event: KeyboardEvent) => {
return (event.metaKey || event.ctrlKey) && event.key === 'z' && !event.shiftKey;
};

export const isRedoEvent = (event: KeyboardEvent) => {
return (event.metaKey || event.ctrlKey) && event.key === 'z' && event.shiftKey;
};

export const isWrapEvent = (event: KeyboardEvent, brackets: Record<string, string>) => {
return event.key in brackets;
};

export const isSubmitEvent = (event: KeyboardEvent) => {
return event.key === 'Enter';
};

export const isPrintableKey = (event: KeyboardEvent) => {
return event.key.length === 1
&& !event.shiftKey
&& !event.ctrlKey
&& !event.metaKey
&& !event.altKey;
}
Loading
Loading