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

[feat] Add the command usage syntax hint #48

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
26 changes: 24 additions & 2 deletions apps/playground-web/components/Shell/Shell.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
// src/components/Shell/Shell.tsx
'use client';

import React from 'react';
// hooks
import { useShell } from './hooks/useShell';
import { SyntaxPart } from '@/data/commandSyntaxMap';

interface ShellProps {
decreaseCommandsLeft: () => void;
}

const InlineHint = ({ part }: { part: SyntaxPart }) => (
<span className="border-b border-dotted border-gray-600">
{' ' + part.syntax}
</span>
);

export default function Shell({ decreaseCommandsLeft }: ShellProps) {
const {
handleInputChange,
Expand All @@ -16,7 +24,9 @@ export default function Shell({ decreaseCommandsLeft }: ShellProps) {
inputRef,
output,
command,
remainingSyntax,
} = useShell(decreaseCommandsLeft);

return (
<div
ref={terminalRef}
Expand All @@ -34,8 +44,8 @@ export default function Shell({ decreaseCommandsLeft }: ShellProps) {
</div>
))}
<div className="flex items-center">
<p className="text-green-500 mr-2 p-1">dice ~$</p>
<div className="flex-grow">
<p className="text-green-500 mr-2 p-1 flex-shrink-0">dice ~$</p>
<div className="flex-grow relative">
<input
ref={inputRef}
type="text"
Expand All @@ -45,6 +55,18 @@ export default function Shell({ decreaseCommandsLeft }: ShellProps) {
data-testid="shell-input"
className="w-full bg-transparent outline-none text-white"
/>
<div
className="absolute top-0 left-0 text-gray-500 pointer-events-none whitespace-wrap overflow-x-auto"
style={{ paddingLeft: `${command.length + 1}ch ` }}
data-testid="inline-hint"
>
{remainingSyntax.map((part, index) => (
<React.Fragment key={index}>
{index > 0}
<InlineHint part={part} />
</React.Fragment>
))}
</div>
</div>
</div>
</div>
Expand Down
48 changes: 48 additions & 0 deletions apps/playground-web/components/Shell/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,52 @@ describe('Shell Component', () => {
await user.keyboard('[ArrowDown]');
expect(cliInputElement.value).toBe(newCommand);
});

it('should show syntax usage hint for SET', async () => {
const { cliInputElement, user, getByTestId } = setupTest();

const newCommand = 'set';
await user.type(cliInputElement, newCommand);

const inlineHint = getByTestId('inline-hint');
expect(inlineHint.childElementCount).toBe(4);

const inlineHintChild = inlineHint.childNodes;

expect(inlineHintChild[0]).toHaveTextContent('Key');
expect(inlineHintChild[1]).toHaveTextContent('Value');
expect(inlineHintChild[2]).toHaveTextContent('[NX | XX]');
expect(inlineHintChild[3]).toHaveTextContent(
'[EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]',
);
});

it('should show syntax usage hint for GET', async () => {
const { cliInputElement, user, getByTestId } = setupTest();

const newCommand = 'get';
await user.type(cliInputElement, newCommand);

const inlineHint = getByTestId('inline-hint');
expect(inlineHint.childElementCount).toBe(1);

const inlineHintChild = inlineHint.childNodes;

expect(inlineHintChild[0]).toHaveTextContent('Key');
});

it('should show syntax usage hint for DEL', async () => {
const { cliInputElement, user, getByTestId } = setupTest();

const newCommand = 'del';
await user.type(cliInputElement, newCommand);

const inlineHint = getByTestId('inline-hint');
expect(inlineHint.childElementCount).toBe(2);

const inlineHintChild = inlineHint.childNodes;

expect(inlineHintChild[0]).toHaveTextContent('Key');
expect(inlineHintChild[1]).toHaveTextContent('[Key ...]');
});
});
29 changes: 26 additions & 3 deletions apps/playground-web/components/Shell/hooks/useShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { useState, useEffect, useRef, KeyboardEvent, ChangeEvent } from 'react';

// utils
import { handleCommand } from '@/shared/utils/shellUtils';
import blacklistedCommands from '@/shared/utils/blacklist';
import blacklistedCommands from '@/shared/utils/blacklist'; // Assuming you added blacklist here
import { syntaxMap, SyntaxPart } from '@/data/commandSyntaxMap';

export const useShell = (decreaseCommandsLeft: () => void) => {
// states
Expand All @@ -13,6 +14,7 @@ export const useShell = (decreaseCommandsLeft: () => void) => {
// Initialise the command history with sessionStorage
const [commandHistory, setCommandHistory] = useState<string[]>([]);
const [historyIndex, setHistoryIndex] = useState<number>(-1);
const [remainingSyntax, setRemainingSyntax] = useState<SyntaxPart[]>([]);

// useRefs
const terminalRef = useRef<HTMLDivElement>(null);
Expand All @@ -39,6 +41,24 @@ export const useShell = (decreaseCommandsLeft: () => void) => {
decreaseCommandsLeft(); // Call to update remaining commands
};

const updateSyntax = (value: string) => {
const inputParts = value.trim().split(' ');
const command = inputParts[0].toUpperCase();
if (syntaxMap[command]) {
const parts = syntaxMap[command].parts;
if (inputParts.length === 1) {
// Only command typed, show all parts
setRemainingSyntax(parts);
} else {
// Show remaining parts based on what's already typed
const remainingParts = parts.slice(inputParts.length - 1);
setRemainingSyntax(remainingParts);
}
} else {
setRemainingSyntax([]);
}
};

useEffect(() => {
if (terminalRef.current) {
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
Expand All @@ -64,9 +84,10 @@ export const useShell = (decreaseCommandsLeft: () => void) => {
}, []);

const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setCommand(e.target.value);
// Save current input when starting to navigate history
const value = e.target.value;
setCommand(value);
setTempCommand(e.target.value);
updateSyntax(value);
};

const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
Expand All @@ -76,6 +97,7 @@ export const useShell = (decreaseCommandsLeft: () => void) => {
setCommandHistory((prev) => [...prev, command]);
setHistoryIndex(-1);
}
setRemainingSyntax([]);
return;
}

Expand Down Expand Up @@ -125,5 +147,6 @@ export const useShell = (decreaseCommandsLeft: () => void) => {
command,
tempCommand,
setTempCommand,
remainingSyntax,
};
};
56 changes: 56 additions & 0 deletions apps/playground-web/data/commandSyntaxMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export type SyntaxPart = {
syntax: string;
doc: string;
};

export type CommandSyntax = {
parts: SyntaxPart[];
};

export type SyntaxMap = {
[command: string]: CommandSyntax;
};

export const syntaxMap: SyntaxMap = {
SET: {
parts: [
{
syntax: 'Key',
doc: 'The key under which to store the value',
},
{
syntax: 'Value',
doc: 'The value to be stored',
},
{
syntax: '[NX | XX]',
doc: 'NX - Only set if key does not exist. XX - Only set if key exists',
},
{
syntax:
'[EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]',
doc: 'Options to set the key expiration: EX (seconds), PX (milliseconds), EXAT/PXAT (unix timestamp), or KEEPTTL to retain existing TTL',
},
],
},
GET: {
parts: [
{
syntax: 'Key',
doc: 'Key of the value you want to retrive',
},
],
},
DEL: {
parts: [
{
syntax: 'Key',
doc: 'Key that you want to delete',
},
{
syntax: '[Key ...]',
doc: 'Multiple keys you want to delete',
},
],
},
};
Loading