Skip to content

Commit

Permalink
feat: show current value on set commnd (#143)
Browse files Browse the repository at this point in the history
This change adds supporting current value of the property by vim-like
command. You can see the current value of the property `hintchars` by
the following command.

:set hintchars 

This command also support `?`-suffix to show a boolean value:

:set smoothscroll?

If you omit property name, the add-on shows all properties and it's
value:

:set
  • Loading branch information
ueokande authored Jun 18, 2023
1 parent ef71d79 commit afc48b2
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 46 deletions.
98 changes: 77 additions & 21 deletions src/background/command/SetCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type Command from "./Command";
import type { CommandContext, Completions } from "./Command";
import type PropertySettings from "../settings/PropertySettings";
import type PropertyRegistry from "../property/PropertyRegistry";
import type ConsoleClient from "../clients/ConsoleClient";

const mustNumber = (v: any): number => {
const num = Number(v);
Expand All @@ -14,7 +15,8 @@ const mustNumber = (v: any): number => {
class SetCommand implements Command {
constructor(
private readonly propretySettings: PropertySettings,
private readonly propertyRegsitry: PropertyRegistry
private readonly propertyRegsitry: PropertyRegistry,
private readonly consoleClient: ConsoleClient
) {}

names(): string[] {
Expand Down Expand Up @@ -65,43 +67,97 @@ class SetCommand implements Command {
}

async exec(
_ctx: CommandContext,
ctx: CommandContext,
_force: boolean,
args: string
): Promise<void> {
if (args.length === 0) {
return;
// set
return this.showProperties(ctx);
} else if (args.includes("=")) {
// set key=value
const [key, value]: string[] = args.split("=");
return this.setProperty(key, value);
} else if (args.endsWith("?")) {
// set key?
const key = args.slice(0, -1);
return this.showProperty(ctx, key);
} else {
// set key
// set nokey
return this.showPropertyOrSetBoolean(ctx, args);
}
const [name, value] = this.parseSetOption(args);
await this.propretySettings.setProperty(name, value);
}

private parseSetOption(args: string): [string, string | number | boolean] {
let [key, value]: any[] = args.split("=");
if (value === undefined) {
value = !key.startsWith("no");
key = value ? key : key.slice(2);
private async showProperties(ctx: CommandContext): Promise<void> {
const props = this.propertyRegsitry.getProperties();
const kvs = [];
for (const p of props) {
const value = await this.propretySettings.getProperty(p.name());
if (p.type() === "boolean") {
if (value) {
kvs.push(`${p.name()}`);
} else {
kvs.push(`no${p.name()}`);
}
} else {
kvs.push(`${p.name()}=${value}`);
}
}
await this.consoleClient.showInfo(ctx.sender.tabId, kvs.join("\n"));
}

private async showProperty(ctx: CommandContext, key: string): Promise<void> {
const def = this.propertyRegsitry.getProperty(key);
if (!def) {
if (typeof def === "undefined") {
throw new Error("Unknown property: " + key);
}
if (
(def.type() === "boolean" && typeof value !== "boolean") ||
(def.type() !== "boolean" && typeof value === "boolean")
) {
throw new Error("Invalid argument: " + args);
const value = await this.propretySettings.getProperty(key);

if (def.type() === "boolean") {
if (value) {
await this.consoleClient.showInfo(ctx.sender.tabId, key);
} else {
await this.consoleClient.showInfo(ctx.sender.tabId, `no${key}`);
}
} else {
const message = `${key}=${value}`;
await this.consoleClient.showInfo(ctx.sender.tabId, message);
}
}

private async setProperty(key: string, value: string): Promise<void> {
const def = this.propertyRegsitry.getProperty(key);
if (!def) {
throw new Error("Unknown property: " + key);
}
switch (def.type()) {
case "string":
return [key, value];
return this.propretySettings.setProperty(key, value);
case "number":
return [key, mustNumber(value)];
return this.propretySettings.setProperty(key, mustNumber(value));
case "boolean":
return [key, value];
default:
throw new Error("Unknown property type: " + def.type);
throw new Error("Invalid argument: " + value);
}
}

private async showPropertyOrSetBoolean(
ctx: CommandContext,
args: string
): Promise<void> {
const def = this.propertyRegsitry.getProperty(args);
if (def?.type() === "boolean") {
return this.propretySettings.setProperty(def.name(), true);
}
if (typeof def !== "undefined") {
return this.showProperty(ctx, def.name());
}
if (args.startsWith("no")) {
const def2 = this.propertyRegsitry.getProperty(args.slice(2));
if (def2?.type() !== "boolean") {
throw new Error("Invalid argument: " + args);
}
return this.propretySettings.setProperty(def2.name(), false);
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/background/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ export class CommandRegistryFactory {
registory.register(new QuitAllCommand());
registory.register(new QuitCommand());
registory.register(
new SetCommand(this.propertySettings, this.propertyRegistry)
new SetCommand(
this.propertySettings,
this.propertyRegistry,
this.consoleClient
)
);

return registory;
Expand Down
4 changes: 2 additions & 2 deletions src/background/settings/PropertySettings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { injectable, inject } from "inversify";
import PropertyRegistry from "../property/PropertyRegistry";
import SettingsRepository from "./SettingsRepository";
import type PropertyRegistry from "../property/PropertyRegistry";
import type SettingsRepository from "./SettingsRepository";

export default interface PropertySettings {
setProperty(name: string, value: string | number | boolean): Promise<void>;
Expand Down
1 change: 1 addition & 0 deletions src/console/components/InfoMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const Wrapper = styled.p`
background-color: ${({ theme }) => theme.info?.background};
color: ${({ theme }) => theme.info?.foreground};
font-weight: normal;
white-space: pre-wrap;
`;

const InfoMessage: React.FC = ({ children }) => {
Expand Down
164 changes: 143 additions & 21 deletions test/background/command/SetCommand.test.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,181 @@
import SetCommand from "../../../src/background/command/SetCommand";
import { PropertyRegistryFactry } from "../../../src/background/property";
import type { CommandContext } from "../../../src/background/command/Command";
import { PropertyRegistryImpl } from "../../../src/background/property/PropertyRegistry";
import MockPropertySettings from "../mock/MockPropertySettings";
import MockConsoleClient from "../mock/MockConsoleClient";

const strprop1 = {
name: () => "strprop1",
description: () => "",
type: () => "string" as const,
defaultValue: () => "foo",
validate: () => {},
};

const strprop2 = {
name: () => "strprop2",
description: () => "",
type: () => "string" as const,
defaultValue: () => "bar",
validate: () => {},
};

const booleanprop = {
name: () => "booleanprop",
description: () => "",
type: () => "boolean" as const,
defaultValue: () => false,
validate: () => {},
};

describe("SetCommand", () => {
const propertySettings = new MockPropertySettings();
const propertyRegistry = new PropertyRegistryFactry().create();
const propertyRegistry = new PropertyRegistryImpl();
const consoleClient = new MockConsoleClient();
const ctx = {
sender: {
tabId: 10,
},
} as CommandContext;

propertyRegistry.register(strprop1);
propertyRegistry.register(strprop2);
propertyRegistry.register(booleanprop);

describe("exec", () => {
const mockSetProperty = jest.spyOn(propertySettings, "setProperty");
const mockGetProperty = jest.spyOn(propertySettings, "getProperty");
const mockShowInfo = jest.spyOn(consoleClient, "showInfo");
const cmd = new SetCommand(
propertySettings,
propertyRegistry,
consoleClient
);

beforeEach(() => {
mockSetProperty.mockClear();

mockSetProperty.mockResolvedValue();
mockGetProperty.mockClear();
mockShowInfo.mockClear();
mockShowInfo.mockResolvedValue();
});

it("saves string property", async () => {
const cmd = new SetCommand(propertySettings, propertyRegistry);
await cmd.exec({} as any, false, "hintchars=abcdef");
await cmd.exec(ctx, false, "strprop1=newvalue");
expect(mockSetProperty).toHaveBeenCalledWith("strprop1", "newvalue");
});

it("shows string value with non value", async () => {
mockGetProperty.mockResolvedValue("saved-value");
await cmd.exec(ctx, false, "strprop1");
expect(mockShowInfo).toHaveBeenCalledWith(
ctx.sender.tabId,
"strprop1=saved-value"
);
});

it("shows string value with ?-suffix", async () => {
mockGetProperty.mockResolvedValue("saved-value");
await cmd.exec(ctx, false, "strprop1?");
expect(mockShowInfo).toHaveBeenCalledWith(
ctx.sender.tabId,
"strprop1=saved-value"
);
});

expect(mockSetProperty).toHaveBeenCalledWith("hintchars", "abcdef");
it("shows truthly boolean value with ?-suffix", async () => {
mockGetProperty.mockResolvedValue(true);
await cmd.exec(ctx, false, "booleanprop?");
expect(mockShowInfo).toHaveBeenCalledWith(
ctx.sender.tabId,
"booleanprop"
);
});

it("shows falsy boolean value with ?-suffix", async () => {
mockGetProperty.mockResolvedValue(false);
await cmd.exec(ctx, false, "booleanprop?");
expect(mockShowInfo).toHaveBeenCalledWith(
ctx.sender.tabId,
"nobooleanprop"
);
});

it("saves boolean property", async () => {
const cmd = new SetCommand(propertySettings, propertyRegistry);
await cmd.exec(ctx, false, "booleanprop");
expect(mockSetProperty).toHaveBeenCalledWith("booleanprop", true);

await cmd.exec({} as any, false, "smoothscroll");
expect(mockSetProperty).toHaveBeenCalledWith("smoothscroll", true);
await cmd.exec(ctx, false, "nobooleanprop");
expect(mockSetProperty).toHaveBeenCalledWith("booleanprop", false);
});

it("shows all properties", async () => {
mockGetProperty.mockImplementation((name: string) => {
switch (name) {
case "strprop1":
return Promise.resolve("foo");
case "strprop2":
return Promise.resolve("bar");
case "booleanprop":
return Promise.resolve(false);
}
throw new Error("an error");
});
await cmd.exec(ctx, false, "");
expect(mockShowInfo).toHaveBeenCalledWith(
ctx.sender.tabId,
"strprop1=foo\nstrprop2=bar\nnobooleanprop"
);
});

await cmd.exec({} as any, false, "nosmoothscroll");
expect(mockSetProperty).toHaveBeenCalledWith("smoothscroll", false);
it("throws error when invalid boolean statement", async () => {
await expect(cmd.exec(ctx, false, "booleanprop=1")).rejects.toThrowError(
"Invalid"
);
});
});

describe("getCompletions", () => {
it("returns all properties", async () => {
const cmd = new SetCommand(propertySettings, propertyRegistry);
const cmd = new SetCommand(
propertySettings,
propertyRegistry,
consoleClient
);
const completions = await cmd.getCompletions(false, "");
expect(completions).toHaveLength(1);
expect(completions[0].items).toMatchObject([
{ primary: "hintchars", value: "hintchars" },
{ primary: "smoothscroll", value: "smoothscroll" },
{ primary: "nosmoothscroll", value: "nosmoothscroll" },
{ primary: "complete", value: "complete" },
{ primary: "colorscheme", value: "colorscheme" },
{ primary: "strprop1", value: "strprop1" },
{ primary: "strprop2", value: "strprop2" },
{ primary: "booleanprop", value: "booleanprop" },
{ primary: "nobooleanprop", value: "nobooleanprop" },
]);
});

it("returns properties matched with a prefix", async () => {
const cmd = new SetCommand(propertySettings, propertyRegistry);
const completions = await cmd.getCompletions(false, "c");
const cmd = new SetCommand(
propertySettings,
propertyRegistry,
consoleClient
);
const completions = await cmd.getCompletions(false, "str");
expect(completions).toHaveLength(1);
expect(completions[0].items).toMatchObject([
{ primary: "strprop1", value: "strprop1" },
{ primary: "strprop2", value: "strprop2" },
]);
});

it("returns properties matched with 'no' prefix", async () => {
const cmd = new SetCommand(
propertySettings,
propertyRegistry,
consoleClient
);
const completions = await cmd.getCompletions(false, "no");
expect(completions).toHaveLength(1);
expect(completions[0].items).toMatchObject([
{ primary: "complete", value: "complete" },
{ primary: "colorscheme", value: "colorscheme" },
{ primary: "nobooleanprop", value: "nobooleanprop" },
]);
});
});
Expand Down
2 changes: 1 addition & 1 deletion test/background/mock/MockConsoleClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default class MockConsoleClient implements ConsoleClient {
throw new Error("not implemented");
}

showInfo(_tabId: number, _message: string): Promise<any> {
showInfo(_tabId: number, _message: string): Promise<void> {
throw new Error("not implemented");
}
}

0 comments on commit afc48b2

Please sign in to comment.