Source: treeProviders/storyTreeProvider.ts

import * as vscode from 'vscode';
import { Story } from '../models/story';
import { Workspace } from '../models/workspace';
import removeMarkdown from "markdown-to-text";
import { Config } from '../models/config';

/**
 * Tree data provider for displaying pending Shortcut stories and their tasks.
 * Implements the VS Code TreeDataProvider interface to create a hierarchical view of:
 * Workspace -> Stories -> Tasks
 * 
 * The tree shows pending stories and their tasks, grouped by workspace.
 * Each workspace displays the number of pending stories in parentheses.
 * Empty workspaces can be optionally hidden via configuration.
 * Stories show their ID and can be expanded to show tasks.
 * Tasks display their completion status with a checkbox icon.
 */
export class StoryTreeProvider implements vscode.TreeDataProvider<TreeItem> {
    /** Event emitter for notifying VS Code when the tree data changes */
    private _onDidChangeTreeData: vscode.EventEmitter<TreeItem | undefined | null | void> = new vscode.EventEmitter<TreeItem | undefined | null | void>();
    /** Event that VS Code listens to for tree data changes */
    readonly onDidChangeTreeData: vscode.Event<TreeItem | undefined | null | void> = this._onDidChangeTreeData.event;

    /** Array of workspaces containing stories and tasks */
    private workspaces: Workspace[] = [];
    /** Configuration settings for the tree view */
    private config: Config = new Config();

    constructor() {}

    /**
     * Refreshes the tree view with updated workspace data and configuration.
     * Triggers a re-render of the entire tree.
     * 
     * @param {Workspace[]} workspaces - Array of workspaces to display
     * @param {Config} config - Updated configuration settings
     */
    refresh(workspaces: Workspace[], config: Config): void {
        this.workspaces = workspaces;
        this.config = config;
        this._onDidChangeTreeData.fire();
    }

    /**
     * Gets the tree item for an element.
     * Required by VS Code's TreeDataProvider interface.
     * 
     * @param {TreeItem} element - The element to get the tree item for
     * @returns {vscode.TreeItem} The tree item for the element
     */
    getTreeItem(element: TreeItem): vscode.TreeItem {
        return element;
    }

    /**
     * Gets the children of a tree item.
     * Handles the hierarchical structure of the tree:
     * - Root level shows workspaces with pending story counts
     * - Workspaces contain stories with their IDs
     * - Stories contain tasks with completion status
     * 
     * Empty workspaces can be filtered out based on configuration.
     * 
     * @param {TreeItem} [element] - The parent element to get children for
     * @returns {Thenable<TreeItem[]>} Promise resolving to array of child tree items
     */
    getChildren(element?: TreeItem): Thenable<TreeItem[]> {
        if (!element) {
            // Root level - return stories
            let displayedWorkspaces = this.workspaces.map(workspace => new WorkspaceTreeItem(
                `${workspace.name} (${workspace.pendingStories.length})`,
                workspace.pendingStories.length > 0 ? 
                    vscode.TreeItemCollapsibleState.Collapsed : 
                    vscode.TreeItemCollapsibleState.None,
                workspace
            ));

            if (this.config.hideEmptyWorkspaces) {
                displayedWorkspaces = displayedWorkspaces.filter(workspace => workspace.workspace.pendingStories.length > 0);
            }

            return Promise.resolve(
                displayedWorkspaces
            );
        } else if (element instanceof WorkspaceTreeItem) {
            // Story level - return tasks
            return Promise.resolve(
                element.workspace.pendingStories.map(story => new StoryTreeItem(
                    story.name,
                    story.id,
                    story.tasks.length > 0 ? 
                        vscode.TreeItemCollapsibleState.Collapsed : 
                        vscode.TreeItemCollapsibleState.None,
                    story,
                    element.workspace
                ))
            );
        } else if (element instanceof StoryTreeItem) {
            // Task level - return tasks
            return Promise.resolve(
                element.story.tasks.map(task => new TaskTreeItem(
                    removeMarkdown(task.description),
                    task.complete,
                    task.id,
                    element.story.id,
                    element.workspace
                ))
            );
        }
        return Promise.resolve([]);
    }
}

/**
 * Base tree item class that all other tree items extend from.
 * Provides common functionality for tree items in the view.
 * 
 * @extends vscode.TreeItem
 */
class TreeItem extends vscode.TreeItem {
    /**
     * Creates a new TreeItem instance.
     * 
     * @param {string} label - The display text for the tree item
     * @param {vscode.TreeItemCollapsibleState} collapsibleState - Whether and how the tree item can be expanded
     */
    constructor(
        public readonly label: string,
        public readonly collapsibleState: vscode.TreeItemCollapsibleState
    ) {
        super(label, collapsibleState);
    }
}

/**
 * Tree item representing a Shortcut workspace.
 * The top level of the hierarchy that contains stories.
 * Displays the total number of pending stories in parentheses.
 * 
 * @extends TreeItem
 */
class WorkspaceTreeItem extends TreeItem {
    /**
     * Creates a new WorkspaceTreeItem instance.
     * 
     * @param {string} label - The display text for the workspace
     * @param {vscode.TreeItemCollapsibleState} collapsibleState - Whether and how the workspace can be expanded
     * @param {Workspace} workspace - The workspace this tree item represents
     */
    constructor(
        public readonly label: string,
        public readonly collapsibleState: vscode.TreeItemCollapsibleState,
        public readonly workspace: Workspace
    ) {
        super(label, collapsibleState);
    }
}

/**
 * Tree item representing a Shortcut story.
 * The second level of the hierarchy that contains tasks.
 * Displays the story name and ID, with a book icon.
 * Provides context menu actions via contextValue.
 * 
 * @extends TreeItem
 */
class StoryTreeItem extends TreeItem {
    /**
     * Creates a new StoryTreeItem instance.
     * 
     * @param {string} label - The display text for the story
     * @param {number} storyId - The unique identifier of the story
     * @param {vscode.TreeItemCollapsibleState} collapsibleState - Whether and how the story can be expanded
     * @param {Story} story - The story this tree item represents
     * @param {Workspace} workspace - The workspace containing this story
     */
    constructor(
        public readonly label: string,
        public readonly storyId: number,
        public readonly collapsibleState: vscode.TreeItemCollapsibleState,
        public readonly story: Story,
        public readonly workspace: Workspace
    ) {
        super(label, collapsibleState);
        this.tooltip = `${this.label}`;
        this.description = `#${this.storyId}`;
        this.contextValue = 'story';
        this.iconPath = new vscode.ThemeIcon('book');
    }
}

/**
 * Tree item representing a task within a Shortcut story.
 * The leaf level of the hierarchy.
 * Displays the task description and completion status with a checkbox icon.
 * Provides context menu actions via contextValue.
 * 
 * @extends TreeItem
 */
class TaskTreeItem extends TreeItem {
    /**
     * Creates a new TaskTreeItem instance.
     * 
     * @param {string} label - The display text for the task
     * @param {boolean} isComplete - Whether the task is marked as complete
     * @param {number} taskId - The unique identifier of the task
     * @param {number} storyId - The unique identifier of the story containing this task
     * @param {Workspace} workspace - The workspace containing this task
     */
    constructor(
        public readonly label: string,
        public readonly isComplete: boolean,
        public readonly taskId: number,
        public readonly storyId: number,
        public readonly workspace: Workspace
    ) {
        super(label, vscode.TreeItemCollapsibleState.None);
        this.tooltip = this.label;
        this.contextValue = 'task';
        this.iconPath = new vscode.ThemeIcon(
            this.isComplete ? 'check' : 'circle-outline'
        );
    }
}