Skip to content

Commit

Permalink
feat: check transformation result when typing
Browse files Browse the repository at this point in the history
This means that the transformation function for the current argument
will be executed on the current text for the argument. If an error
occurs during transformation, it will be displayed in the argument
suggestion.

There is also now an `expensive` flag for types, indicating if this
functionality should be disabled for the type.
  • Loading branch information
paradoxuum committed Jan 11, 2024
1 parent c146a1f commit d7e9172
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 58 deletions.
15 changes: 15 additions & 0 deletions src/client/interface/app/suggestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,28 @@ export function getArgumentSuggestion(
);
}

// If the type is not marked as "expensive", transform the text into the type
// If the transformation fails, include the error message in the suggestion
let errorText: string | undefined;
try {
if (!typeObject.expensive) {
const transformResult = typeObject.transform(text ?? "");
if (transformResult.isErr()) {
errorText = transformResult.unwrapErr();
}
}
} catch {
errorText = "Failed to transform type";
}

return {
main: {
type: "argument",
title: arg.name,
description: arg.description,
dataType: typeObject.name,
optional: arg.optional ?? false,
error: errorText,
},
others: typeSuggestions,
};
Expand Down
10 changes: 4 additions & 6 deletions src/client/interface/components/terminal/TerminalTextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { useRem } from "../../hooks/useRem";
import { useStore } from "../../hooks/useStore";
import { CommanderContext } from "../../providers/commanderProvider";
import { SuggestionContext } from "../../providers/suggestionProvider";
import { selectCommand, selectErrorText, selectVisible } from "../../store/app";
import { selectCommand, selectValid, selectVisible } from "../../store/app";
import { getArgumentNames } from "../../util/argument";
import { Frame } from "../interface/Frame";
import { Padding } from "../interface/Padding";
Expand Down Expand Up @@ -92,14 +92,12 @@ export function TerminalTextField({
}, [appVisible]);

useMountEffect(() => {
let errored = false;
store.subscribe(selectErrorText, (text) => {
errored = text !== undefined;
setValid(!errored);
store.subscribe(selectValid, (valid) => {
setValid(valid);
});

store.subscribe(selectCommand, (command) => {
if (errored) return;
if (!store.getState().app.valid) return;
setValid(command !== undefined);
});
});
Expand Down
3 changes: 1 addition & 2 deletions src/client/interface/components/terminal/TerminalWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,7 @@ export function TerminalWindow() {
: undefined;

if (commandPath !== undefined && command !== undefined) {
const argCheckMessage = checkArgs(commandPath, command);
store.setErrorText(argCheckMessage);
store.setValid(checkArgs(commandPath, command) === undefined);
}
} else {
store.setCommand(undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import { Group } from "../../interface/Group";
import { Padding } from "../../interface/Padding";
import { Text } from "../../interface/Text";
import { Badge } from "./Badge";
import { SuggestionSizes } from "./types";
import { SuggestionTextBounds } from "./types";
import { highlightMatching } from "./util";

export interface MainSuggestionProps {
suggestion?: Suggestion;
argument: BindingOrValue<boolean>;
currentText?: string;
size: BindingOrValue<UDim2>;
sizes: Binding<SuggestionSizes>;
sizes: Binding<SuggestionTextBounds>;
}

export function MainSuggestion({
Expand Down Expand Up @@ -109,6 +109,22 @@ export function MainSuggestion({
textWrapped={true}
richText={true}
/>

<Text
key="error"
anchorPoint={new Vector2(0, 1)}
size={sizes.map((val) => new UDim2(1, 0, 0, val.errorTextHeight))}
position={UDim2.fromScale(0, 1)}
text={
argument && suggestion !== undefined
? (suggestion.main as ArgumentSuggestion).error ?? ""
: ""
}
textColor={palette.red}
textSize={rem(1.5)}
textWrapped={true}
textXAlignment="Left"
/>
</Frame>
);
}
67 changes: 27 additions & 40 deletions src/client/interface/components/terminal/suggestion/Suggestions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getBindingValue } from "@rbxts/pretty-react-hooks";
import { useSelector } from "@rbxts/react-reflex";
import Roact, {
useBinding,
Expand All @@ -7,7 +6,7 @@ import Roact, {
useMemo,
} from "@rbxts/roact";
import { TextService } from "@rbxts/services";
import { DEFAULT_FONT, fonts } from "../../../constants/fonts";
import { DEFAULT_FONT } from "../../../constants/fonts";
import { palette } from "../../../constants/palette";
import { springs } from "../../../constants/springs";
import { useMotion } from "../../../hooks/useMotion";
Expand All @@ -19,8 +18,8 @@ import { Group } from "../../interface/Group";
import { Padding } from "../../interface/Padding";
import { Text } from "../../interface/Text";
import { MainSuggestion } from "./MainSuggestion";
import { SuggestionSizes } from "./types";
import { highlightMatching } from "./util";
import { SuggestionTextBounds } from "./types";
import { getSuggestionTextBounds, highlightMatching } from "./util";

export interface SuggestionListProps {
position?: UDim2;
Expand All @@ -47,9 +46,10 @@ export function SuggestionList({ position }: SuggestionListProps) {

// Suggestions
const suggestion = useContext(SuggestionContext).suggestion;
const [sizes, setSizes] = useBinding<SuggestionSizes>({
const [sizes, setSizes] = useBinding<SuggestionTextBounds>({
title: UDim2.fromOffset(rem(16), rem(2)),
description: UDim2.fromOffset(rem(16), rem(2)),
errorTextHeight: 0,
typeBadgeWidth: rem(6),
});

Expand All @@ -72,48 +72,35 @@ export function SuggestionList({ position }: SuggestionListProps) {
otherSuggestionSizeMotion.spring(new UDim2());
}

textBoundsParams.Text = mainSuggestion.title;
textBoundsParams.Font = fonts.inter.bold;
textBoundsParams.Size = rem(2);
textBoundsParams.Width = rem(16);

const titleBounds = TextService.GetTextBoundsAsync(textBoundsParams);
const textBounds = getSuggestionTextBounds(
mainSuggestion,
rem(2),
rem(1.5),
rem(16),
rem(8),
);

let descriptionBounds: Vector2;
if (mainSuggestion.description !== undefined) {
textBoundsParams.Text = mainSuggestion.description;
textBoundsParams.Font = DEFAULT_FONT;
textBoundsParams.Size = rem(1.5);
descriptionBounds = TextService.GetTextBoundsAsync(textBoundsParams);
} else {
descriptionBounds = new Vector2();
}
setSizes(textBounds);

let windowWidth = math.max(titleBounds.X, descriptionBounds.X) + rem(2);
let windowHeight = titleBounds.Y + descriptionBounds.Y + rem(2);
let windowWidth =
math.max(textBounds.title.X.Offset, textBounds.description.X.Offset) +
textBounds.typeBadgeWidth +
rem(6);

// If the suggestion is an argument, calculate the data type text bounds
// and add it to the size of the suggestion window
let typeBadgeBounds: Vector2 | undefined;
if (mainSuggestion.type === "argument") {
textBoundsParams.Text = mainSuggestion.dataType;
textBoundsParams.Font = DEFAULT_FONT;
textBoundsParams.Size = rem(1.5);
textBoundsParams.Width = rem(8);
let windowHeight =
textBounds.title.Y.Offset +
textBounds.description.Y.Offset +
textBounds.errorTextHeight +
rem(2);

typeBadgeBounds = TextService.GetTextBoundsAsync(textBoundsParams);
windowWidth += typeBadgeBounds.X + rem(4);
if (textBounds.typeBadgeWidth > 0) {
windowHeight += rem(1);
windowWidth += rem(0.5);
}

setSizes({
title: UDim2.fromOffset(titleBounds.X, titleBounds.Y),
description: UDim2.fromOffset(descriptionBounds.X, descriptionBounds.Y),
typeBadgeWidth:
typeBadgeBounds !== undefined
? typeBadgeBounds.X + rem(2)
: getBindingValue(sizes).typeBadgeWidth,
});
if (textBounds.errorTextHeight > 0) {
windowHeight += rem(0.5);
}

// Calculate other suggestion sizes
if (!otherSuggestions.isEmpty()) {
Expand Down
3 changes: 2 additions & 1 deletion src/client/interface/components/terminal/suggestion/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface SuggestionSizes {
export interface SuggestionTextBounds {
title: UDim2;
description: UDim2;
errorTextHeight: number;
typeBadgeWidth: number;
}
53 changes: 53 additions & 0 deletions src/client/interface/components/terminal/suggestion/util.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { TextService } from "@rbxts/services";
import { DEFAULT_FONT, fonts } from "../../../constants/fonts";
import { palette } from "../../../constants/palette";
import { ArgumentSuggestion, CommandSuggestion } from "../../../types";
import { toHex } from "../../../util/color";
import { SuggestionTextBounds } from "./types";

const HIGHLIGHT_PREFIX = `<font color="${toHex(palette.blue)}">`;
const TEXT_BOUNDS_PARAMS = new Instance("GetTextBoundsParams");
const DEFAULT_BOUNDS = new Vector2();

export function highlightMatching(text?: string, terminalText?: string) {
if (text === undefined) return "";
Expand All @@ -15,3 +21,50 @@ export function highlightMatching(text?: string, terminalText?: string) {
const unhighlightedText = text.sub(terminalText.size() + 1);
return `${HIGHLIGHT_PREFIX}${subText}</font>${unhighlightedText}`;
}

export function getSuggestionTextBounds(
suggestion: ArgumentSuggestion | CommandSuggestion,
titleTextSize: number,
textSize: number,
maxWidth: number,
maxBadgeWidth: number,
): SuggestionTextBounds {
// Get title text bounds
TEXT_BOUNDS_PARAMS.Text = suggestion.title;
TEXT_BOUNDS_PARAMS.Size = titleTextSize;
TEXT_BOUNDS_PARAMS.Font = fonts.inter.bold;
TEXT_BOUNDS_PARAMS.Width = maxWidth;
const titleBounds = TextService.GetTextBoundsAsync(TEXT_BOUNDS_PARAMS);

// Get description text bounds
TEXT_BOUNDS_PARAMS.Size = textSize;
TEXT_BOUNDS_PARAMS.Font = DEFAULT_FONT;

let descriptionBounds = DEFAULT_BOUNDS;
if (suggestion.description !== undefined) {
TEXT_BOUNDS_PARAMS.Text = suggestion.description;
descriptionBounds = TextService.GetTextBoundsAsync(TEXT_BOUNDS_PARAMS);
}

let errorTextHeight = 0;
let typeBadgeWidth = 0;
if (suggestion.type === "argument") {
// Get error text bounds
if (suggestion.error !== undefined) {
TEXT_BOUNDS_PARAMS.Text = suggestion.error ?? "";
errorTextHeight = TextService.GetTextBoundsAsync(TEXT_BOUNDS_PARAMS).Y;
}

// Get type badge bounds
TEXT_BOUNDS_PARAMS.Text = suggestion.dataType;
TEXT_BOUNDS_PARAMS.Width = maxBadgeWidth;
typeBadgeWidth = TextService.GetTextBoundsAsync(TEXT_BOUNDS_PARAMS).X;
}

return {
title: UDim2.fromOffset(titleBounds.X, titleBounds.Y),
description: UDim2.fromOffset(descriptionBounds.X, descriptionBounds.Y),
errorTextHeight,
typeBadgeWidth,
};
}
4 changes: 2 additions & 2 deletions src/client/interface/store/app/appSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ export const selectVisible = (state: RootState) => state.app.visible;

export const selectText = (state: RootState) => state.app.text;

export const selectErrorText = (state: RootState) => state.app.errorText;

export const selectCommand = (state: RootState) => state.app.command;

export const selectValid = (state: RootState) => state.app.valid;
9 changes: 5 additions & 4 deletions src/client/interface/store/app/appSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface AppState {
parts: string[];
index: number;
};
errorText?: string;
valid: boolean;
}

export const initialAppState: AppState = {
Expand All @@ -28,10 +28,11 @@ export const initialAppState: AppState = {
parts: [],
index: -1,
},
valid: false,
};

/**
* Limits an array by removing the first n (limit) elments if
* Limits an array by removing the first n (limit) elements if
* the array's size exceeds the limit.
*
* @param array The array to limit
Expand Down Expand Up @@ -88,8 +89,8 @@ export const appSlice = createProducer(initialAppState, {
};
},

setErrorText: (state, text?: string) => ({
setValid: (state, valid: boolean) => ({
...state,
errorText: text,
valid,
}),
});
1 change: 1 addition & 0 deletions src/client/interface/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface ArgumentSuggestion {
description?: string;
dataType: string;
optional: boolean;
error?: string;
}

export interface Suggestion {
Expand Down
1 change: 1 addition & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type TransformationResult<T extends defined> = Result<T, string>;

export interface TypeOptions<T extends defined> {
name: string;
expensive: boolean;
validate: t.check<T>;
transform: (text: string) => TransformationResult<T>;
suggestions?: (text: string) => string[];
Expand Down
Loading

0 comments on commit d7e9172

Please sign in to comment.