Skip to content

Commit

Permalink
fix(ls): support using relative and absolute paths when using ls auto…
Browse files Browse the repository at this point in the history
…complete
  • Loading branch information
ctaylo21 committed Mar 29, 2020
1 parent 9c25840 commit 5fd13fe
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 53 deletions.
4 changes: 3 additions & 1 deletion src/__tests__/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`autocomplete with tab ls ls tab with argument and relative nested path 1`] = `"<div class=\\"preview-list\\"><span class=\\"ls-preview-file\\">file1.txt</span><span class=\\"ls-preview-file\\">file5.txt</span></div>"`;
exports[`autocomplete with tab ls ls tab with no argument 1`] = `"<div class=\\"preview-list\\"><span class=\\"ls-preview-folder\\">home/</span><span class=\\"ls-preview-folder\\">docs/</span><span class=\\"ls-preview-file\\">file3.txt</span><span class=\\"ls-preview-file\\">file4.txt</span><span class=\\"ls-preview-file\\">blog.txt</span></div>"`;
exports[`cat should list contents of file that contains react component 1`] = `"<li><div id=\\"input-container\\" spellcheck=\\"false\\"><form><span data-testid=\\"input-prompt-path\\">/</span>&nbsp;<span id=\\"inputPrompt\\">$&gt;</span><input aria-label=\\"terminal-input\\" autocomplete=\\"none\\" autocapitalize=\\"none\\" autocorrect=\\"off\\" type=\\"text\\" readonly=\\"\\" value=\\"cat blog.txt\\"></form></div><span class=\\"commandResult\\"><h3>3/22</h3><p>Today is a good day</p></span></li>"`;
Expand Down Expand Up @@ -30,7 +32,7 @@ exports[`ls should correctly return contents for absolute path from nested path
exports[`ls should correctly return contents for given relative directory from nested path 1`] = `"<li><div id=\\"input-container\\" spellcheck=\\"false\\"><form><span data-testid=\\"input-prompt-path\\">/</span>&nbsp;<span id=\\"inputPrompt\\">$&gt;</span><input aria-label=\\"terminal-input\\" autocomplete=\\"none\\" autocapitalize=\\"none\\" autocorrect=\\"off\\" type=\\"text\\" readonly=\\"\\" value=\\"cd home\\"></form></div><span class=\\"commandResult\\"></span></li><li><div id=\\"input-container\\" spellcheck=\\"false\\"><form><span data-testid=\\"input-prompt-path\\">/home</span>&nbsp;<span id=\\"inputPrompt\\">$&gt;</span><input aria-label=\\"terminal-input\\" autocomplete=\\"none\\" autocapitalize=\\"none\\" autocorrect=\\"off\\" type=\\"text\\" readonly=\\"\\" value=\\"ls user\\"></form></div><span class=\\"commandResult\\"><ul class=\\"terminal-ls-list\\"><li class=\\"ls-folder\\"> <span>test</span></li></ul></span></li>"`;
exports[`ls should correctly return contents for given relative directory from root 1`] = `"<li><div id=\\"input-container\\" spellcheck=\\"false\\"><form><span data-testid=\\"input-prompt-path\\">/</span>&nbsp;<span id=\\"inputPrompt\\">$&gt;</span><input aria-label=\\"terminal-input\\" autocomplete=\\"none\\" autocapitalize=\\"none\\" autocorrect=\\"off\\" type=\\"text\\" readonly=\\"\\" value=\\"ls home\\"></form></div><span class=\\"commandResult\\"><ul class=\\"terminal-ls-list\\"><li class=\\"ls-folder\\"> <span>user</span></li><li class=\\"ls-folder\\"> <span>videos</span></li><li class=\\"ls-file\\"> <span>dog.png</span></li><li class=\\"ls-file\\"> <span>file1.txt</span></li></ul></span></li>"`;
exports[`ls should correctly return contents for given relative directory from root 1`] = `"<li><div id=\\"input-container\\" spellcheck=\\"false\\"><form><span data-testid=\\"input-prompt-path\\">/</span>&nbsp;<span id=\\"inputPrompt\\">$&gt;</span><input aria-label=\\"terminal-input\\" autocomplete=\\"none\\" autocapitalize=\\"none\\" autocorrect=\\"off\\" type=\\"text\\" readonly=\\"\\" value=\\"ls home\\"></form></div><span class=\\"commandResult\\"><ul class=\\"terminal-ls-list\\"><li class=\\"ls-folder\\"> <span>user</span></li><li class=\\"ls-folder\\"> <span>videos</span></li><li class=\\"ls-file\\"> <span>dog.png</span></li><li class=\\"ls-file\\"> <span>file1.txt</span></li><li class=\\"ls-file\\"> <span>file5.txt</span></li></ul></span></li>"`;
exports[`ls should handle invalid directory for ls 1`] = `"<li><div id=\\"input-container\\" spellcheck=\\"false\\"><form><span data-testid=\\"input-prompt-path\\">/</span>&nbsp;<span id=\\"inputPrompt\\">$&gt;</span><input aria-label=\\"terminal-input\\" autocomplete=\\"none\\" autocapitalize=\\"none\\" autocorrect=\\"off\\" type=\\"text\\" readonly=\\"\\" value=\\"ls invalid\\"></form></div><span class=\\"commandResult\\">Error: Target folder does not exist</span></li>"`;
Expand Down
35 changes: 32 additions & 3 deletions src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,15 @@ describe('autocomplete with tab', (): void => {
<Terminal fileSystem={exampleFileSystem} />,
);

const input = getByLabelText('terminal-input');
const input = getByLabelText('terminal-input') as HTMLInputElement;
await userEvent.type(input, 'ls ');

const keyDownEvent = new KeyboardEvent('keydown', {
const tabEvent = new KeyboardEvent('keydown', {
bubbles: true,
code: '9',
key: 'Tab',
});
fireEvent(input, keyDownEvent);
fireEvent(input, tabEvent);

const autoCopmleteContent = await findByLabelText(
container,
Expand All @@ -93,6 +93,35 @@ describe('autocomplete with tab', (): void => {
expect(autoCopmleteContent.innerHTML).toContain(item);
});
expect(autoCopmleteContent.innerHTML).toMatchSnapshot();
expect(input.value).toBe('ls ');
});

test('ls tab with argument and relative nested path', async (): Promise<
void
> => {
const { container, getByLabelText } = render(
<Terminal fileSystem={exampleFileSystem} />,
);

const input = getByLabelText('terminal-input') as HTMLInputElement;
await userEvent.type(input, 'ls home/fi');

const tabEvent = new KeyboardEvent('keydown', {
bubbles: true,
code: '9',
key: 'Tab',
});
fireEvent(input, tabEvent);

const autoCopmleteContent = await findByLabelText(
container,
'autocomplete-preview',
);

expect(autoCopmleteContent.innerHTML).toContain('file1.txt');
expect(autoCopmleteContent.innerHTML).toContain('file5.txt');
expect(autoCopmleteContent.innerHTML).toMatchSnapshot();
expect(input.value).toBe('ls home/fi');
});
});

Expand Down
26 changes: 24 additions & 2 deletions src/commands/__tests__/ls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ describe('ls suite', (): void => {
const lsResult = await lsAutoComplete(exampleFileSystem, '/', 'fi');
const { container } = render(lsResult.commandResult as JSX.Element);

expect(container.innerHTML).toContain('file3');
expect(container.innerHTML).toContain('file4');
expect(container.innerHTML).toContain('file3.txt');
expect(container.innerHTML).toContain('file4.txt');
expect(container.innerHTML).not.toContain('blog');
expect(container.innerHTML).not.toContain('docs');
expect(container.innerHTML).not.toContain('home');
Expand All @@ -93,5 +93,27 @@ describe('ls suite', (): void => {

expect(container.innerHTML).toBe('');
});

test('relative path', async (): Promise<void> => {
const lsResult = await lsAutoComplete(exampleFileSystem, '/', 'home/fi');
const { container } = render(lsResult.commandResult as JSX.Element);

expect(container.innerHTML).toContain('file1.txt');
expect(container.innerHTML).toContain('file5.txt');
expect(container.innerHTML).not.toContain('user');
expect(container.innerHTML).not.toContain('videos');
expect(container.innerHTML).not.toContain('dog.png');
});

test('absolute path', async (): Promise<void> => {
const lsResult = await lsAutoComplete(exampleFileSystem, '/', '/home/d');
const { container } = render(lsResult.commandResult as JSX.Element);

expect(container.innerHTML).toContain('dog.png');
expect(container.innerHTML).not.toContain('user');
expect(container.innerHTML).not.toContain('videos');
expect(container.innerHTML).not.toContain('file1.txt');
expect(container.innerHTML).not.toContain('file5.txt');
});
});
});
106 changes: 67 additions & 39 deletions src/commands/ls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,53 @@ import AutoCompleteList from '../components/AutoCompleteList';
import LsResult from '../components/LsResult';
import { CommandResponse, FileSystem, TerminalFolder } from '../index';

export interface LsResultType {
[index: string]: {
type: 'FOLDER' | 'FILE';
};
}

function getTargetFolder(
fileSystem: FileSystem,
currentPath: string,
targetPath: string,
): FileSystem | null {
): FileSystem {
const internalPath = getInternalPath(currentPath, targetPath);

if (internalPath === '/' || !internalPath) {
return fileSystem;
} else if (has(fileSystem, internalPath)) {
return (get(fileSystem, internalPath) as TerminalFolder).children;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return (get(fileSystem, internalPath) as TerminalFolder).children!;
}

throw new Error('Target folder does not exist');
}

export interface LsResultType {
[index: string]: {
type: 'FOLDER' | 'FILE';
};
/**
* Takes an internally formatted directory and formats it into the
* expected format for an ls command. Optionally takes a function to apply
* to the intiial result to filter out certain items.
*
* @param directory {object} - internally formatted directory
* @param filterFn {function} - optional fn to filter certain items
*/
function buildLsFormatDirectory(
directory: FileSystem,
filterFn: (item: LsResultType) => boolean = (): boolean => true,
): LsResultType {
return Object.assign(
{},
...Object.keys(directory)
.map((item) => ({
[directory[item].type === 'FILE'
? `${item}.${directory[item].extension}`
: item]: {
type: directory[item].type,
},
}))
.filter(filterFn),
);
}

/**
Expand All @@ -42,8 +69,6 @@ export default function ls(
targetPath = '',
): Promise<CommandResponse> {
return new Promise((resolve, reject): void => {
const externalFormatDir: LsResultType = {};

let targetFolderContents;
try {
targetFolderContents = getTargetFolder(
Expand All @@ -52,63 +77,66 @@ export default function ls(
targetPath,
);
} catch (e) {
reject(e.message);
return reject(e.message);
}

for (const key in targetFolderContents) {
const lsKey =
targetFolderContents[key].type === 'FILE'
? `${key}.${targetFolderContents[key].extension}`
: key;

externalFormatDir[lsKey] = {
type: targetFolderContents[key].type,
};
}
resolve({
commandResult: <LsResult lsResult={externalFormatDir} />,
commandResult: (
<LsResult lsResult={buildLsFormatDirectory(targetFolderContents)} />
),
});
});
}

/**
* Given a fileysystem, lists all items for a given directory
* Given a fileysystem, current path, and target, list the items in the desired
* folder that start with target string
*
* @param fileSystem {object} - filesystem to ls upon
* @param currentPath {string} - current path within filesystem
* @param target {string} - potentially empty string to match autocomplete against
* @returns Promise<object> - resolves with contents of given path
* @param target {string} - string to match against (maybe be path)
* @returns Promise<object> - resolves with contents that match target in path
*/
function lsAutoComplete(
fileSystem: FileSystem,
currentPath: string,
target = '',
): Promise<CommandResponse> {
return new Promise((resolve): void => {
const externalFormatDir: LsResultType = {};
// Default to searching in currenty directory with simple target
// that contains no path
let autoCompleteMatch = target;
let targetPath = '';

// Handle case where target is a nested path and
// we need to pull off last part of path to match against
const pathParts = target.split('/');
if (pathParts.length > 1) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
autoCompleteMatch = pathParts.pop()!;
targetPath = pathParts.join('/');
}

let targetFolderContents;
try {
targetFolderContents = getTargetFolder(fileSystem, currentPath, '');
targetFolderContents = getTargetFolder(
fileSystem,
currentPath,
targetPath,
);
} catch (e) {
resolve({ commandResult: '' });
return resolve({ commandResult: '' });
}

for (const key in targetFolderContents) {
const lsKey =
targetFolderContents[key].type === 'FILE'
? `${key}.${targetFolderContents[key].extension}`
: key;

if (lsKey.startsWith(target)) {
externalFormatDir[lsKey] = {
type: targetFolderContents[key].type,
};
}
}
const matchFilterFn = (item: LsResultType): boolean =>
Object.keys(item)[0].startsWith(autoCompleteMatch);

resolve({
commandResult: <AutoCompleteList items={externalFormatDir} />,
commandResult: (
<AutoCompleteList
items={buildLsFormatDirectory(targetFolderContents, matchFilterFn)}
/>
),
});
});
}
Expand Down
5 changes: 5 additions & 0 deletions src/data/exampleFileSystem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ const exampleFileSystem: FileSystem = {
content: 'Contents of file 1',
extension: 'txt',
},
file5: {
type: 'FILE',
content: 'Contents of file 5',
extension: 'txt',
},
},
},
docs: {
Expand Down
9 changes: 1 addition & 8 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,7 @@ export class Terminal extends Component<TerminalProps, TerminalState> {
};

const handleTabPress = async (): Promise<void> => {
const {
history,

inputValue,
currentPath,
fileSystem,
} = this.state;
const { history, inputValue, currentPath, fileSystem } = this.state;
const [commandName, ...commandArgs] = inputValue.split(' ');
const commandTarget = commandArgs.pop() || '';

Expand All @@ -173,7 +167,6 @@ export class Terminal extends Component<TerminalProps, TerminalState> {
currentCommandId: this.state.currentCommandId + 1,
currentHistoryId: this.state.currentCommandId,
history: history,
inputValue: '',
tabCompleteResult: commandResult,
},
updatedState,
Expand Down

0 comments on commit 5fd13fe

Please sign in to comment.