Skip to content

Commit 6d1cb70

Browse files
committed
feat: favorite items
closes #5
1 parent ed92efe commit 6d1cb70

File tree

8 files changed

+167
-30
lines changed

8 files changed

+167
-30
lines changed

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@
3636
{
3737
"command": "task-explorer.favorite-task",
3838
"title": "Add task to favorites",
39-
"icon": "$(extensions-star-empty)"
39+
"icon": "$(extensions-star-full)"
4040
},
4141
{
4242
"command": "task-explorer.unfavorite-task",
4343
"title": "Remove task from favorites",
44-
"icon": "$(extensions-star-full)"
44+
"icon": "$(extensions-star-empty)"
4545
}
4646
],
4747
"viewsContainers": {
@@ -81,6 +81,11 @@
8181
"command": "task-explorer.favorite-task",
8282
"when": "view == task-explorer && viewItem == taskItem",
8383
"group": "inline"
84+
},
85+
{
86+
"command": "task-explorer.unfavorite-task",
87+
"when": "view == task-explorer && viewItem == favoriteItem",
88+
"group": "inline"
8489
}
8590
]
8691
},

src/commands.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { commands, window } from 'vscode'
1+
import { commands } from 'vscode'
22
import { Container, EXTENSION_ID } from './extension'
3+
import { TaskItem } from './services/task-data-provider'
34

45
export default function registerCommands(ioc: Container) {
56
const context = ioc.resolve('context')
67
const taskDataProvider = ioc.resolve('taskDataProvider')
8+
const favorites = ioc.resolve('favorites')
79

810
const {
911
subscriptions,
@@ -16,11 +18,11 @@ export default function registerCommands(ioc: Container) {
1618
),
1719
commands.registerCommand(
1820
`${EXTENSION_ID}.favorite-task`,
19-
() => window.showInformationMessage('Favoriting tasks in currently work-in-progress.')
21+
(item: TaskItem) => favorites.add(item)
2022
),
2123
commands.registerCommand(
2224
`${EXTENSION_ID}.unfavorite-task`,
23-
() => window.showInformationMessage('Favoriting tasks in currently work-in-progress.')
25+
(item: TaskItem) => favorites.remove(item)
2426
),
2527
)
2628
}

src/disposable.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
export interface Disposable {
3+
dispose: () => void | Promise<void>
4+
}

src/extension.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ export async function activate(context: ExtensionContext): Promise<TaskExplorerA
3636
context: asValue(context),
3737
api: asClass(TaskExplorerApi),
3838

39-
config: asClass(Config),
40-
favorites: asClass(Favorites),
41-
taskDataProvider: asClass(TaskDataProvider),
39+
config: asClass(Config).singleton(),
40+
favorites: asClass(Favorites).singleton(),
41+
taskDataProvider: asClass(TaskDataProvider).singleton(),
4242
})
4343

4444
subscriptions.push(ioc)
@@ -48,6 +48,11 @@ export async function activate(context: ExtensionContext): Promise<TaskExplorerA
4848
registerCommands(ioc)
4949

5050

51+
// NOTE: TaskDataProvider#refresh() is called automagically by resolving the
52+
// api component below, which itself requires the TaskDataProvider. Thus
53+
// the TaskDataProvider will be resolved and initialized.
54+
55+
5156
// api
5257
return ioc.resolve('api')
5358
}

src/services/config.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ExtensionContext, WorkspaceConfiguration, workspace } from 'vscode'
2+
import { Disposable } from '../disposable'
23
import TypedEventEmitter from '../events'
34
import { EXTENSION_ID } from '../extension'
45

@@ -12,7 +13,7 @@ interface LocalEventTypes {
1213
'change': []
1314
}
1415

15-
export default class Config extends TypedEventEmitter<LocalEventTypes> {
16+
export default class Config extends TypedEventEmitter<LocalEventTypes> implements Disposable {
1617

1718
private delegate: WorkspaceConfiguration
1819

@@ -25,8 +26,6 @@ export default class Config extends TypedEventEmitter<LocalEventTypes> {
2526

2627
this.delegate = workspace.getConfiguration(EXTENSION_ID)
2728

28-
subscriptions.push(this)
29-
3029
// listen for config changes
3130
subscriptions.push(
3231
workspace.onDidChangeConfiguration(e => {

src/services/favorites.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,72 @@
1-
import { ExtensionContext } from 'vscode'
1+
import { ExtensionContext, window } from 'vscode'
2+
import { Disposable } from '../disposable'
3+
import TypedEventEmitter from '../events'
4+
import { EXTENSION_ID } from '../extension'
5+
import { TaskItem } from './task-data-provider'
26

3-
export class Favorites {
7+
interface LocalEventTypes {
8+
'change': []
9+
}
10+
11+
const storageKey = `${EXTENSION_ID}-favorites`
12+
13+
export class Favorites extends TypedEventEmitter<LocalEventTypes> implements Disposable {
414

515
private context: ExtensionContext
616

17+
private items: Set<string> = new Set()
18+
719
constructor(context: ExtensionContext) {
20+
super()
21+
822
this.context = context
23+
24+
this.init()
25+
}
26+
27+
private notify(): void {
28+
this.emit('change')
29+
}
30+
31+
private save() {
32+
const ids: string[] = []
33+
this.items.forEach(v => ids.push(v))
34+
this.context.workspaceState.update(storageKey, ids)
35+
}
36+
37+
init(): void {
38+
const items = this.context.workspaceState.get<string[]>(storageKey)
39+
if (Array.isArray(items)) {
40+
this.items = new Set(items)
41+
console.log(`loaded ${items.length} favorites`)
42+
43+
this.notify()
44+
}
45+
}
46+
47+
dispose(): void {
48+
this.emitter.removeAllListeners()
49+
this.save()
50+
}
51+
52+
list(): Set<string> {
53+
return this.items
54+
}
55+
56+
add(item: TaskItem): void {
57+
this.items.add(item.id)
58+
this.notify()
59+
this.save()
60+
}
61+
62+
remove(item: TaskItem): void {
63+
const id = item.id.substring(9)
64+
if (this.items.delete(id)) {
65+
this.notify()
66+
this.save()
67+
} else {
68+
window.showWarningMessage(`item with ID "${item.id}" was not in Set!`)
69+
}
970
}
1071

1172
}

src/services/task-data-provider.ts

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,31 @@ import { stat } from 'fs/promises'
33
import { join } from 'path'
44
import { groupBy, identity } from 'remeda'
55
import { Command, Event, EventEmitter, ExtensionContext, ProgressLocation, ProviderResult, Task, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, tasks, window } from 'vscode'
6-
import Config from './config'
6+
import { Disposable } from '../disposable'
77
import { EXTENSION_ID } from '../extension'
8+
import { notEmpty } from '../util'
9+
import Config from './config'
10+
import { Favorites } from './favorites'
811

9-
function makeTaskId(task: Task): string {
12+
const groupKeyFavorites = 'favorites'
13+
14+
function makeTaskId(task: Task, isFavorite: boolean): string {
1015
const id = `${task.definition.type}-${task.name.replace(/\s+/g, '_').toLocaleLowerCase()}-${task.source}`
11-
return createHash('md5').update(id).digest('hex')
16+
const hash = createHash('md5').update(id).digest('hex')
17+
18+
if (isFavorite) {
19+
return `favorite-${hash}`
20+
}
21+
22+
return hash
1223
}
1324

1425
function makeGroupLabel(label: string): string {
1526
switch (label) {
1627
case '$composite':
1728
return 'Composite Tasks'
29+
case groupKeyFavorites:
30+
return 'Favorite Tasks'
1831
default:
1932
return label
2033
}
@@ -53,6 +66,9 @@ export class GroupItem extends TreeItem {
5366
case 'shell':
5467
this.iconPath = new ThemeIcon('terminal-view-icon')
5568
return
69+
case groupKeyFavorites:
70+
this.iconPath = new ThemeIcon('star-full')
71+
return
5672
}
5773

5874
const file = join(__dirname, '..', 'resources', 'icons', `${name}.svg`)
@@ -74,51 +90,66 @@ export class GroupItem extends TreeItem {
7490

7591
export class TaskItem extends TreeItem {
7692

93+
readonly task: Task
7794
readonly id: string
7895
readonly group: string
7996

8097
constructor(
8198
task: Task,
8299
command: Command,
100+
isFavorite: boolean = false,
83101
) {
84102
super(task.name, TreeItemCollapsibleState.None)
85103

86-
this.id = makeTaskId(task)
87-
this.group = task.definition.type
104+
this.task = task
105+
this.id = makeTaskId(task, isFavorite)
106+
this.group = isFavorite ? groupKeyFavorites : task.definition.type
88107
this.description = task.detail
89108
this.command = command
90109
}
91110

111+
public get isFavorite() : boolean {
112+
return this.group === groupKeyFavorites
113+
}
114+
92115
contextValue = 'taskItem'
93116

94117
}
95118

119+
export class FavoriteItem extends TaskItem {
120+
121+
constructor(
122+
task: Task,
123+
command: Command,
124+
) {
125+
super(task, command, true)
126+
}
127+
128+
contextValue = 'favoriteItem'
129+
130+
}
131+
96132
type TaskList = Record<string, TaskItem[]>
97133

98-
export default class TaskDataProvider implements TreeDataProvider<TreeItem> {
134+
export default class TaskDataProvider implements TreeDataProvider<TreeItem>, Disposable {
99135

100136
private config: Config
101137

102138
private groups: TreeItem[] = []
103139
private tasks: TaskList = {}
140+
private favorites: Favorites
104141

105142
private _onDidChangeTreeData: EventEmitter<TreeItem | undefined | void> = new EventEmitter<TreeItem | undefined | void>()
106143
readonly onDidChangeTreeData: Event<TreeItem | undefined | void> = this._onDidChangeTreeData.event
107144

108-
constructor(config: Config, context: ExtensionContext) {
109-
const {
110-
subscriptions
111-
} = context
112-
145+
constructor(config: Config, context: ExtensionContext, favorites: Favorites) {
113146
this.config = config
147+
this.favorites = favorites
114148

115149
this.config.on('change', () => this.refresh())
150+
this.favorites.on('change', () => this.refresh())
116151

117152
this.refresh()
118-
119-
subscriptions.push(
120-
window.registerTreeDataProvider(EXTENSION_ID, this),
121-
)
122153
}
123154

124155
getTreeItem(element: TreeItem): TreeItem {
@@ -133,6 +164,10 @@ export default class TaskDataProvider implements TreeDataProvider<TreeItem> {
133164
return this.groups
134165
}
135166

167+
dispose(): void {
168+
window.registerTreeDataProvider(EXTENSION_ID, this)
169+
}
170+
136171
private isGroupItem(element?: TreeItem): boolean {
137172
return !!element && element.collapsibleState !== TreeItemCollapsibleState.None
138173
}
@@ -152,14 +187,28 @@ export default class TaskDataProvider implements TreeDataProvider<TreeItem> {
152187

153188
const excludedGroups = this.config.get('excludeGroups')
154189

155-
return list
190+
const result = list
156191
.filter(item => !excludedGroups?.includes(item.definition.type))
157192
.map(item => new TaskItem(item, {
158193
command: 'workbench.action.tasks.runTask',
159194
title: 'Run this task',
160195
arguments: this.buildArguments(item),
161196
}))
162197
.sort((a, b) => (a.label as string).localeCompare(b.label as string))
198+
199+
const favorites = Array.from(this.favorites.list())
200+
.map(id => {
201+
const item = result.find(item => item.id === id)
202+
if (item !== undefined) {
203+
return new FavoriteItem(item.task, item.command!)
204+
}
205+
206+
return undefined
207+
})
208+
.filter(notEmpty)
209+
.sort((a, b) => a.task.name.localeCompare(b.task.name))
210+
211+
return favorites.concat(result)
163212
}
164213

165214
async refresh(): Promise<void> {
@@ -176,7 +225,16 @@ export default class TaskDataProvider implements TreeDataProvider<TreeItem> {
176225
item => item.group
177226
)
178227
this.groups = Object.keys(this.tasks)
179-
.sort()
228+
.sort((a, b) => {
229+
if (a === groupKeyFavorites) {
230+
return -1
231+
}
232+
else if (b === groupKeyFavorites) {
233+
return 1
234+
}
235+
236+
return a.localeCompare(b)
237+
})
180238
.map(key => new GroupItem(key))
181239

182240
this._onDidChangeTreeData.fire()

src/util.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
2+
return value !== null && value !== undefined
3+
}

0 commit comments

Comments
 (0)