Skip to content

Commit

Permalink
feat: favorite items
Browse files Browse the repository at this point in the history
closes #5
  • Loading branch information
axelrindle committed Mar 12, 2024
1 parent ed92efe commit 6d1cb70
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 30 deletions.
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
{
"command": "task-explorer.favorite-task",
"title": "Add task to favorites",
"icon": "$(extensions-star-empty)"
"icon": "$(extensions-star-full)"
},
{
"command": "task-explorer.unfavorite-task",
"title": "Remove task from favorites",
"icon": "$(extensions-star-full)"
"icon": "$(extensions-star-empty)"
}
],
"viewsContainers": {
Expand Down Expand Up @@ -81,6 +81,11 @@
"command": "task-explorer.favorite-task",
"when": "view == task-explorer && viewItem == taskItem",
"group": "inline"
},
{
"command": "task-explorer.unfavorite-task",
"when": "view == task-explorer && viewItem == favoriteItem",
"group": "inline"
}
]
},
Expand Down
8 changes: 5 additions & 3 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { commands, window } from 'vscode'
import { commands } from 'vscode'
import { Container, EXTENSION_ID } from './extension'
import { TaskItem } from './services/task-data-provider'

export default function registerCommands(ioc: Container) {
const context = ioc.resolve('context')
const taskDataProvider = ioc.resolve('taskDataProvider')
const favorites = ioc.resolve('favorites')

const {
subscriptions,
Expand All @@ -16,11 +18,11 @@ export default function registerCommands(ioc: Container) {
),
commands.registerCommand(
`${EXTENSION_ID}.favorite-task`,
() => window.showInformationMessage('Favoriting tasks in currently work-in-progress.')
(item: TaskItem) => favorites.add(item)
),
commands.registerCommand(
`${EXTENSION_ID}.unfavorite-task`,
() => window.showInformationMessage('Favoriting tasks in currently work-in-progress.')
(item: TaskItem) => favorites.remove(item)
),
)
}
4 changes: 4 additions & 0 deletions src/disposable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

export interface Disposable {
dispose: () => void | Promise<void>
}
11 changes: 8 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ export async function activate(context: ExtensionContext): Promise<TaskExplorerA
context: asValue(context),
api: asClass(TaskExplorerApi),

config: asClass(Config),
favorites: asClass(Favorites),
taskDataProvider: asClass(TaskDataProvider),
config: asClass(Config).singleton(),
favorites: asClass(Favorites).singleton(),
taskDataProvider: asClass(TaskDataProvider).singleton(),
})

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


// NOTE: TaskDataProvider#refresh() is called automagically by resolving the
// api component below, which itself requires the TaskDataProvider. Thus
// the TaskDataProvider will be resolved and initialized.


// api
return ioc.resolve('api')
}
Expand Down
5 changes: 2 additions & 3 deletions src/services/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ExtensionContext, WorkspaceConfiguration, workspace } from 'vscode'
import { Disposable } from '../disposable'
import TypedEventEmitter from '../events'
import { EXTENSION_ID } from '../extension'

Expand All @@ -12,7 +13,7 @@ interface LocalEventTypes {
'change': []
}

export default class Config extends TypedEventEmitter<LocalEventTypes> {
export default class Config extends TypedEventEmitter<LocalEventTypes> implements Disposable {

private delegate: WorkspaceConfiguration

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

this.delegate = workspace.getConfiguration(EXTENSION_ID)

subscriptions.push(this)

// listen for config changes
subscriptions.push(
workspace.onDidChangeConfiguration(e => {
Expand Down
65 changes: 63 additions & 2 deletions src/services/favorites.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,72 @@
import { ExtensionContext } from 'vscode'
import { ExtensionContext, window } from 'vscode'
import { Disposable } from '../disposable'
import TypedEventEmitter from '../events'
import { EXTENSION_ID } from '../extension'
import { TaskItem } from './task-data-provider'

export class Favorites {
interface LocalEventTypes {
'change': []
}

const storageKey = `${EXTENSION_ID}-favorites`

export class Favorites extends TypedEventEmitter<LocalEventTypes> implements Disposable {

private context: ExtensionContext

private items: Set<string> = new Set()

constructor(context: ExtensionContext) {
super()

this.context = context

this.init()
}

private notify(): void {
this.emit('change')
}

private save() {
const ids: string[] = []
this.items.forEach(v => ids.push(v))
this.context.workspaceState.update(storageKey, ids)
}

init(): void {
const items = this.context.workspaceState.get<string[]>(storageKey)
if (Array.isArray(items)) {
this.items = new Set(items)
console.log(`loaded ${items.length} favorites`)

this.notify()
}
}

dispose(): void {
this.emitter.removeAllListeners()
this.save()
}

list(): Set<string> {
return this.items
}

add(item: TaskItem): void {
this.items.add(item.id)
this.notify()
this.save()
}

remove(item: TaskItem): void {
const id = item.id.substring(9)
if (this.items.delete(id)) {
this.notify()
this.save()
} else {
window.showWarningMessage(`item with ID "${item.id}" was not in Set!`)
}
}

}
92 changes: 75 additions & 17 deletions src/services/task-data-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,31 @@ import { stat } from 'fs/promises'
import { join } from 'path'
import { groupBy, identity } from 'remeda'
import { Command, Event, EventEmitter, ExtensionContext, ProgressLocation, ProviderResult, Task, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, tasks, window } from 'vscode'
import Config from './config'
import { Disposable } from '../disposable'
import { EXTENSION_ID } from '../extension'
import { notEmpty } from '../util'
import Config from './config'
import { Favorites } from './favorites'

function makeTaskId(task: Task): string {
const groupKeyFavorites = 'favorites'

function makeTaskId(task: Task, isFavorite: boolean): string {
const id = `${task.definition.type}-${task.name.replace(/\s+/g, '_').toLocaleLowerCase()}-${task.source}`
return createHash('md5').update(id).digest('hex')
const hash = createHash('md5').update(id).digest('hex')

if (isFavorite) {
return `favorite-${hash}`
}

return hash
}

function makeGroupLabel(label: string): string {
switch (label) {
case '$composite':
return 'Composite Tasks'
case groupKeyFavorites:
return 'Favorite Tasks'
default:
return label
}
Expand Down Expand Up @@ -53,6 +66,9 @@ export class GroupItem extends TreeItem {
case 'shell':
this.iconPath = new ThemeIcon('terminal-view-icon')
return
case groupKeyFavorites:
this.iconPath = new ThemeIcon('star-full')
return
}

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

export class TaskItem extends TreeItem {

readonly task: Task
readonly id: string
readonly group: string

constructor(
task: Task,
command: Command,
isFavorite: boolean = false,
) {
super(task.name, TreeItemCollapsibleState.None)

this.id = makeTaskId(task)
this.group = task.definition.type
this.task = task
this.id = makeTaskId(task, isFavorite)
this.group = isFavorite ? groupKeyFavorites : task.definition.type
this.description = task.detail
this.command = command
}

public get isFavorite() : boolean {
return this.group === groupKeyFavorites
}

contextValue = 'taskItem'

}

export class FavoriteItem extends TaskItem {

constructor(
task: Task,
command: Command,
) {
super(task, command, true)
}

contextValue = 'favoriteItem'

}

type TaskList = Record<string, TaskItem[]>

export default class TaskDataProvider implements TreeDataProvider<TreeItem> {
export default class TaskDataProvider implements TreeDataProvider<TreeItem>, Disposable {

private config: Config

private groups: TreeItem[] = []
private tasks: TaskList = {}
private favorites: Favorites

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

constructor(config: Config, context: ExtensionContext) {
const {
subscriptions
} = context

constructor(config: Config, context: ExtensionContext, favorites: Favorites) {
this.config = config
this.favorites = favorites

this.config.on('change', () => this.refresh())
this.favorites.on('change', () => this.refresh())

this.refresh()

subscriptions.push(
window.registerTreeDataProvider(EXTENSION_ID, this),
)
}

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

dispose(): void {
window.registerTreeDataProvider(EXTENSION_ID, this)
}

private isGroupItem(element?: TreeItem): boolean {
return !!element && element.collapsibleState !== TreeItemCollapsibleState.None
}
Expand All @@ -152,14 +187,28 @@ export default class TaskDataProvider implements TreeDataProvider<TreeItem> {

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

return list
const result = list
.filter(item => !excludedGroups?.includes(item.definition.type))
.map(item => new TaskItem(item, {
command: 'workbench.action.tasks.runTask',
title: 'Run this task',
arguments: this.buildArguments(item),
}))
.sort((a, b) => (a.label as string).localeCompare(b.label as string))

const favorites = Array.from(this.favorites.list())
.map(id => {
const item = result.find(item => item.id === id)
if (item !== undefined) {
return new FavoriteItem(item.task, item.command!)
}

return undefined
})
.filter(notEmpty)
.sort((a, b) => a.task.name.localeCompare(b.task.name))

return favorites.concat(result)
}

async refresh(): Promise<void> {
Expand All @@ -176,7 +225,16 @@ export default class TaskDataProvider implements TreeDataProvider<TreeItem> {
item => item.group
)
this.groups = Object.keys(this.tasks)
.sort()
.sort((a, b) => {
if (a === groupKeyFavorites) {
return -1
}
else if (b === groupKeyFavorites) {
return 1
}

return a.localeCompare(b)
})
.map(key => new GroupItem(key))

this._onDidChangeTreeData.fire()
Expand Down
3 changes: 3 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined
}

0 comments on commit 6d1cb70

Please sign in to comment.