vscode#TreeDataProvider TypeScript Examples

The following examples show how to use vscode#TreeDataProvider. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: taskProvider.ts    From vscode-todo-md with MIT License 6 votes vote down vote up
export class TaskProvider implements TreeDataProvider<TaskTreeItem> {
	private readonly _onDidChangeTreeData: EventEmitter<TaskTreeItem | undefined> = new EventEmitter<TaskTreeItem | undefined>();
	readonly onDidChangeTreeData: Event<TaskTreeItem | undefined> = this._onDidChangeTreeData.event;

	constructor(
		private tasks: TheTask[],
		private readonly isArchived = false,
	) { }

	refresh(newTasks: TheTask[]) {
		this.tasks = newTasks;
		this._onDidChangeTreeData.fire(undefined);
	}
	/**
	 * Resolve `tooltip` only on hover
	 */
	resolveTreeItem(item: TaskTreeItem, el: TaskTreeItem) {
		el.tooltip = getTaskHoverMd(el.task);
		return el;
	}

	getTreeItem(element: TaskTreeItem): TreeItem {
		return element;
	}

	getChildren(element: TaskTreeItem | undefined): TaskTreeItem[] {
		let tasksToTransform: TheTask[] = [];
		if (element) {
			const subtasks = element.task.subtasks;
			if (subtasks.length) {
				return tasksToTreeItems(subtasks, true, this.isArchived);
			}
		} else {
			tasksToTransform = this.tasks;
		}
		return tasksToTreeItems(tasksToTransform, false, this.isArchived);
	}
}
Example #2
Source File: comment-view.ts    From vscode-code-review with MIT License 6 votes vote down vote up
export class CommentsProvider implements TreeDataProvider<CommentListEntry> {
  private _onDidChangeTreeData: EventEmitter<CommentListEntry | undefined> = new EventEmitter<
    CommentListEntry | undefined
  >();
  readonly onDidChangeTreeData: Event<CommentListEntry | undefined> = this._onDidChangeTreeData.event;

  constructor(private context: ExtensionContext, private exportFactory: ExportFactory) {}

  refresh(entry?: CommentListEntry): void {
    this._onDidChangeTreeData.fire(entry);
  }

  getTreeItem(element: CommentListEntry): TreeItem {
    return element;
  }

  getChildren(element?: CommentListEntry): Thenable<CommentListEntry[]> {
    // if no element, the first item level starts
    if (!element) {
      return this.exportFactory.getFilesContainingComments();
    } else {
      return this.exportFactory.getComments(element);
    }
  }
}
Example #3
Source File: extension.ts    From cloudmusic-vscode with MIT License 5 votes vote down vote up
export async function activate(context: ExtensionContext): Promise<void> {
  /* process.setUncaughtExceptionCaptureCallback(({ message }) =>
    console.error(message)
  ); */
  process.on("unhandledRejection", console.error);
  await mkdir(SETTING_DIR, { recursive: true }).catch();

  context.globalState.setKeysForSync([BUTTON_KEY]);
  AccountViewProvider.context = context;
  AccountManager.context = context;
  ButtonManager.context = context;
  State.context = context;
  Webview.extUri = context.extensionUri;

  // Check mode
  if (!State.wasm) {
    const buildUri = Uri.joinPath(context.extensionUri, "build");
    const files = await workspace.fs.readDirectory(buildUri);
    State.wasm = files.findIndex(([file]) => file === NATIVE_MODULE) === -1;
    if (!State.wasm) State.downInit(); // 3
  }
  console.log("Cloudmusic:", State.wasm ? "wasm" : "native", "mode.");

  const createTreeView = <T>(
    viewId: string,
    treeDataProvider: TreeDataProvider<T>
  ) => window.createTreeView(viewId, { treeDataProvider });

  const queue = createTreeView("queue", QueueProvider.getInstance());
  const local = createTreeView("local", LocalProvider.getInstance());
  const playlist = createTreeView("playlist", PlaylistProvider.getInstance());
  const radio = createTreeView("radio", RadioProvider.getInstance());
  context.subscriptions.push(queue, local, playlist, radio);

  // Only checking the visibility of the queue treeview is enough.
  if (AUTO_START || queue.visible) {
    await realActivate(context);
  } else {
    let done = false;
    const disposables: Disposable[] = [];
    const callback = ({ visible }: TreeViewVisibilityChangeEvent) => {
      if (done || !visible) return;
      done = true;
      void realActivate(context);
      let disposable: Disposable | undefined;
      while ((disposable = disposables.pop())) disposable.dispose();
    };
    disposables.push(queue.onDidChangeVisibility(callback));
  }
}
Example #4
Source File: tagProvider.ts    From vscode-todo-md with MIT License 5 votes vote down vote up
export class TagProvider implements TreeDataProvider<TagTreeItem | TaskTreeItem> {
	private readonly _onDidChangeTreeData: EventEmitter<TagTreeItem | undefined> = new EventEmitter<TagTreeItem | undefined>();
	readonly onDidChangeTreeData: Event<TagTreeItem | undefined> = this._onDidChangeTreeData.event;

	constructor(
		private tags: ItemForProvider[],
	) { }

	refresh(newTags: ItemForProvider[]) {
		this.tags = newTags;
		this._onDidChangeTreeData.fire(undefined);
	}

	/**
	 * Resolve `tooltip` only on hover
	 */
	resolveTreeItem(item: TagTreeItem | TaskTreeItem, el: TagTreeItem | TaskTreeItem) {
		if (el instanceof TaskTreeItem) {
			el.tooltip = getTaskHoverMd(el.task);
			return el;
		}
		return undefined;
	}

	getTreeItem(element: TagTreeItem | TaskTreeItem): TreeItem {
		return element;
	}

	getChildren(element: TagTreeItem | TaskTreeItem | undefined): TagTreeItem[] | TaskTreeItem[] {
		if (!element) {
			return this.tags.map(tag => new TagTreeItem(`${tag.title} [${tag.tasks.length}]`, tag.tasks));
		}
		let tasksToTransform: TheTask[] = [];
		if (element instanceof TagTreeItem) {
			tasksToTransform = element.tasks;
		} else {
			const subtasks = element.task.subtasks;
			if (subtasks.length) {
				return tasksToTreeItems(subtasks, true);
			}
		}
		return tasksToTreeItems(tasksToTransform);
	}
}
Example #5
Source File: projectProvider.ts    From vscode-todo-md with MIT License 5 votes vote down vote up
export class ProjectProvider implements TreeDataProvider<ProjectTreeItem | TaskTreeItem> {
	private readonly _onDidChangeTreeData: EventEmitter<ProjectTreeItem | undefined> = new EventEmitter<ProjectTreeItem | undefined>();
	readonly onDidChangeTreeData: Event<ProjectTreeItem | undefined> = this._onDidChangeTreeData.event;

	constructor(
		private projects: ItemForProvider[],
	) { }

	refresh(newProjects: ItemForProvider[]) {
		this.projects = newProjects;
		this._onDidChangeTreeData.fire(undefined);
	}
	/**
	 * Resolve `tooltip` only on hover
	 */
	resolveTreeItem(item: ProjectTreeItem | TaskTreeItem, el: ProjectTreeItem | TaskTreeItem) {
		if (el instanceof TaskTreeItem) {
			el.tooltip = getTaskHoverMd(el.task);
			return el;
		}
		return undefined;
	}

	getTreeItem(element: ProjectTreeItem | TaskTreeItem): TreeItem {
		return element;
	}

	getChildren(element: ProjectTreeItem | TaskTreeItem | undefined): ProjectTreeItem[] | TaskTreeItem[] {
		if (!element) {
			return this.projects.map(project => new ProjectTreeItem(`${project.title} [${project.tasks.length}]`, project.tasks));
		}
		let tasksToTransform: TheTask[] = [];
		if (element instanceof ProjectTreeItem) {
			tasksToTransform = element.tasks;
		} else {
			const subtasks = element.task.subtasks;
			if (subtasks.length) {
				return tasksToTreeItems(subtasks, true);
			}
		}
		return tasksToTreeItems(tasksToTransform);
	}
}
Example #6
Source File: contextProvider.ts    From vscode-todo-md with MIT License 5 votes vote down vote up
export class ContextProvider implements TreeDataProvider<ContextTreeItem | TaskTreeItem> {
	private readonly _onDidChangeTreeData: EventEmitter<ContextTreeItem | undefined> = new EventEmitter<ContextTreeItem | undefined>();
	readonly onDidChangeTreeData: Event<ContextTreeItem | undefined> = this._onDidChangeTreeData.event;

	constructor(
		private contexts: ItemForProvider[],
	) { }

	refresh(newContexts: ItemForProvider[]) {
		this.contexts = newContexts;
		this._onDidChangeTreeData.fire(undefined);
	}
	/**
	 * Resolve `tooltip` only on hover
	 */
	resolveTreeItem(item: ContextTreeItem | TaskTreeItem, el: ContextTreeItem | TaskTreeItem) {
		if (el instanceof TaskTreeItem) {
			el.tooltip = getTaskHoverMd(el.task);
			return el;
		}
		return undefined;
	}

	getTreeItem(element: ContextTreeItem | TaskTreeItem): TreeItem {
		return element;
	}

	getChildren(element: ContextTreeItem | TaskTreeItem | undefined): ContextTreeItem[] | TaskTreeItem[] {
		if (!element) {
			return this.contexts.map(context => new ContextTreeItem(`${context.title} [${context.tasks.length}]`, context.tasks));
		}
		let tasksToTransform: TheTask[] = [];
		if (element instanceof ContextTreeItem) {
			tasksToTransform = element.tasks;
		} else {
			const subtasks = element.task.subtasks;
			if (subtasks.length) {
				return tasksToTreeItems(subtasks, true);
			}
		}
		return tasksToTreeItems(tasksToTransform);
	}
}
Example #7
Source File: stripeTreeViewDataProvider.ts    From vscode-stripe with MIT License 5 votes vote down vote up
export class StripeTreeViewDataProvider implements TreeDataProvider<TreeItem> {
  private treeItems: TreeItem[] | null = null;
  private _onDidChangeTreeData: EventEmitter<TreeItem | null> = new EventEmitter<TreeItem | null>();
  readonly onDidChangeTreeData: Event<TreeItem | null> = this._onDidChangeTreeData.event;

  public refresh() {
    this.treeItems = null;
    this._onDidChangeTreeData.fire(null);
  }

  public getTreeItem(element: TreeItem): TreeItem {
    return element;
  }

  public getParent(element: TreeItem): TreeItem | null {
    if (element instanceof StripeTreeItem && element.parent) {
      return element.parent;
    }
    return null;
  }

  public async getChildren(element?: TreeItem): Promise<TreeItem[]> {
    if (!this.treeItems) {
      this.treeItems = await this.buildTree();
    }

    if (element instanceof StripeTreeItem) {
      return element.children;
    }

    if (!element) {
      if (this.treeItems) {
        return this.treeItems;
      }
    }
    return [];
  }

  buildTree(): Promise<StripeTreeItem[]> {
    return Promise.resolve([]);
  }
}
Example #8
Source File: HelpFeedbackTreeview.ts    From dendron with GNU Affero General Public License v3.0 5 votes vote down vote up
class HelpFeedbackTreeDataProvider implements TreeDataProvider<MenuItem> {
  ALL_ITEMS = Object.values(MenuItem) as MenuItem[];

  onDidChangeTreeData?: Event<void | MenuItem | null | undefined> | undefined;

  getTreeItem(element: MenuItem): TreeItem {
    let iconPath: vscode.ThemeIcon;

    switch (element) {
      case MenuItem.getStarted:
        iconPath = new ThemeIcon("star");
        break;

      case MenuItem.readDocs:
        iconPath = new ThemeIcon("book");
        break;

      case MenuItem.reviewIssues:
        iconPath = new ThemeIcon("issues");
        break;

      case MenuItem.reportIssue:
        iconPath = new ThemeIcon("comment");
        break;

      case MenuItem.joinCommunity:
        iconPath = new ThemeIcon("organization");
        break;

      case MenuItem.followUs:
        iconPath = new ThemeIcon("twitter");
        break;

      default:
        assertUnreachable(element);
    }

    return {
      label: element.toString(),
      collapsibleState: TreeItemCollapsibleState.None,
      iconPath,
    };
  }
  getChildren(element?: MenuItem): ProviderResult<MenuItem[]> {
    switch (element) {
      case undefined:
        return this.ALL_ITEMS;
      default:
        return [];
    }
  }
}
Example #9
Source File: driveTreeDataProvider.ts    From google-drive-vscode with MIT License 5 votes vote down vote up
export class DriveTreeDataProvider implements TreeDataProvider<string> {

    /** Helper objects to refresh UI when a new monitor is added */
    private _onDidChangeTreeData: EventEmitter<string | undefined> = new EventEmitter<string | undefined>();
    readonly onDidChangeTreeData: Event<string | undefined> = this._onDidChangeTreeData.event;

    constructor(private model: DriveModel, private notificator: INotificator) {
        window.registerTreeDataProvider('driveView', this);
    }

    refresh(): void {
        this._onDidChangeTreeData.fire();
    }

    private askToCreateFolderOnRoot(): void {
        const yesButton = 'Yes';
        this.notificator.showInformationMessage(`It looks like you don't have any folder on Google Drive accessible from this extension. Do you want to create a folder on Google Drive now?`, yesButton, 'No')
            .then(selectedButton => {
                if (selectedButton === yesButton) {
                    commands.executeCommand(CREATE_FOLDER_COMMAND);
                }
            });
    }

    private extractFileIds(files: DriveFile[]): string[] {
        const result = files.map(f => f.id);
        return result;
    }

    private buildItemFromDriveFile(currentFile: DriveFile): TreeItem {
        const iconUri = Uri.parse(currentFile.iconLink);
        const collapsible = this.detectCollapsibleState(currentFile);
        const contextValue = DriveFileUtils.extractTextFromType(currentFile.type);
        return {
            iconPath: iconUri,
            label: currentFile.name,
            collapsibleState: collapsible,
            contextValue: contextValue
        };
    }

    private detectCollapsibleState(currentFile: DriveFile): TreeItemCollapsibleState {
        const collapsible = currentFile.type == FileType.DIRECTORY ?
            TreeItemCollapsibleState.Collapsed :
            TreeItemCollapsibleState.None;
        return collapsible;
    }

    //------- Interface methods

    getTreeItem(id: string): TreeItem {
        const currentFile = this.model.getDriveFile(id);
        return currentFile ? this.buildItemFromDriveFile(currentFile) : {};
    }

    getChildren(id?: string): Promise<string[]> {
        return new Promise((resolve, reject) => {
            const currentFileId = id ? id : 'root';
            this.model.listFiles(currentFileId)
                .then(files => {
                    if (currentFileId === 'root' && files.length === 0) {
                        this.askToCreateFolderOnRoot();
                    }
                    resolve(this.extractFileIds(files));
                }).catch(err => reject(err));
        });
    }

}
Example #10
Source File: BacklinksTreeDataProvider.ts    From dendron with GNU Affero General Public License v3.0 4 votes vote down vote up
/**
 * Provides the data to support the backlinks tree view panel
 */
export default class BacklinksTreeDataProvider
  implements TreeDataProvider<Backlink>, Disposable
{
  private readonly MAX_LINES_OF_CONTEXÌ£T = 10;
  private readonly FRONTMATTER_TAG_CONTEXT_PLACEHOLDER =
    "_Link is a Frontmatter Tag_";

  private _onDidChangeTreeDataEmitter: EventEmitter<
    Backlink | undefined | void
  >;
  private _onEngineNoteStateChangedDisposable: Disposable | undefined;
  private _onDidChangeActiveTextEditorDisposable: Disposable | undefined;
  private _engineEvents;
  private _sortOrder: BacklinkPanelSortOrder | undefined = undefined;
  readonly _isLinkCandidateEnabled: boolean | undefined;

  /**
   * Signals to vscode UI engine that the backlinks view needs to be refreshed.
   */
  readonly onDidChangeTreeData: Event<Backlink | undefined | void>;

  /**
   *
   * @param engineEvents - specifies when note state has been changed on the
   * engine
   */
  constructor(
    engineEvents: EngineEventEmitter,
    isLinkCandidateEnabled: boolean | undefined
  ) {
    this._isLinkCandidateEnabled = isLinkCandidateEnabled;

    // Set default sort order to use last updated
    this.sortOrder =
      MetadataService.instance().BacklinksPanelSortOrder ??
      BacklinkPanelSortOrder.LastUpdated;

    this._onDidChangeTreeDataEmitter = new EventEmitter<
      Backlink | undefined | void
    >();

    this.onDidChangeTreeData = this._onDidChangeTreeDataEmitter.event;
    this._engineEvents = engineEvents;
    this.setupSubscriptions();
  }

  /**
   * How items are sorted in the backlink panel
   */
  public get sortOrder(): BacklinkPanelSortOrder {
    return this._sortOrder!;
  }

  /**
   * Update the sort order of the backlinks panel. This will also save the
   * update into metadata service for persistence.
   */
  public set sortOrder(sortOrder: BacklinkPanelSortOrder) {
    if (sortOrder !== this._sortOrder) {
      this._sortOrder = sortOrder;

      VSCodeUtils.setContextStringValue(
        DendronContext.BACKLINKS_SORT_ORDER,
        sortOrder
      );

      this.refreshBacklinks();

      // Save the setting update into persistance storage:
      MetadataService.instance().BacklinksPanelSortOrder = sortOrder;
    }
  }

  public getTreeItem(element: Backlink) {
    try {
      return element;
    } catch (error) {
      Sentry.captureException(error);
      throw error;
    }
  }

  public getParent(element: Backlink): ProviderResult<Backlink> {
    try {
      if (element.parentBacklink) {
        return element.parentBacklink;
      } else {
        return undefined;
      }
    } catch (error) {
      Sentry.captureException(error);
      throw error;
    }
  }

  public async getChildren(element?: Backlink): Promise<Backlink[]> {
    try {
      // TODO: Make the backlinks panel also work when preview is the active editor.
      const activeNote = WSUtilsV2.instance().getActiveNote();

      if (!activeNote) {
        return [];
      }

      if (!element) {
        // Root case, branch will get top level backlinks.
        return this.getAllBacklinkedNotes(
          activeNote.id,
          this._isLinkCandidateEnabled,
          this._sortOrder!
        );
      } else {
        // 2nd-level children, which contains line-level references belonging to
        // a single note
        const refs = element?.refs;
        if (!refs) {
          return [];
        }

        return this.getAllBacklinksInNoteFromRefs(
          refs,
          activeNote.fname,
          element
        );
      }
    } catch (error) {
      Sentry.captureException(error);
      throw error;
    }
  }

  /**
   * Implementing this method allows us to asynchronously calculate hover
   * contents ONLY when the user actually hovers over an item. Lazy loading this
   * data allows us to speed up the initial load time of the backlinks panel.
   * @param _item
   * @param element
   * @param _token
   * @returns
   */
  public resolveTreeItem(
    _item: TreeItem,
    element: Backlink,
    _token: CancellationToken
  ): ProviderResult<TreeItem> {
    // This method implies that an item was hovered over
    AnalyticsUtils.track(VSCodeEvents.BacklinksPanelUsed, {
      type: "ItemHoverDisplayed",
      state: element.treeItemType,
    });

    if (
      element.treeItemType === BacklinkTreeItemType.noteLevel &&
      element.refs
    ) {
      return new Promise<TreeItem>((resolve) => {
        this.getTooltipForNoteLevelTreeItem(element.refs!).then((tooltip) => {
          resolve({
            tooltip,
          });
        });
      });
    } else if (element.treeItemType === BacklinkTreeItemType.referenceLevel) {
      return new Promise<TreeItem>((resolve) => {
        if (element.singleRef?.isFrontmatterTag) {
          resolve({
            tooltip: new MarkdownString(
              this.FRONTMATTER_TAG_CONTEXT_PLACEHOLDER
            ),
          });
        }
        this.getSurroundingContextForRef(
          element.singleRef!,
          this.MAX_LINES_OF_CONTEXÌ£T
        ).then((value) => {
          const tooltip = new MarkdownString();
          tooltip.appendMarkdown(value);

          tooltip.supportHtml = true;
          tooltip.isTrusted = true;
          tooltip.supportThemeIcons = true;

          resolve({
            tooltip,
          });
        });
      });
    } else {
      return undefined;
    }
  }

  public dispose(): void {
    if (this._onDidChangeTreeDataEmitter) {
      this._onDidChangeTreeDataEmitter.dispose();
    }
    if (this._onEngineNoteStateChangedDisposable) {
      this._onEngineNoteStateChangedDisposable.dispose();
    }
    if (this._onDidChangeActiveTextEditorDisposable) {
      this._onDidChangeActiveTextEditorDisposable.dispose();
    }
  }

  private setupSubscriptions(): void {
    this._onDidChangeActiveTextEditorDisposable =
      window.onDidChangeActiveTextEditor(() => {
        const ctx = "refreshBacklinksChangeActiveTextEditor";
        Logger.info({ ctx });
        this.refreshBacklinks();
      });

    this._onEngineNoteStateChangedDisposable =
      this._engineEvents.onEngineNoteStateChanged(() => {
        const ctx = "refreshBacklinksEngineNoteStateChanged";
        Logger.info({ ctx });
        this.refreshBacklinks();
      });
  }

  /**
   * Tells VSCode to refresh the backlinks view. Debounced to fire every 250 ms
   */
  private refreshBacklinks = _.debounce(() => {
    this._onDidChangeTreeDataEmitter.fire();
  }, 250);

  /**
   * Takes found references corresponding to a single note and turn them into
   * TreeItems
   * @param refs list of found references (for a single note)
   * @param fsPath fsPath of current note
   * @param parent parent backlink of these refs.
   * @returns list of TreeItems of found references
   */
  private getAllBacklinksInNoteFromRefs = (
    refs: FoundRefT[],
    fsPath: string,
    parent: Backlink
  ) => {
    return refs.map((ref) => {
      const lineNum = ref.location.range.start.line;
      const backlink = Backlink.createRefLevelBacklink(ref);

      backlink.iconPath = ref.isCandidate
        ? new ThemeIcon(ICONS.LINK_CANDIDATE)
        : new ThemeIcon(ICONS.WIKILINK);
      backlink.parentBacklink = parent;
      backlink.description = `on line ${lineNum + 1}`;

      backlink.command = {
        command: DENDRON_COMMANDS.GOTO_BACKLINK.key,
        arguments: [
          ref.location.uri,
          { selection: ref.location.range },
          ref.isCandidate ?? false,
        ],
        title: "Open File",
      };
      if (ref.isCandidate) {
        backlink.command = {
          command: "dendron.convertCandidateLink",
          title: "Convert Candidate Link",
          arguments: [
            { location: ref.location, text: path.parse(fsPath).name },
          ],
        };
      }
      return backlink;
    });
  };

  /**
   * Return the array of notes that have backlinks to the current note ID as
   * Backlink TreeItem objects
   * @param noteId - note ID for which to get backlinks for
   * @param isLinkCandidateEnabled
   * @param sortOrder
   * @returns
   */
  private async getAllBacklinkedNotes(
    noteId: string,
    isLinkCandidateEnabled: boolean | undefined,
    sortOrder: BacklinkPanelSortOrder
  ): Promise<Backlink[]> {
    const references = await findReferencesById(noteId);
    const referencesByPath = _.groupBy(
      // Exclude self-references:
      _.filter(references, (ref) => ref.note?.id !== noteId),
      ({ location }) => location.uri.fsPath
    );

    let pathsSorted: string[];
    if (sortOrder === BacklinkPanelSortOrder.PathNames) {
      pathsSorted = this.shallowFirstPathSort(referencesByPath);
    } else if (sortOrder === BacklinkPanelSortOrder.LastUpdated) {
      pathsSorted = Object.keys(referencesByPath).sort((p1, p2) => {
        const ref1 = referencesByPath[p1];
        const ref2 = referencesByPath[p2];

        if (
          ref1.length === 0 ||
          ref2.length === 0 ||
          ref1[0].note === undefined ||
          ref2[0].note === undefined
        ) {
          Logger.error({
            msg: "Missing info for well formed backlink sort by last updated.",
          });

          return 0;
        }

        const ref2Updated = ref2[0].note.updated;
        const ref1Updated = ref1[0].note.updated;

        // We want to sort in descending order by last updated
        return ref2Updated - ref1Updated;
      });
    } else assertUnreachable(sortOrder);

    if (!pathsSorted.length) {
      return [];
    }

    const out = pathsSorted.map((pathParam) => {
      const references = referencesByPath[pathParam];

      const backlink = Backlink.createNoteLevelBacklink(
        _.trimEnd(path.basename(pathParam), path.extname(pathParam)),
        references
      );

      const totalCount = references.length;
      const linkCount = references.filter((ref) => !ref.isCandidate).length;
      const candidateCount = isLinkCandidateEnabled
        ? totalCount - linkCount
        : 0;

      const backlinkCount = isLinkCandidateEnabled
        ? references.length
        : references.filter((ref) => !ref.isCandidate).length;

      if (backlinkCount === 0) return undefined;

      let linkCountDescription;

      if (linkCount === 1) {
        linkCountDescription = "1 link";
      } else if (linkCount > 1) {
        linkCountDescription = `${linkCount} links`;
      }

      let candidateCountDescription;

      if (candidateCount === 1) {
        candidateCountDescription = "1 candidate";
      } else if (candidateCount > 1) {
        candidateCountDescription = `${candidateCountDescription} candidates`;
      }

      const description = _.compact([
        linkCountDescription,
        candidateCountDescription,
      ]).join(", ");

      backlink.description = description;

      backlink.command = {
        command: DENDRON_COMMANDS.GOTO_BACKLINK.key,
        arguments: [
          Uri.file(pathParam),
          { selection: references[0].location.range },
          false,
        ],
        title: "Open File",
      };

      return backlink;
    });
    return _.filter(out, (item) => !_.isUndefined(item)) as Backlink[];
  }

  private shallowFirstPathSort(
    referencesByPath: Dictionary<[unknown, ...unknown[]]>
  ) {
    return sortPaths(Object.keys(referencesByPath), {
      shallowFirst: true,
    });
  }

  /**
   * This tooltip will return a markdown string that has several components:
   * 1. A header section containing title, created, and updated times
   * 2. A concatenated list of references with some lines of surrounding context
   *    for each one.
   * @param references
   * @returns
   */
  private async getTooltipForNoteLevelTreeItem(
    references: FoundRefT[]
  ): Promise<MarkdownString> {
    // Shoot for around a max of 40 lines to render in the hover, otherwise,
    // it's a bit too long. Note, this doesn't take into account note reference
    // length, so those can potentially blow up the size of the context.
    // Factoring in note ref length can be a later enhancement
    let linesOfContext = 0;

    switch (references.length) {
      case 1: {
        linesOfContext = this.MAX_LINES_OF_CONTEXÌ£T;
        break;
      }
      case 2: {
        linesOfContext = 7;
        break;
      }
      case 3: {
        linesOfContext = 5;
        break;
      }
      default:
        linesOfContext = 3;
        break;
    }

    const markdownBlocks = await Promise.all(
      references.map(async (foundRef) => {
        // Just use a simple place holder if it's a frontmatter tag instead of
        // trying to render context
        if (foundRef.isFrontmatterTag) {
          return {
            content: this.FRONTMATTER_TAG_CONTEXT_PLACEHOLDER,
            isCandidate: false,
          };
        }

        return {
          content: (await this.getSurroundingContextForRef(
            foundRef,
            linesOfContext
          ))!,
          isCandidate: foundRef.isCandidate,
        };
      })
    );

    const tooltip = new MarkdownString();
    tooltip.isTrusted = true;
    tooltip.supportHtml = true;
    tooltip.supportThemeIcons = true;

    const noteProps = references[0].note;

    if (noteProps) {
      tooltip.appendMarkdown(
        `## ${noteProps.title}
_created: ${DateFormatUtil.formatDate(noteProps.created)}_<br>
_updated: ${DateFormatUtil.formatDate(noteProps.updated)}_`
      );
      tooltip.appendMarkdown("<hr/>");
    }

    let curLinkCount = 1;
    let curCandidateCount = 1;

    for (const block of markdownBlocks) {
      let header;
      if (block.isCandidate) {
        header = `\n\n**CANDIDATE ${curCandidateCount}**<br>`;
        curCandidateCount += 1;
      } else {
        header = `\n\n**LINK ${curLinkCount}**<br>`;
        curLinkCount += 1;
      }

      tooltip.appendMarkdown(header);
      tooltip.appendMarkdown(block.content);
      tooltip.appendMarkdown("<hr/>");
    }

    return tooltip;
  }

  private async getSurroundingContextForRef(
    ref: FoundRefT,
    linesOfContext: number
  ): Promise<string> {
    const proc = MDUtilsV5.procRemarkFull(
      {
        engine: ExtensionProvider.getEngine(),
        fname: ref.note.fname,
        vault: ref.note.vault,
        dest: DendronASTDest.MD_REGULAR,
        backlinkHoverOpts: {
          linesOfContext,
          location: {
            start: {
              line: ref.location.range.start.line + 1, // 1 indexed
              column: ref.location.range.start.character + 1, // 1 indexed
            },
            end: {
              line: ref.location.range.end.line + 1,
              column: ref.location.range.end.character + 1,
            },
          },
        },
      },
      {
        flavor: ProcFlavor.BACKLINKS_PANEL_HOVER,
      }
    );

    const note = ref.note!;

    const fsPath = NoteUtils.getFullPath({
      note,
      wsRoot: ExtensionProvider.getDWorkspace().wsRoot,
    });

    const fileContent = fs.readFileSync(fsPath).toString();

    return (await proc.process(fileContent)).toString();
  }
}
Example #11
Source File: tree-data-provider.ts    From plugin-vscode with Apache License 2.0 4 votes vote down vote up
/**
 * Data provider class for package tree.
 */
export class PackageOverviewDataProvider implements TreeDataProvider<PackageTreeItem> {
    private langClient?: ExtendedLangClient;
    private ballerinaExtension: BallerinaExtension;
    private extensionPath: string;

    constructor(ballerinaExtension: BallerinaExtension) {
        this.ballerinaExtension = ballerinaExtension;
        this.langClient = ballerinaExtension.langClient;
        this.extensionPath = ballerinaExtension.extension.extensionPath;
        workspace.onDidOpenTextDocument(document => {
            if (document.languageId === BALLERINA || document.fileName.endsWith(BAL_TOML)) {
                this.refresh();
            }
        });
        workspace.onDidChangeTextDocument(activatedTextEditor => {
            if (activatedTextEditor && activatedTextEditor.document.languageId === BALLERINA ||
                activatedTextEditor.document.fileName.endsWith(BAL_TOML)) {
                this.refresh();
            }
        });
    }
    private _onDidChangeTreeData: EventEmitter<PackageTreeItem | undefined> = new EventEmitter<PackageTreeItem | undefined>();
    readonly onDidChangeTreeData: Event<PackageTreeItem | undefined> = this._onDidChangeTreeData.event;

    refresh(): void {
        this._onDidChangeTreeData.fire(undefined);
    }

    getTreeItem(element: PackageTreeItem): TreeItem | Thenable<TreeItem> {
        return element;
    }

    getParent?(element: PackageTreeItem): ProviderResult<PackageTreeItem> {
        return element.getParent();
    }

    getChildren(element?: PackageTreeItem): ProviderResult<PackageTreeItem[]> {
        if (!this.ballerinaExtension.isSwanLake) {
            window.showErrorMessage("Ballerina package overview is only supported in Swan Lake.");
            return;
        }
        if (!element) {
            return this.getPackageStructure();
        } else if (element.getKind() === PROJECT_KIND.PACKAGE) {
            return this.getModuleStructure(element);
        } else if (element.getKind() === PROJECT_KIND.MODULE) {
            return this.getComponentStructure(element);
        } else if (element.getKind() === PROJECT_KIND.SERVICE) {
            return this.getResourceStructure(element);
        }
    }

    /**
     * Returns the tree structure for packages.
     * @returns An array of tree nodes with package data.
     */
    public getPackageStructure(): Promise<PackageTreeItem[]> {
        return new Promise<PackageTreeItem[]>((resolve) => {
            if (!window.activeTextEditor) {
                resolve([]);
            }
            const activeDocument = window.activeTextEditor!.document;
            this.ballerinaExtension.onReady().then(() => {
                this.langClient!.getBallerinaProject({
                    documentIdentifier: {
                        uri: activeDocument.uri.toString()
                    }
                }).then(project => {
                    const uri: string = activeDocument.fileName.endsWith(BAL_TOML) ? activeDocument.uri.toString(true).replace(BAL_TOML, '') :
                        activeDocument.uri.toString(true);
                    const documentIdentifiers: DocumentIdentifier[] = [{ uri }];
                    if (project.kind === PROJECT_TYPE.BUILD_PROJECT || project.kind === PROJECT_TYPE.SINGLE_FILE) {
                        this.langClient!.getBallerinaProjectComponents({ documentIdentifiers }).then((response) => {
                            if (response.packages) {
                                const projectItems: PackageTreeItem[] = this.createPackageData(response.packages);
                                resolve(projectItems);
                            } else {
                                resolve([]);
                            }
                        });
                    } else {
                        resolve([]);
                    }
                });
            }).catch(() => {
                resolve([]);
            });
        });
    }

    /**
     * Returns the tree structure for functions and services.
     * @returns An array of tree nodes with module component data.
     */
    private getComponentStructure(parent: PackageTreeItem, isDefaultModule: boolean = false,
        childrenData: ChildrenData = {}): PackageTreeItem[] {
        let components: PackageTreeItem[] = [];
        const children: ChildrenData = isDefaultModule ? childrenData : parent.getChildrenData();
        //Process function nodes
        if (children.functions) {
            const functionNodes = children.functions;
            functionNodes.sort((fn1, fn2) => {
                return fn1.name!.localeCompare(fn2.name!);
            });
            functionNodes.forEach(fn => {
                components.push(new PackageTreeItem(fn.name, `${fn.filePath}`, TreeItemCollapsibleState.None,
                    PROJECT_KIND.FUNCTION, join(parent.getFilePath(), fn.filePath), this.extensionPath, true, parent,
                    {}, fn.startLine, fn.startColumn, fn.endLine, fn.endColumn));
            });
        }

        //Process service nodes
        if (children.services) {
            const serviceNodes = children.services.filter(service => {
                return service.name;
            });
            serviceNodes.sort((service1, service2) => {
                return service1.name!.localeCompare(service2.name!);
            });
            serviceNodes.forEach(service => {
                components.push(new PackageTreeItem(service.name, `${service.filePath}`,
                    TreeItemCollapsibleState.Collapsed, PROJECT_KIND.SERVICE, join(parent.getFilePath(),
                        service.filePath), this.extensionPath, true, parent, { resources: service.resources },
                    service.startLine, service.startColumn, service.endLine, service.endColumn));
            });

            const serviceNodesWithoutName = children.services.filter(service => {
                return !service.name;
            });
            let count: number = 0;
            serviceNodesWithoutName.forEach(service => {
                components.push(new PackageTreeItem(`${PROJECT_KIND.SERVICE} ${++count}`, `${service.filePath}`,
                    TreeItemCollapsibleState.Collapsed, PROJECT_KIND.SERVICE, join(parent.getFilePath(),
                        service.filePath), this.extensionPath, true, parent, { resources: service.resources },
                    service.startLine, service.startColumn, service.endLine, service.endColumn));
            });
        }
        return components;
    }

    private createPackageData(packages: Package[]): PackageTreeItem[] {
        let packageItems: PackageTreeItem[] = [];
        packages.sort((package1, package2) => {
            return package1.name.localeCompare(package2.name!);
        });
        packages.forEach(projectPackage => {
            projectPackage.name = projectPackage.name === '.' ? window.activeTextEditor!.document.fileName
                .replace('.bal', '').split(sep).pop()!.toString() : projectPackage.name;
            if (projectPackage.name) {
                packageItems.push(new PackageTreeItem(projectPackage.name, '',
                    TreeItemCollapsibleState.Expanded, PROJECT_KIND.PACKAGE, fileUriToPath(projectPackage.filePath),
                    this.extensionPath, true, null, { modules: projectPackage.modules }));
            }
        });
        return packageItems;
    }

    /**
     * Returns the tree structure for modules.
     * @returns An array of tree nodes with module data.
     */
    private getModuleStructure(parent: PackageTreeItem): PackageTreeItem[] {
        let moduleItems: PackageTreeItem[] = [];
        if (parent.getChildrenData().modules) {
            const defaultModules: Module[] = parent.getChildrenData().modules!.filter(module => {
                return !module.name;
            });
            if (defaultModules.length === 1) {
                const defaultModuleItems: PackageTreeItem[] = this.getComponentStructure(parent, true,
                    { functions: defaultModules[0].functions, services: defaultModules[0].services });
                if (defaultModuleItems.length > 0) {
                    moduleItems = moduleItems.concat(defaultModuleItems);
                }
            }

            const nonDefaultModules: Module[] = parent.getChildrenData().modules!.filter(module => {
                return module.name;
            });
            nonDefaultModules.sort((mod1, mod2) => {
                return mod1.name!.localeCompare(mod2.name!);
            });
            nonDefaultModules.forEach(module => {
                moduleItems.push(new PackageTreeItem(module.name!, '',
                    TreeItemCollapsibleState.Collapsed, PROJECT_KIND.MODULE, join(parent.getFilePath(), 'modules',
                        module.name!), this.extensionPath, false, parent,
                    {
                        functions: module.functions,
                        services: module.services
                    }));
            });
        }
        return moduleItems;
    }

    /**
      * Returns the tree structure for resources.
      * @returns An array of tree nodes with resource data.
      */
    private getResourceStructure(parent: PackageTreeItem): PackageTreeItem[] {
        let resources: PackageTreeItem[] = [];
        const children: ChildrenData = parent.getChildrenData();
        if (children.resources) {
            const resourceNodes = children.resources;
            resourceNodes.sort((resource1, resource2) => {
                return resource1.name!.localeCompare(resource2.name!);
            });
            resourceNodes.forEach(resource => {
                resources.push(new PackageTreeItem(resource.name, '',
                    TreeItemCollapsibleState.None, PROJECT_KIND.RESOURCE, parent.getFilePath(), this.extensionPath, true,
                    parent, {}, resource.startLine, resource.startColumn, resource.endLine, resource.endColumn));
            });
        }
        return resources;
    }
}
Example #12
Source File: EngineNoteProvider.ts    From dendron with GNU Affero General Public License v3.0 4 votes vote down vote up
/**
 * Provides engine event data to generate the views for the native Tree View
 */
export class EngineNoteProvider
  implements TreeDataProvider<NoteProps>, Disposable
{
  private _onDidChangeTreeDataEmitter: EventEmitter<
    NoteProps | undefined | void
  >;
  private _onEngineNoteStateChangedDisposable: Disposable;
  private _tree: { [key: string]: TreeNote } = {};
  private _engineEvents;
  private _labelType: TreeViewItemLabelTypeEnum;
  /**
   * Signals to vscode UI engine that the tree view needs to be refreshed.
   */
  readonly onDidChangeTreeData: Event<NoteProps | undefined | void>;

  /**
   *
   * @param engineEvents - specifies when note state has been changed on the
   * engine
   */
  constructor(engineEvents: EngineEventEmitter) {
    this._onDidChangeTreeDataEmitter = new EventEmitter<
      NoteProps | undefined | void
    >();

    this.onDidChangeTreeData = this._onDidChangeTreeDataEmitter.event;
    this._engineEvents = engineEvents;
    this._onEngineNoteStateChangedDisposable = this.setupSubscriptions();
    this._labelType = MetadataService.instance().getTreeViewItemLabelType();
    VSCodeUtils.setContextStringValue(
      DendronContext.TREEVIEW_TREE_ITEM_LABEL_TYPE,
      this._labelType
    );
  }

  dispose(): void {
    if (this._onDidChangeTreeDataEmitter) {
      this._onDidChangeTreeDataEmitter.dispose();
    }
    if (this._onEngineNoteStateChangedDisposable) {
      this._onEngineNoteStateChangedDisposable.dispose();
    }
  }

  getTree(): { [key: string]: TreeNote } {
    return this._tree;
  }

  getTreeItem(noteProps: NoteProps): TreeItem {
    try {
      return this._tree[noteProps.id];
    } catch (error) {
      Sentry.captureException(error);
      throw error;
    }
  }

  public getExpandableTreeItems(): TreeItem[] {
    const candidateItems = _.toArray(
      _.pickBy(this._tree, (item) => {
        const isCollapsed =
          item.collapsibleState === TreeItemCollapsibleState.Collapsed;
        const isShallow = DNodeUtils.getDepth(item.note) < 3;
        return isCollapsed && isShallow;
      })
    );
    return _.sortBy(candidateItems, (item) => {
      return DNodeUtils.getDepth(item.note);
    });
  }

  getChildren(noteProps?: NoteProps): ProviderResult<NoteProps[]> {
    try {
      const ctx = "TreeView:getChildren";
      Logger.debug({ ctx, id: noteProps });
      const { engine } = ExtensionProvider.getDWorkspace();
      const roots = _.filter(_.values(engine.notes), DNodeUtils.isRoot);
      if (!roots) {
        window.showInformationMessage("No notes found");
        return Promise.resolve([]);
      }
      if (noteProps) {
        const childrenIds = TreeUtils.sortNotesAtLevel({
          noteIds: noteProps.children,
          noteDict: engine.notes,
          labelType: this._labelType,
        });

        const childrenNoteProps = childrenIds.map((id) => {
          return engine.notes[id];
        });

        return Promise.resolve(childrenNoteProps);
      } else {
        Logger.info({ ctx, msg: "reconstructing tree: enter" });
        const out = Promise.all(
          roots.flatMap(async (root) => {
            const treeNote = await this.parseTree(root, engine.notes);
            return treeNote.note;
          })
        );
        Logger.info({ ctx, msg: "reconstructing tree: exit" });
        return out;
      }
    } catch (error) {
      Sentry.captureException(error);
      throw error;
    }
  }

  getParent(id: NoteProps): ProviderResult<NoteProps> {
    try {
      const { engine: client } = ExtensionProvider.getDWorkspace();

      const maybeParent = client.notes[id.parent || ""];
      return maybeParent || null;
    } catch (error) {
      Sentry.captureException(error);
      throw error;
    }
  }

  private setupSubscriptions(): Disposable {
    return this._engineEvents.onEngineNoteStateChanged(() => {
      this.refreshTreeView();
    });
  }

  public updateLabelType(opts: {
    labelType: TreeViewItemLabelTypeEnum;
    noRefresh?: boolean;
  }) {
    const { labelType, noRefresh } = opts;
    this._labelType = labelType;

    VSCodeUtils.setContextStringValue(
      DendronContext.TREEVIEW_TREE_ITEM_LABEL_TYPE,
      labelType
    );

    MetadataService.instance().setTreeViewItemLabelType(labelType);
    if (!noRefresh) {
      this.refreshTreeView();
    }
  }

  /**
   * Tells VSCode to refresh the tree view. Debounced to fire every 250 ms
   */
  private refreshTreeView = _.debounce(() => {
    this._onDidChangeTreeDataEmitter.fire();
  }, 250);

  private createTreeNote(note: NoteProps) {
    const collapsibleState = _.isEmpty(note.children)
      ? TreeItemCollapsibleState.None
      : TreeItemCollapsibleState.Collapsed;

    const tn = new TreeNote({
      note,
      collapsibleState,
      labelType: this._labelType,
    });
    if (note.stub) {
      tn.iconPath = new ThemeIcon(ICONS.STUB);
    } else if (note.schema) {
      tn.iconPath = new ThemeIcon(ICONS.SCHEMA);
    }
    return tn;
  }

  private async parseTree(
    note: NoteProps,
    ndict: NotePropsByIdDict
  ): Promise<TreeNote> {
    const ctx = "parseTree";
    const tn = this.createTreeNote(note);
    this._tree[note.id] = tn;

    const labelType = this._labelType;

    const children = TreeUtils.sortNotesAtLevel({
      noteIds: note.children,
      noteDict: ndict,
      labelType,
    });
    tn.children = await Promise.all(
      children.map(async (c) => {
        const childNote = ndict[c];
        if (!childNote) {
          const payload = {
            msg: `no childNote found: ${c}, current note: ${note.id}`,
            fullDump: _.values(ndict).map((n) => NoteUtils.toLogObj(n)),
          };
          const err = new DendronError({
            message: "error updating tree view",
            payload,
          });
          Logger.error({ ctx, error: err });
          throw err;
        }
        return (await this.parseTree(childNote, ndict)).id;
      })
    );
    return tn;
  }
}
Example #13
Source File: ModelTreeviewProvider.ts    From vscode-dbt-power-user with MIT License 4 votes vote down vote up
@provide(ModelTreeviewProvider)
abstract class ModelTreeviewProvider
  implements TreeDataProvider<NodeTreeItem>, Disposable {
  private eventMap: Map<string, ManifestCacheProjectAddedEvent> = new Map();
  private _onDidChangeTreeData: EventEmitter<
    ModelTreeItem | undefined | void
  > = new EventEmitter<ModelTreeItem | undefined | void>();
  readonly onDidChangeTreeData: Event<ModelTreeItem | undefined | void> = this
    ._onDidChangeTreeData.event;
  private disposables: Disposable[] = [this._onDidChangeTreeData];

  constructor(
    private dbtProjectContainer: DBTProjectContainer,
    @unmanaged() private treeType: keyof GraphMetaMap
  ) {
    this.treeType = treeType;
    this.disposables.push(
      window.onDidChangeActiveTextEditor(() => {
        this._onDidChangeTreeData.fire();
      }),
      this.dbtProjectContainer.onManifestChanged((event) =>
        this.onManifestCacheChanged(event)
      )
    );
  }

  dispose() {
    this.disposables.forEach((disposable) => disposable.dispose());
  }

  private onManifestCacheChanged(event: ManifestCacheChangedEvent): void {
    event.added?.forEach((added) => {
      this.eventMap.set(added.projectRoot.fsPath, added);
    });
    event.removed?.forEach((removed) => {
      this.eventMap.delete(removed.projectRoot.fsPath);
    });
    this._onDidChangeTreeData.fire();
  }

  getTreeItem(element: NodeTreeItem): NodeTreeItem | Thenable<ModelTreeItem> {
    return element;
  }

  getChildren(element?: NodeTreeItem): Thenable<NodeTreeItem[]> {
    if (window.activeTextEditor === undefined || this.eventMap === undefined) {
      return Promise.resolve([]);
    }

    const currentFilePath = window.activeTextEditor.document.uri;
    const projectRootpath = this.dbtProjectContainer.getProjectRootpath(
      currentFilePath
    );
    if (projectRootpath === undefined) {
      return Promise.resolve([]);
    }

    const event = this.eventMap.get(projectRootpath.fsPath);
    if (event === undefined) {
      return Promise.resolve([]);
    }

    if (element) {
      return Promise.resolve(this.getTreeItems(element.key, event));
    }

    const { projectName } = event;
    const fileName = path.basename(
      window.activeTextEditor!.document.fileName,
      ".sql"
    );
    const packageName =
      this.dbtProjectContainer.getPackageName(currentFilePath) || projectName;
    return Promise.resolve(
      this.getTreeItems(`model.${packageName}.${fileName}`, event)
    );
  }

  private getTreeItems(
    elementName: string,
    event: ManifestCacheProjectAddedEvent
  ): NodeTreeItem[] {
    const { graphMetaMap } = event;
    const parentModels = graphMetaMap[this.treeType].get(elementName);
    if (parentModels === undefined) {
      return [];
    }
    return parentModels.nodes
      .filter((node) => node.displayInModelTree)
      .map((node) => {
        const childNodes = graphMetaMap[this.treeType]
          .get(node.key)
          ?.nodes.filter((node) => node.displayInModelTree);

        if (node instanceof Model && childNodes?.length === 0) {
          return new DashboardTreeItem(node);
        }
        return node instanceof Source
          ? new SourceTreeItem(node)
          : new ModelTreeItem(node);
      });
  }
}
Example #14
Source File: radio.ts    From cloudmusic-vscode with MIT License 4 votes vote down vote up
export class RadioProvider
  implements TreeDataProvider<UserTreeItem | RadioTreeItem | ProgramTreeItem>
{
  private static _instance: RadioProvider;

  private static readonly _actions = new WeakMap<
    RadioTreeItem,
    { resolve: (value: PlayTreeItemData[]) => void; reject: () => void }
  >();

  _onDidChangeTreeData = new EventEmitter<
    UserTreeItem | RadioTreeItem | ProgramTreeItem | void
  >();

  readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

  static getInstance(): RadioProvider {
    return this._instance || (this._instance = new RadioProvider());
  }

  static refresh(): void {
    this._instance._onDidChangeTreeData.fire();
  }

  static refreshUser(element: UserTreeItem): void {
    IPC.deleteCache(`dj_sublist${element.uid}`);
    this._instance._onDidChangeTreeData.fire(element);
  }

  static async refreshRadio(
    element: RadioTreeItem
  ): Promise<readonly PlayTreeItemData[]> {
    const old = this._actions.get(element);
    old?.reject();
    return new Promise((resolve, reject) => {
      this._actions.set(element, { resolve, reject });
      this._instance._onDidChangeTreeData.fire(element);
    });
  }

  static refreshRadioHard(element: RadioTreeItem): void {
    IPC.deleteCache(`dj_program${element.valueOf}`);
    this._instance._onDidChangeTreeData.fire(element);
  }

  getTreeItem(
    element: UserTreeItem | RadioTreeItem | ProgramTreeItem
  ): UserTreeItem | RadioTreeItem | ProgramTreeItem {
    return element;
  }

  async getChildren(
    element?: UserTreeItem | RadioTreeItem
  ): Promise<UserTreeItem[] | RadioTreeItem[] | ProgramTreeItem[]> {
    if (!element) {
      const accounts = [];
      for (const [, { userId, nickname }] of AccountManager.accounts)
        accounts.push(UserTreeItem.new(nickname, userId));
      return accounts;
    }
    if (element instanceof UserTreeItem) {
      const { uid } = element;
      return (await AccountManager.djradio(uid)).map((radio) =>
        RadioTreeItem.new(radio)
      );
    }
    const {
      uid,
      item: { id: pid, programCount },
    } = element;
    const programs = await IPC.netease("djProgram", [uid, pid, programCount]);
    const ret = programs.map((program) =>
      ProgramTreeItem.new({ ...program, pid })
    );
    const action = RadioProvider._actions.get(element);
    if (action) {
      RadioProvider._actions.delete(element);
      action.resolve(ret.map(({ data }) => data));
    }
    return ret;
  }
}
Example #15
Source File: queue.ts    From cloudmusic-vscode with MIT License 4 votes vote down vote up
export class QueueProvider implements TreeDataProvider<QueueContent> {
  static id = -1;

  private static _songs: PlayTreeItemData[] = [];

  private static _instance: QueueProvider;

  _onDidChangeTreeData = new EventEmitter<QueueContent | void>();

  readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

  static get len(): number {
    return this._songs.length;
  }

  static get head(): QueueContent | undefined {
    return this._songs?.[0] ? this._parseRaw(this._songs[0]) : undefined;
  }

  static get next(): QueueContent | undefined {
    return this._songs?.[1] ? this._parseRaw(this._songs[1]) : undefined;
  }

  static get songs(): readonly PlayTreeItemData[] {
    return this._songs;
  }

  static getInstance(): QueueProvider {
    return this._instance || (this._instance = new QueueProvider());
  }

  static random(): readonly PlayTreeItemData[] {
    const [head, ...rest] = this._songs;
    return head ? [head, ...unsortInplace(rest)] : [];
  }

  static new(elements: readonly PlayTreeItemData[], id = this.id + 1): void {
    if (this.id === id) return;
    this.id = id;
    this._clear();
    this._add(elements);

    this._instance._onDidChangeTreeData.fire();
  }

  static clear(): void {
    this._clear();

    this._instance._onDidChangeTreeData.fire();
  }

  static top(that: number | string): void {
    this.shift(this._songs.findIndex(({ id }) => that === id));
  }

  static shift(index: number): void {
    this._shift(index);

    this._instance._onDidChangeTreeData.fire();
  }

  static add(
    elements: readonly PlayTreeItemData[],
    index: number = this.len
  ): void {
    this._add(elements, index);

    this._instance._onDidChangeTreeData.fire();
  }

  static delete(that: number | string): void {
    const index = this._songs.findIndex(({ id }) => that === id);
    if (index >= 0) this._songs.splice(index, 1);

    this._instance._onDidChangeTreeData.fire();
  }

  static sort(
    type: QueueSortType,
    order: QueueSortOrder
  ): readonly PlayTreeItemData[] {
    const getName = (item: PlayTreeItemData): string => {
      switch (item.itemType) {
        case "q":
          return item.name;
        case "p":
          return item.mainSong.name;
        case "l":
          return item.filename;
      }
    };
    const getAlbum = (item: PlayTreeItemData): string => {
      switch (item.itemType) {
        case "q":
          return item.al.name;
        case "p":
          return item.mainSong.al.name;
        case "l":
          return item.ext ?? "";
      }
    };
    const getArtist = (item: PlayTreeItemData): string => {
      switch (item.itemType) {
        case "q":
          return item.ar?.[0].name ?? "";
        case "p":
          return item.mainSong.ar?.[0].name ?? "";
        case "l":
          return "";
      }
    };

    switch (type) {
      case QueueSortType.song:
        this._songs.sort(
          order === QueueSortOrder.ascending
            ? (a, b) => getName(a).localeCompare(getName(b))
            : (a, b) => getName(b).localeCompare(getName(a))
        );
        break;
      case QueueSortType.album:
        this._songs.sort(
          order === QueueSortOrder.ascending
            ? (a, b) => getAlbum(a).localeCompare(getAlbum(b))
            : (a, b) => getAlbum(b).localeCompare(getAlbum(a))
        );
        break;
      case QueueSortType.artist:
        this._songs.sort(
          order === QueueSortOrder.ascending
            ? (a, b) => getArtist(a).localeCompare(getArtist(b))
            : (a, b) => getArtist(b).localeCompare(getArtist(a))
        );
    }
    return this._songs;
  }

  private static _parseRaw(item: PlayTreeItemData): QueueContent {
    switch (item.itemType) {
      case "q":
        return QueueItemTreeItem.new(item);
      case "p":
        return ProgramTreeItem.new(item);
      default:
        return LocalFileTreeItem.new(item);
    }
  }

  private static _add(
    elements: readonly PlayTreeItemData[],
    index: number = this.len
  ): void {
    this._songs.splice(index, 0, ...elements);
    this._songs = [...new Set(this._songs.map((i) => this._parseRaw(i)))].map(
      ({ data }) => data
    );
  }

  private static _clear() {
    this._songs = [];
  }

  private static _shift(index: number): void {
    // Allow replay current playing song
    // if (index === 0) return;
    while (index < 0) index += this.len;
    this._songs.push(...this._songs.splice(0, index));
  }

  getTreeItem(element: QueueContent): QueueContent {
    return element;
  }

  getChildren(): QueueContent[] {
    return QueueProvider._songs.map((i) => QueueProvider._parseRaw(i));
  }
}
Example #16
Source File: playlist.ts    From cloudmusic-vscode with MIT License 4 votes vote down vote up
export class PlaylistProvider
  implements
    TreeDataProvider<UserTreeItem | PlaylistItemTreeItem | QueueItemTreeItem>
{
  private static _instance: PlaylistProvider;

  private static readonly _actions = new WeakMap<
    PlaylistItemTreeItem,
    { resolve: (value: PlayTreeItemData[]) => void; reject: () => void }
  >();

  _onDidChangeTreeData = new EventEmitter<
    UserTreeItem | PlaylistItemTreeItem | undefined | void
  >();

  readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

  static getInstance(): PlaylistProvider {
    return this._instance || (this._instance = new PlaylistProvider());
  }

  static refresh(): void {
    this._instance._onDidChangeTreeData.fire();
  }

  static refreshUser(element: UserTreeItem): void {
    IPC.deleteCache(`user_playlist${element.uid}`);
    this._instance._onDidChangeTreeData.fire(element);
  }

  static async refreshPlaylist(
    element: PlaylistItemTreeItem
  ): Promise<readonly PlayTreeItemData[]> {
    const old = this._actions.get(element);
    old?.reject();
    return new Promise((resolve, reject) => {
      this._actions.set(element, { resolve, reject });
      this._instance._onDidChangeTreeData.fire(element);
    });
  }

  static refreshPlaylistHard(element: PlaylistItemTreeItem): void {
    IPC.deleteCache(`playlist_detail${element.valueOf}`);
    this._instance._onDidChangeTreeData.fire(element);
  }

  getTreeItem(
    element: UserTreeItem | PlaylistItemTreeItem | QueueItemTreeItem
  ): UserTreeItem | PlaylistItemTreeItem | QueueItemTreeItem {
    return element;
  }

  async getChildren(
    element?: UserTreeItem | PlaylistItemTreeItem
  ): Promise<UserTreeItem[] | PlaylistItemTreeItem[] | QueueItemTreeItem[]> {
    if (!element) {
      const accounts = [];
      for (const [, { userId, nickname }] of AccountManager.accounts)
        accounts.push(UserTreeItem.new(nickname, userId));
      return accounts;
    }
    if (element instanceof UserTreeItem) {
      const { uid } = element;
      return (await AccountManager.playlist(uid)).map((playlist) =>
        PlaylistItemTreeItem.new(playlist, uid)
      );
    }
    const {
      uid,
      item: { id: pid },
    } = element;
    const songs = await IPC.netease("playlistDetail", [uid, pid]);
    const ret = songs.map((song) => QueueItemTreeItem.new({ ...song, pid }));
    const action = PlaylistProvider._actions.get(element);
    if (action) {
      PlaylistProvider._actions.delete(element);
      action.resolve(ret.map(({ data }) => data));
    }
    return ret;
  }
}
Example #17
Source File: local.ts    From cloudmusic-vscode with MIT License 4 votes vote down vote up
export class LocalProvider
  implements TreeDataProvider<LocalFileTreeItem | LocalLibraryTreeItem>
{
  static readonly folders: string[] = [];

  private static _instance: LocalProvider;

  private static readonly _files = new WeakMap<
    LocalLibraryTreeItem,
    LocalFileTreeItem[]
  >();

  private static _actions = new WeakMap<
    LocalLibraryTreeItem,
    { resolve: (value: PlayTreeItemData[]) => void; reject: () => void }
  >();

  _onDidChangeTreeData = new EventEmitter<LocalLibraryTreeItem | void>();

  readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

  static getInstance(): LocalProvider {
    return this._instance || (this._instance = new LocalProvider());
  }

  static refresh(): void {
    this._instance._onDidChangeTreeData.fire();
  }

  static async refreshLibrary(
    element: LocalLibraryTreeItem,
    hard?: boolean
  ): Promise<readonly PlayTreeItemData[]> {
    if (hard) this._files.delete(element);
    const old = this._actions.get(element);
    old?.reject();
    return new Promise((resolve, reject) => {
      this._actions.set(element, { resolve, reject });
      this._instance._onDidChangeTreeData.fire(element);
    });
  }

  getTreeItem(
    element: LocalFileTreeItem | LocalLibraryTreeItem
  ): LocalFileTreeItem | LocalLibraryTreeItem {
    return element;
  }

  async getChildren(
    element?: LocalLibraryTreeItem
  ): Promise<(LocalFileTreeItem | LocalLibraryTreeItem)[]> {
    if (!element)
      return [MUSIC_CACHE_DIR, ...LocalProvider.folders].map(
        (folder) => new LocalLibraryTreeItem(folder)
      );

    const action = LocalProvider._actions.get(element);
    LocalProvider._actions.delete(element);

    let items: LocalFileTreeItem[] = [];
    if (LocalProvider._files.has(element)) {
      items = LocalProvider._files.get(element) ?? [];
      action?.resolve(items.map(({ data }) => data));
      return items;
    }

    const folders: string[] = [element.label];
    try {
      for (let idx = 0; idx < folders.length; ++idx) {
        const folder = folders[idx];
        const dirents = await readdir(folder, { withFileTypes: true });
        const paths: string[] = [];

        for (const dirent of dirents) {
          if (dirent.isFile()) paths.push(dirent.name);
          else if (dirent.isDirectory())
            folders.push(resolve(folder, dirent.name));
        }

        const treeitems = (
          await Promise.all(
            paths.map(async (filename) => {
              const id = resolve(folder, filename);
              const mime = await fileTypeFromFile(id);
              return { filename, id, ...mime };
            })
          )
        )
          .filter(({ mime }) => mime && supportedType.includes(mime))
          .map((item) => LocalFileTreeItem.new(item));
        items.push(...treeitems);
      }
    } catch {}
    LocalProvider._files.set(element, items);

    action?.resolve(items.map(({ data }) => data));
    return items;
  }
}
Example #18
Source File: NinjaTreeView.ts    From al-objid with MIT License 4 votes vote down vote up
export abstract class NinjaTreeView implements TreeDataProvider<Node>, ViewController, Disposable {
    private readonly _id: string;
    private readonly _view: TreeView<Node>;
    private readonly _workspaceFoldersChangeEvent: Disposable;
    private _disposed: boolean = false;
    private _watchersMap: PropertyBag<Disposable[]> = {};
    private _watchers: Disposable[] = [];
    protected readonly _expandCollapseController: ExpandCollapseController;
    protected _onDidChangeTreeData = new EventEmitter<Node | void>();
    public readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

    public constructor(id: string) {
        this.setUpWatchers();
        this._id = id;
        this._view = this.createTreeView();
        this._workspaceFoldersChangeEvent = WorkspaceManager.instance.onDidChangeALFolders(
            this.onDidChangeWorkspaceFolders.bind(this)
        );
        this._expandCollapseController = new ExpandCollapseController(id, () => this.refresh());
    }

    private createTreeView(): TreeView<Node> {
        const view = window.createTreeView(this._id, {
            showCollapseAll: false, // Until there is showExpandAll, we have to use ExpandCollapsecontroller for this
            canSelectMany: false,
            treeDataProvider: this,
        });
        view.onDidCollapseElement(e => this._expandCollapseController.collapse(e.element));
        view.onDidExpandElement(e => this._expandCollapseController.expand(e.element));
        return view;
    }

    private onDidChangeWorkspaceFolders({ added, removed }: ALFoldersChangedEvent) {
        this.disposeWatchers(removed);
        this.setUpWatchers(added);
        this.refreshAfterWorkspaceChange(added, removed);
    }

    private setUpWatchers(alApps?: ALApp[]) {
        let apps = alApps;
        if (!apps || !apps.length) {
            apps = WorkspaceManager.instance.alApps;
        }
        if (apps.length === 0) {
            return;
        }
        for (let app of apps) {
            const manifestChangedWatcher = app.onManifestChanged(app => this.refreshAfterConfigChange(app));
            const confingChangedWatcher = app.onConfigChanged(app => this.refreshAfterConfigChange(app));
            this._watchers.push(manifestChangedWatcher);
            this._watchers.push(confingChangedWatcher);
            this._watchersMap[app.uri.fsPath] = [manifestChangedWatcher, confingChangedWatcher];
        }
    }

    private disposeWatchers(removed?: ALApp[]) {
        if (removed) {
            for (let app of removed) {
                const watchers = this._watchersMap[app.uri.fsPath];
                if (watchers && watchers.length) {
                    for (let disposable of watchers) {
                        disposable.dispose();
                    }
                }
            }
            return;
        }

        for (let disposable of this._watchers) {
            disposable.dispose();
        }
        this._watchers = [];
    }

    protected refresh() {
        this._onDidChangeTreeData.fire();
    }

    protected abstract refreshAfterConfigChange(app: ALApp): void;
    protected abstract refreshAfterWorkspaceChange(added: ALApp[], removed: ALApp[]): void;
    protected abstract getRootNodes(): Node[] | Promise<Node[]>;
    protected abstract disposeExtended(): void;
    protected abstract decorate(element: DecorableNode, decoration: Decoration): void;

    //#region Interface implementations

    // Implements TreeDataProvider<Node>
    public getTreeItem(element: Node): TreeItem {
        const item = element.getTreeItem();

        if (element instanceof DecorableNode && element.decoration) {
            this.decorate(element, element.decoration);
        }

        if (item.id) {
            item.id = `${item.id}.${item.collapsibleState}.${this._expandCollapseController.iteration}`;
            const state = this._expandCollapseController.getState(element, element.collapsibleState);
            if (state !== undefined && item.collapsibleState !== TreeItemCollapsibleState.None) {
                item.collapsibleState = state;
            }
        }
        return item;
    }

    // Implements TreeDataProvider<Node>
    public getChildren(element?: Node): Node[] | Promise<Node[]> {
        return element?.children || this.getRootNodes();
    }

    // Implements ViewController
    public abstract update(node: Node): void;

    // Implements Disposable
    public dispose() {
        if (this._disposed) {
            return;
        }
        this._disposed = true;
        this.disposeWatchers();
        this.disposeExtended();
        this._onDidChangeTreeData.dispose();
        this._workspaceFoldersChangeEvent.dispose();
        this._view.dispose();
    }

    //#endregion
}
Example #19
Source File: connectionProvider.ts    From vscode-ssh with MIT License 4 votes vote down vote up
export default class ConnectionProvider implements TreeDataProvider<AbstractNode> {
    _onDidChangeTreeData: EventEmitter<AbstractNode> = new EventEmitter<AbstractNode>();
    readonly onDidChangeTreeData: Event<AbstractNode> = this._onDidChangeTreeData.event;
    public static tempRemoteMap = new Map<string, { remote: string, sshConfig: SSHConfig }>()

    constructor(private context: ExtensionContext) {
        vscode.workspace.onDidSaveTextDocument(e => {
            const tempPath = path.resolve(e.fileName);
            const data = ConnectionProvider.tempRemoteMap.get(tempPath)
            if (data) {
                this.saveFile(tempPath, data.remote, data.sshConfig)
            }
        })
    }
    getTreeItem(element: AbstractNode): vscode.TreeItem {
        return element;
    }

    // usage: https://www.npmjs.com/package/redis
    async getChildren(element?: AbstractNode) {
        try {
            if (!element) {
                const config = this.getConnections();
                const nodes = Object.keys(config).map(key => {
                    const sshConfig = config[key];
                    if (sshConfig.private && existsSync(sshConfig.private)) {
                        sshConfig.privateKey = require('fs').readFileSync(sshConfig.private)
                    }
                    key=`${sshConfig.name ? sshConfig.name + "_" : ""}${key}`
                    return new ParentNode(sshConfig, key);
                });
                return nodes
            } else {
                return element.getChildren()
            }
        } catch (error) {
            return [new InfoNode(error)]
        }
    }

    async saveFile(tempPath: string, remotePath: string, sshConfig: SSHConfig) {
        const { sftp } = await ClientManager.getSSH(sshConfig)
        sftp.fastPut(tempPath, remotePath, async (err) => {
            if (err) {
                vscode.window.showErrorMessage(err.message)
            } else {
                vscode.commands.executeCommand(Command.REFRESH)
                vscode.window.showInformationMessage("Update to remote success!")
            }
        })
    }

    refresh() {
        this._onDidChangeTreeData.fire();
    }

    async save(parentNode?: ParentNode) {

        ViewManager.createWebviewPanel({
            iconPath: Util.getExtPath("resources", "image", "icon", "add.svg"),
            path: "connect", title: "Add SSH Config", splitView: false,
            eventHandler: (handler) => {
                handler.on("init", () => {
                    if(parentNode){
                        if(!parentNode.sshConfig.algorithms){
                            parentNode.sshConfig.algorithms={cipher:[]}
                        }
                        handler.emit("edit",parentNode.sshConfig)
                    }
                }).on("CONNECT_TO_SQL_SERVER", (content) => {
                    const sshConfig: SSHConfig = content.connectionOption
                    let msg = null;
                    if (!sshConfig.username) {
                        msg = "You must input username!"
                    }
                    if (!sshConfig.password && !sshConfig.private) {
                        msg = "You must input password!"
                    }
                    if (!sshConfig.host) {
                        msg = "You must input host!"
                    }
                    if (!sshConfig.port) {
                        msg = "You must input port!"
                    }
                    if (msg) {
                        handler.emit('CONNECTION_ERROR', msg)
                        return;
                    }

                    ClientManager.getSSH(sshConfig,false).then(() => {
                        const id = `${sshConfig.username}@${sshConfig.host}:${sshConfig.port}`;
                        const configs = this.getConnections();
                        configs[id] = sshConfig;
                        this.context.globalState.update(CacheKey.CONECTIONS_CONFIG, configs);
                        handler.panel.dispose()
                        this.refresh();
                    }).catch(err => {
                        handler.emit('CONNECTION_ERROR', err.message)
                    })

                })
            }
        })

    }

    delete(element: ParentNode) {
        Util.confirm(`Are you want remove connection ${element.sshConfig.username}@${element.sshConfig.host}?`, () => {
            const configs = this.getConnections();
            delete configs[element.id];
            this.context.globalState.update(CacheKey.CONECTIONS_CONFIG, configs);
            this.refresh();
        })
    }

    private getConnections(): { [key: string]: SSHConfig } {
        return this.context.globalState.get<{ [key: string]: SSHConfig }>(CacheKey.CONECTIONS_CONFIG) || {};
    }

}