Skip to content

Commit 7a8ee3e

Browse files
authored
feat: pin/unpin/togglepin command (#487)
This PR add the three command for tab pinning - `pin` ... Pin the current tab or specific tab which is matched with the keyword. - `unpin` ... Unpin the current tab or specific tab which is matched with the keyword. - `togglepin` ... Toggle the pinning of the current tab which is matched with the keyword. Close #478
1 parent 433affd commit 7a8ee3e

16 files changed

+543
-101
lines changed

docs/console_commands.md

+27
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,30 @@ the current page.
9898
The `:set` command can be used to temporarily override properties in the
9999
console. See the [properties](./properties) section for more details on
100100
the available properties.
101+
102+
## `:pin`
103+
104+
The `:pin` command pins the current tab or specific tab by URL or title keywords.
105+
106+
```
107+
:pin " pin the current tab
108+
:pin foobar " pin the tab with "foobar" in the title or URL
109+
```
110+
111+
## `:unpin`
112+
113+
The `:unpin` command unpins the current tab or specific tab by URL or title keywords.
114+
115+
```
116+
:unpin " unpin the current tab
117+
:unpin foobar " unpin the tab with "foobar" in the title or URL
118+
```
119+
120+
## `:togglepin`
121+
122+
The `:togglepin` command toggles pinning of the current tab or specific tab by URL or title keywords.
123+
124+
```
125+
:togglepin " toggle pinning of the current tab
126+
:togglepin foobar " toggle pinning of the tab with "foobar" in the title or URL
127+
```

src/background/command/BufferCommand.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import type { Command, CommandContext, Completions } from "./types";
22
import type { LastSelectedTabRepository } from "../repositories/LastSelectedTabRepository";
3-
import { BufferCommandHelper } from "./BufferCommandHelper";
3+
import type { TabQueryHelper } from "./TabQueryHelper";
44

55
export class BufferCommand implements Command {
66
constructor(
77
private readonly lastSelectedTabRepository: LastSelectedTabRepository,
8-
private readonly bufferCommandHelper = new BufferCommandHelper(
9-
lastSelectedTabRepository,
10-
),
8+
private readonly tabQueryHelper: TabQueryHelper,
119
) {}
1210

1311
names(): string[] {
@@ -23,7 +21,9 @@ export class BufferCommand implements Command {
2321
}
2422

2523
async getCompletions(_force: boolean, query: string): Promise<Completions> {
26-
return this.bufferCommandHelper.getCompletions(true, query);
24+
return this.tabQueryHelper.getCompletions(query, {
25+
includePinned: true,
26+
});
2727
}
2828

2929
// eslint-disable-next-line max-statements
@@ -67,7 +67,9 @@ export class BufferCommand implements Command {
6767
currentWindow: true,
6868
active: true,
6969
});
70-
const tabs = await this.bufferCommandHelper.queryTabs(true, keywords);
70+
const tabs = await this.tabQueryHelper.queryTabs(keywords, {
71+
includePinned: true,
72+
});
7173
if (tabs.length === 0) {
7274
throw new RangeError("No matching buffer for " + keywords);
7375
}

src/background/command/BufferDeleteCommand.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Command, CommandContext, Completions } from "./types";
2-
import type { BufferCommandHelper } from "./BufferCommandHelper";
2+
import type { TabQueryHelper } from "./TabQueryHelper";
33

44
export class BufferDeleteCommand implements Command {
5-
constructor(private readonly bufferCommandHelper: BufferCommandHelper) {}
5+
constructor(private readonly tabQueryHelper: TabQueryHelper) {}
66

77
names(): string[] {
88
return ["bd", "bdel", "bdelete"];
@@ -17,7 +17,9 @@ export class BufferDeleteCommand implements Command {
1717
}
1818

1919
async getCompletions(force: boolean, query: string): Promise<Completions> {
20-
return this.bufferCommandHelper.getCompletions(force, query);
20+
return this.tabQueryHelper.getCompletions(query, {
21+
includePinned: force,
22+
});
2123
}
2224

2325
async exec(
@@ -26,7 +28,9 @@ export class BufferDeleteCommand implements Command {
2628
args: string,
2729
): Promise<void> {
2830
const keywords = args.trim();
29-
const tabs = await this.bufferCommandHelper.queryTabs(force, keywords);
31+
const tabs = await this.tabQueryHelper.queryTabs(keywords, {
32+
includePinned: force,
33+
});
3034
if (tabs.length === 0) {
3135
throw new Error("No matching buffer for " + keywords);
3236
} else if (tabs.length > 1) {

src/background/command/BufferDeletesCommand.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Command, CommandContext, Completions } from "./types";
2-
import type { BufferCommandHelper } from "./BufferCommandHelper";
2+
import type { TabQueryHelper } from "./TabQueryHelper";
33

44
export class BufferDeletesCommand implements Command {
5-
constructor(private readonly bufferCommandHelper: BufferCommandHelper) {}
5+
constructor(private readonly tabQueryHelper: TabQueryHelper) {}
66

77
names(): string[] {
88
return ["bdeletes"];
@@ -17,7 +17,9 @@ export class BufferDeletesCommand implements Command {
1717
}
1818

1919
async getCompletions(force: boolean, query: string): Promise<Completions> {
20-
return this.bufferCommandHelper.getCompletions(force, query);
20+
return this.tabQueryHelper.getCompletions(query, {
21+
includePinned: force,
22+
});
2123
}
2224

2325
async exec(
@@ -26,7 +28,9 @@ export class BufferDeletesCommand implements Command {
2628
args: string,
2729
): Promise<void> {
2830
const keywords = args.trim();
29-
const tabs = await this.bufferCommandHelper.queryTabs(force, keywords);
31+
const tabs = await this.tabQueryHelper.queryTabs(keywords, {
32+
includePinned: force,
33+
});
3034
if (tabs.length === 0) {
3135
throw new Error("No matching buffer for " + keywords);
3236
}

src/background/command/PinCommand.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { Command, CommandContext, Completions } from "./types";
2+
import type { TabQueryHelper } from "./TabQueryHelper";
3+
import type { ConsoleClient } from "../clients/ConsoleClient";
4+
5+
export class PinCommand implements Command {
6+
constructor(
7+
private readonly tabQueryHelper: TabQueryHelper,
8+
private readonly consoleClient: ConsoleClient,
9+
) {}
10+
11+
names(): string[] {
12+
return ["pin"];
13+
}
14+
15+
fullname(): string {
16+
return "pin";
17+
}
18+
19+
description(): string {
20+
return "Make a tab pinned";
21+
}
22+
23+
getCompletions(_force: boolean, query: string): Promise<Completions> {
24+
return this.tabQueryHelper.getCompletions(query, {
25+
includePinned: true,
26+
});
27+
}
28+
29+
async exec(
30+
{ sender }: CommandContext,
31+
_force: boolean,
32+
args: string,
33+
): Promise<void> {
34+
let targetTabId: number | undefined;
35+
const keywords = args.trim();
36+
if (keywords.length === 0) {
37+
targetTabId = sender.tabId;
38+
} else {
39+
const tabs = await this.tabQueryHelper.queryTabs(keywords, {
40+
includePinned: true,
41+
});
42+
if (tabs.length === 0) {
43+
throw new Error("No matching buffer for " + keywords);
44+
}
45+
targetTabId = tabs[0].id!;
46+
}
47+
48+
await chrome.tabs.update(targetTabId, { pinned: true });
49+
await this.consoleClient.showInfo(sender.tabId, "Pinned tab");
50+
}
51+
}

src/background/command/BufferCommandHelper.ts renamed to src/background/command/TabQueryHelper.ts

+20-18
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import type { Completions } from "../../shared/completions";
22
import type { LastSelectedTabRepository } from "../repositories/LastSelectedTabRepository";
33

4-
export class BufferCommandHelper {
4+
type TabOption = {
5+
includePinned: boolean;
6+
};
7+
8+
export class TabQueryHelper {
59
constructor(
610
private readonly lastSelectedTabRepository: LastSelectedTabRepository,
711
) {}
812

9-
async getCompletions(force: boolean, query: string): Promise<Completions> {
13+
// async getCompletions(force: boolean, query: string): Promise<Completions> {
14+
async getCompletions(query: string, opts: TabOption): Promise<Completions> {
1015
const lastTabId =
1116
await this.lastSelectedTabRepository.getLastSelectedTabId();
12-
const allTabs = await this.getAllTabs(force);
17+
const allTabs = await this.getAllTabs(opts);
1318
const num = parseInt(query, 10);
1419
let tabs: chrome.tabs.Tab[] = [];
1520
if (!isNaN(num)) {
@@ -28,7 +33,7 @@ export class BufferCommandHelper {
2833
tabs = [tab];
2934
}
3035
} else {
31-
tabs = await this.queryTabs(force, query);
36+
tabs = await this.queryTabs(query, opts);
3237
}
3338

3439
const items = tabs.map((tab) => {
@@ -50,28 +55,25 @@ export class BufferCommandHelper {
5055
return [{ name: "Buffers", items }];
5156
}
5257

53-
async queryTabs(force: boolean, query: string): Promise<chrome.tabs.Tab[]> {
54-
const tabs = await chrome.tabs.query({ currentWindow: true });
55-
const matched = tabs
58+
async queryTabs(query: string, opts: TabOption): Promise<chrome.tabs.Tab[]> {
59+
const tabs = await chrome.tabs.query({
60+
currentWindow: true,
61+
pinned: opts.includePinned ? undefined : false,
62+
});
63+
return tabs
5664
.filter((t) => {
5765
return (
5866
(t.url && t.url.toLowerCase().includes(query.toLowerCase())) ||
5967
(t.title && t.title.toLowerCase().includes(query.toLowerCase()))
6068
);
6169
})
6270
.filter((item) => item.id && item.title && item.url);
63-
64-
if (force) {
65-
return matched;
66-
}
67-
return matched.filter((tab) => !tab.pinned);
6871
}
6972

70-
private async getAllTabs(force: boolean): Promise<chrome.tabs.Tab[]> {
71-
const tabs = await chrome.tabs.query({ currentWindow: true });
72-
if (force) {
73-
return tabs;
74-
}
75-
return tabs.filter((tab) => !tab.pinned);
73+
private async getAllTabs(opts: TabOption): Promise<chrome.tabs.Tab[]> {
74+
return await chrome.tabs.query({
75+
currentWindow: true,
76+
pinned: opts.includePinned ? undefined : false,
77+
});
7678
}
7779
}
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { Command, CommandContext, Completions } from "./types";
2+
import type { TabQueryHelper } from "./TabQueryHelper";
3+
import type { ConsoleClient } from "../clients/ConsoleClient";
4+
5+
export class TogglePinCommand implements Command {
6+
constructor(
7+
private readonly tabQueryHelper: TabQueryHelper,
8+
private readonly consoleClient: ConsoleClient,
9+
) {}
10+
11+
names(): string[] {
12+
return ["togglepin"];
13+
}
14+
15+
fullname(): string {
16+
return "togglepin";
17+
}
18+
19+
description(): string {
20+
return "Toggle a tab's pinning";
21+
}
22+
23+
getCompletions(_force: boolean, query: string): Promise<Completions> {
24+
return this.tabQueryHelper.getCompletions(query, {
25+
includePinned: true,
26+
});
27+
}
28+
29+
async exec(
30+
{ sender }: CommandContext,
31+
_force: boolean,
32+
args: string,
33+
): Promise<void> {
34+
let targetTab: chrome.tabs.Tab | undefined;
35+
const keywords = args.trim();
36+
if (keywords.length === 0) {
37+
targetTab = sender.tab;
38+
} else {
39+
const tabs = await this.tabQueryHelper.queryTabs(keywords, {
40+
includePinned: true,
41+
});
42+
if (tabs.length === 0) {
43+
throw new Error("No matching buffer for " + keywords);
44+
}
45+
targetTab = tabs[0];
46+
}
47+
if (typeof targetTab.id !== "number") {
48+
return;
49+
}
50+
51+
await chrome.tabs.update(targetTab.id, { pinned: !targetTab.pinned });
52+
await this.consoleClient.showInfo(sender.tabId, "Toggled tab pinning");
53+
}
54+
}
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Command, CommandContext, Completions } from "./types";
2+
import type { TabQueryHelper } from "./TabQueryHelper";
3+
import type { ConsoleClient } from "../clients/ConsoleClient";
4+
5+
export class UnpinCommand implements Command {
6+
constructor(
7+
private readonly tabQueryHelper: TabQueryHelper,
8+
private readonly consoleClient: ConsoleClient,
9+
) {}
10+
11+
names(): string[] {
12+
return ["unpin"];
13+
}
14+
15+
fullname(): string {
16+
return "unpin";
17+
}
18+
19+
description(): string {
20+
return "Make a tab unpinned";
21+
}
22+
23+
getCompletions(_force: boolean, query: string): Promise<Completions> {
24+
return this.tabQueryHelper.getCompletions(query, {
25+
includePinned: true,
26+
});
27+
}
28+
29+
async exec(
30+
{ sender }: CommandContext,
31+
_force: boolean,
32+
args: string,
33+
): Promise<void> {
34+
let targetTabId: number | undefined;
35+
const keywords = args.trim();
36+
if (keywords.length === 0) {
37+
targetTabId = sender.tabId;
38+
} else {
39+
const tabs = await this.tabQueryHelper.queryTabs(keywords, {
40+
includePinned: true,
41+
});
42+
if (tabs.length === 0) {
43+
throw new Error("No matching buffer for " + keywords);
44+
}
45+
46+
targetTabId = tabs[0].id!;
47+
}
48+
49+
await chrome.tabs.update(targetTabId, { pinned: false });
50+
await this.consoleClient.showInfo(sender.tabId, "Unpinned tab");
51+
}
52+
}

0 commit comments

Comments
 (0)