Path: blob/main/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';6import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';7import { TreeFindMode } from '../../../../base/browser/ui/tree/abstractTree.js';8import type { ICompressedTreeNode } from '../../../../base/browser/ui/tree/compressedObjectTreeModel.js';9import type { ICompressibleTreeRenderer } from '../../../../base/browser/ui/tree/objectTree.js';10import { ITreeElement, ITreeFilter, ITreeNode, TreeFilterResult, TreeVisibility } from '../../../../base/browser/ui/tree/tree.js';11import { RunOnceScheduler } from '../../../../base/common/async.js';12import { Codicon } from '../../../../base/common/codicons.js';13import { createMatches, FuzzyScore } from '../../../../base/common/filters.js';14import { normalizeDriveLetter, tildify } from '../../../../base/common/labels.js';15import { dispose } from '../../../../base/common/lifecycle.js';16import { isAbsolute, normalize, posix } from '../../../../base/common/path.js';17import { isWindows } from '../../../../base/common/platform.js';18import { ltrim } from '../../../../base/common/strings.js';19import { URI } from '../../../../base/common/uri.js';20import * as nls from '../../../../nls.js';21import { MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';22import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';23import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';24import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';25import { FileKind } from '../../../../platform/files/common/files.js';26import { IHoverService } from '../../../../platform/hover/browser/hover.js';27import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';28import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';29import { ILabelService } from '../../../../platform/label/common/label.js';30import { WorkbenchCompressibleObjectTree } from '../../../../platform/list/browser/listService.js';31import { IOpenerService } from '../../../../platform/opener/common/opener.js';32import { IFileIconTheme, IThemeService } from '../../../../platform/theme/common/themeService.js';33import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';34import { IResourceLabel, IResourceLabelOptions, IResourceLabelProps, ResourceLabels } from '../../../browser/labels.js';35import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js';36import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js';37import { IViewDescriptorService } from '../../../common/views.js';38import { IEditorService } from '../../../services/editor/common/editorService.js';39import { IPathService } from '../../../services/path/common/pathService.js';40import { CONTEXT_LOADED_SCRIPTS_ITEM_TYPE, IDebugService, IDebugSession, LOADED_SCRIPTS_VIEW_ID } from '../common/debug.js';41import { DebugContentProvider } from '../common/debugContentProvider.js';42import { Source } from '../common/debugSource.js';43import { renderViewTree } from './baseDebugView.js';4445const NEW_STYLE_COMPRESS = true;4647// RFC 2396, Appendix A: https://www.ietf.org/rfc/rfc2396.txt48const URI_SCHEMA_PATTERN = /^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/;4950type LoadedScriptsItem = BaseTreeItem;5152class BaseTreeItem {5354private _showedMoreThanOne: boolean;55private _children = new Map<string, BaseTreeItem>();56private _source: Source | undefined;5758constructor(private _parent: BaseTreeItem | undefined, private _label: string, public readonly isIncompressible = false) {59this._showedMoreThanOne = false;60}6162updateLabel(label: string) {63this._label = label;64}6566isLeaf(): boolean {67return this._children.size === 0;68}6970getSession(): IDebugSession | undefined {71if (this._parent) {72return this._parent.getSession();73}74return undefined;75}7677setSource(session: IDebugSession, source: Source): void {78this._source = source;79this._children.clear();80if (source.raw && source.raw.sources) {81for (const src of source.raw.sources) {82if (src.name && src.path) {83const s = new BaseTreeItem(this, src.name);84this._children.set(src.path, s);85const ss = session.getSource(src);86s.setSource(session, ss);87}88}89}90}9192createIfNeeded<T extends BaseTreeItem>(key: string, factory: (parent: BaseTreeItem, label: string) => T): T {93let child = <T>this._children.get(key);94if (!child) {95child = factory(this, key);96this._children.set(key, child);97}98return child;99}100101getChild(key: string): BaseTreeItem | undefined {102return this._children.get(key);103}104105remove(key: string): void {106this._children.delete(key);107}108109removeFromParent(): void {110if (this._parent) {111this._parent.remove(this._label);112if (this._parent._children.size === 0) {113this._parent.removeFromParent();114}115}116}117118getTemplateId(): string {119return 'id';120}121122// a dynamic ID based on the parent chain; required for reparenting (see #55448)123getId(): string {124const parent = this.getParent();125return parent ? `${parent.getId()}/${this.getInternalId()}` : this.getInternalId();126}127128getInternalId(): string {129return this._label;130}131132// skips intermediate single-child nodes133getParent(): BaseTreeItem | undefined {134if (this._parent) {135if (this._parent.isSkipped()) {136return this._parent.getParent();137}138return this._parent;139}140return undefined;141}142143isSkipped(): boolean {144if (this._parent) {145if (this._parent.oneChild()) {146return true; // skipped if I'm the only child of my parents147}148return false;149}150return true; // roots are never skipped151}152153// skips intermediate single-child nodes154hasChildren(): boolean {155const child = this.oneChild();156if (child) {157return child.hasChildren();158}159return this._children.size > 0;160}161162// skips intermediate single-child nodes163getChildren(): BaseTreeItem[] {164const child = this.oneChild();165if (child) {166return child.getChildren();167}168const array: BaseTreeItem[] = [];169for (const child of this._children.values()) {170array.push(child);171}172return array.sort((a, b) => this.compare(a, b));173}174175// skips intermediate single-child nodes176getLabel(separateRootFolder = true): string {177const child = this.oneChild();178if (child) {179const sep = (this instanceof RootFolderTreeItem && separateRootFolder) ? ' • ' : posix.sep;180return `${this._label}${sep}${child.getLabel()}`;181}182return this._label;183}184185// skips intermediate single-child nodes186getHoverLabel(): string | undefined {187if (this._source && this._parent && this._parent._source) {188return this._source.raw.path || this._source.raw.name;189}190const label = this.getLabel(false);191const parent = this.getParent();192if (parent) {193const hover = parent.getHoverLabel();194if (hover) {195return `${hover}/${label}`;196}197}198return label;199}200201// skips intermediate single-child nodes202getSource(): Source | undefined {203const child = this.oneChild();204if (child) {205return child.getSource();206}207return this._source;208}209210protected compare(a: BaseTreeItem, b: BaseTreeItem): number {211if (a._label && b._label) {212return a._label.localeCompare(b._label);213}214return 0;215}216217private oneChild(): BaseTreeItem | undefined {218if (!this._source && !this._showedMoreThanOne && this.skipOneChild()) {219if (this._children.size === 1) {220return this._children.values().next().value;221}222// if a node had more than one child once, it will never be skipped again223if (this._children.size > 1) {224this._showedMoreThanOne = true;225}226}227return undefined;228}229230private skipOneChild(): boolean {231if (NEW_STYLE_COMPRESS) {232// if the root node has only one Session, don't show the session233return this instanceof RootTreeItem;234} else {235return !(this instanceof RootFolderTreeItem) && !(this instanceof SessionTreeItem);236}237}238}239240class RootFolderTreeItem extends BaseTreeItem {241242constructor(parent: BaseTreeItem, public folder: IWorkspaceFolder) {243super(parent, folder.name, true);244}245}246247class RootTreeItem extends BaseTreeItem {248249constructor(private _pathService: IPathService, private _contextService: IWorkspaceContextService, private _labelService: ILabelService) {250super(undefined, 'Root');251}252253add(session: IDebugSession): SessionTreeItem {254return this.createIfNeeded(session.getId(), () => new SessionTreeItem(this._labelService, this, session, this._pathService, this._contextService));255}256257find(session: IDebugSession): SessionTreeItem {258return <SessionTreeItem>this.getChild(session.getId());259}260}261262class SessionTreeItem extends BaseTreeItem {263264private static readonly URL_REGEXP = /^(https?:\/\/[^/]+)(\/.*)$/;265266private _session: IDebugSession;267private _map = new Map<string, BaseTreeItem>();268private _labelService: ILabelService;269270constructor(labelService: ILabelService, parent: BaseTreeItem, session: IDebugSession, private _pathService: IPathService, private rootProvider: IWorkspaceContextService) {271super(parent, session.getLabel(), true);272this._labelService = labelService;273this._session = session;274}275276override getInternalId(): string {277return this._session.getId();278}279280override getSession(): IDebugSession {281return this._session;282}283284override getHoverLabel(): string | undefined {285return undefined;286}287288override hasChildren(): boolean {289return true;290}291292protected override compare(a: BaseTreeItem, b: BaseTreeItem): number {293const acat = this.category(a);294const bcat = this.category(b);295if (acat !== bcat) {296return acat - bcat;297}298return super.compare(a, b);299}300301private category(item: BaseTreeItem): number {302303// workspace scripts come at the beginning in "folder" order304if (item instanceof RootFolderTreeItem) {305return item.folder.index;306}307308// <...> come at the very end309const l = item.getLabel();310if (l && /^<.+>$/.test(l)) {311return 1000;312}313314// everything else in between315return 999;316}317318async addPath(source: Source): Promise<void> {319320let folder: IWorkspaceFolder | null;321let url: string;322323let path = source.raw.path;324if (!path) {325return;326}327328if (this._labelService && URI_SCHEMA_PATTERN.test(path)) {329path = this._labelService.getUriLabel(URI.parse(path));330}331332const match = SessionTreeItem.URL_REGEXP.exec(path);333if (match && match.length === 3) {334url = match[1];335path = decodeURI(match[2]);336} else {337if (isAbsolute(path)) {338const resource = URI.file(path);339340// return early if we can resolve a relative path label from the root folder341folder = this.rootProvider ? this.rootProvider.getWorkspaceFolder(resource) : null;342if (folder) {343// strip off the root folder path344path = normalize(ltrim(resource.path.substring(folder.uri.path.length), posix.sep));345const hasMultipleRoots = this.rootProvider.getWorkspace().folders.length > 1;346if (hasMultipleRoots) {347path = posix.sep + path;348} else {349// don't show root folder350folder = null;351}352} else {353// on unix try to tildify absolute paths354path = normalize(path);355if (isWindows) {356path = normalizeDriveLetter(path);357} else {358path = tildify(path, (await this._pathService.userHome()).fsPath);359}360}361}362}363364let leaf: BaseTreeItem = this;365path.split(/[\/\\]/).forEach((segment, i) => {366if (i === 0 && folder) {367const f = folder;368leaf = leaf.createIfNeeded(folder.name, parent => new RootFolderTreeItem(parent, f));369} else if (i === 0 && url) {370leaf = leaf.createIfNeeded(url, parent => new BaseTreeItem(parent, url));371} else {372leaf = leaf.createIfNeeded(segment, parent => new BaseTreeItem(parent, segment));373}374});375376leaf.setSource(this._session, source);377if (source.raw.path) {378this._map.set(source.raw.path, leaf);379}380}381382removePath(source: Source): boolean {383if (source.raw.path) {384const leaf = this._map.get(source.raw.path);385if (leaf) {386leaf.removeFromParent();387return true;388}389}390return false;391}392}393394interface IViewState {395readonly expanded: Set<string>;396}397398/**399* This maps a model item into a view model item.400*/401function asTreeElement(item: BaseTreeItem, viewState?: IViewState): ITreeElement<LoadedScriptsItem> {402const children = item.getChildren();403const collapsed = viewState ? !viewState.expanded.has(item.getId()) : !(item instanceof SessionTreeItem);404405return {406element: item,407collapsed,408collapsible: item.hasChildren(),409children: children.map(i => asTreeElement(i, viewState))410};411}412413export class LoadedScriptsView extends ViewPane {414415private treeContainer!: HTMLElement;416private loadedScriptsItemType: IContextKey<string>;417private tree!: WorkbenchCompressibleObjectTree<LoadedScriptsItem, FuzzyScore>;418private treeLabels!: ResourceLabels;419private changeScheduler!: RunOnceScheduler;420private treeNeedsRefreshOnVisible = false;421private filter!: LoadedScriptsFilter;422423constructor(424options: IViewletViewOptions,425@IContextMenuService contextMenuService: IContextMenuService,426@IKeybindingService keybindingService: IKeybindingService,427@IInstantiationService instantiationService: IInstantiationService,428@IViewDescriptorService viewDescriptorService: IViewDescriptorService,429@IConfigurationService configurationService: IConfigurationService,430@IEditorService private readonly editorService: IEditorService,431@IContextKeyService contextKeyService: IContextKeyService,432@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,433@IDebugService private readonly debugService: IDebugService,434@ILabelService private readonly labelService: ILabelService,435@IPathService private readonly pathService: IPathService,436@IOpenerService openerService: IOpenerService,437@IThemeService themeService: IThemeService,438@IHoverService hoverService: IHoverService,439) {440super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);441this.loadedScriptsItemType = CONTEXT_LOADED_SCRIPTS_ITEM_TYPE.bindTo(contextKeyService);442}443444protected override renderBody(container: HTMLElement): void {445super.renderBody(container);446447this.element.classList.add('debug-pane');448container.classList.add('debug-loaded-scripts', 'show-file-icons');449450this.treeContainer = renderViewTree(container);451452this.filter = new LoadedScriptsFilter();453454const root = new RootTreeItem(this.pathService, this.contextService, this.labelService);455456this.treeLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility });457this._register(this.treeLabels);458459const onFileIconThemeChange = (fileIconTheme: IFileIconTheme) => {460this.treeContainer.classList.toggle('align-icons-and-twisties', fileIconTheme.hasFileIcons && !fileIconTheme.hasFolderIcons);461this.treeContainer.classList.toggle('hide-arrows', fileIconTheme.hidesExplorerArrows === true);462};463464this._register(this.themeService.onDidFileIconThemeChange(onFileIconThemeChange));465onFileIconThemeChange(this.themeService.getFileIconTheme());466467this.tree = this.instantiationService.createInstance(WorkbenchCompressibleObjectTree<LoadedScriptsItem, FuzzyScore>,468'LoadedScriptsView',469this.treeContainer,470new LoadedScriptsDelegate(),471[new LoadedScriptsRenderer(this.treeLabels)],472{473compressionEnabled: NEW_STYLE_COMPRESS,474collapseByDefault: true,475hideTwistiesOfChildlessElements: true,476identityProvider: {477getId: (element: LoadedScriptsItem) => element.getId()478},479keyboardNavigationLabelProvider: {480getKeyboardNavigationLabel: (element: LoadedScriptsItem) => {481return element.getLabel();482},483getCompressedNodeKeyboardNavigationLabel: (elements: LoadedScriptsItem[]) => {484return elements.map(e => e.getLabel()).join('/');485}486},487filter: this.filter,488accessibilityProvider: new LoadedSciptsAccessibilityProvider(),489overrideStyles: this.getLocationBasedColors().listOverrideStyles490}491);492493const updateView = (viewState?: IViewState) => this.tree.setChildren(null, asTreeElement(root, viewState).children);494495updateView();496497this.changeScheduler = new RunOnceScheduler(() => {498this.treeNeedsRefreshOnVisible = false;499if (this.tree) {500updateView();501}502}, 300);503this._register(this.changeScheduler);504505this._register(this.tree.onDidOpen(e => {506if (e.element instanceof BaseTreeItem) {507const source = e.element.getSource();508if (source && source.available) {509const nullRange = { startLineNumber: 0, startColumn: 0, endLineNumber: 0, endColumn: 0 };510source.openInEditor(this.editorService, nullRange, e.editorOptions.preserveFocus, e.sideBySide, e.editorOptions.pinned);511}512}513}));514515this._register(this.tree.onDidChangeFocus(() => {516const focus = this.tree.getFocus();517if (focus instanceof SessionTreeItem) {518this.loadedScriptsItemType.set('session');519} else {520this.loadedScriptsItemType.reset();521}522}));523524const scheduleRefreshOnVisible = () => {525if (this.isBodyVisible()) {526this.changeScheduler.schedule();527} else {528this.treeNeedsRefreshOnVisible = true;529}530};531532const addSourcePathsToSession = async (session: IDebugSession) => {533if (session.capabilities.supportsLoadedSourcesRequest) {534const sessionNode = root.add(session);535const paths = await session.getLoadedSources();536for (const path of paths) {537await sessionNode.addPath(path);538}539scheduleRefreshOnVisible();540}541};542543const registerSessionListeners = (session: IDebugSession) => {544this._register(session.onDidChangeName(async () => {545const sessionRoot = root.find(session);546if (sessionRoot) {547sessionRoot.updateLabel(session.getLabel());548scheduleRefreshOnVisible();549}550}));551this._register(session.onDidLoadedSource(async event => {552let sessionRoot: SessionTreeItem;553switch (event.reason) {554case 'new':555case 'changed':556sessionRoot = root.add(session);557await sessionRoot.addPath(event.source);558scheduleRefreshOnVisible();559if (event.reason === 'changed') {560DebugContentProvider.refreshDebugContent(event.source.uri);561}562break;563case 'removed':564sessionRoot = root.find(session);565if (sessionRoot && sessionRoot.removePath(event.source)) {566scheduleRefreshOnVisible();567}568break;569default:570this.filter.setFilter(event.source.name);571this.tree.refilter();572break;573}574}));575};576577this._register(this.debugService.onDidNewSession(registerSessionListeners));578this.debugService.getModel().getSessions().forEach(registerSessionListeners);579580this._register(this.debugService.onDidEndSession(({ session }) => {581root.remove(session.getId());582this.changeScheduler.schedule();583}));584585this.changeScheduler.schedule(0);586587this._register(this.onDidChangeBodyVisibility(visible => {588if (visible && this.treeNeedsRefreshOnVisible) {589this.changeScheduler.schedule();590}591}));592593// feature: expand all nodes when filtering (not when finding)594let viewState: IViewState | undefined;595this._register(this.tree.onDidChangeFindPattern(pattern => {596if (this.tree.findMode === TreeFindMode.Highlight) {597return;598}599600if (!viewState && pattern) {601const expanded = new Set<string>();602const visit = (node: ITreeNode<BaseTreeItem | null, FuzzyScore>) => {603if (node.element && !node.collapsed) {604expanded.add(node.element.getId());605}606607for (const child of node.children) {608visit(child);609}610};611612visit(this.tree.getNode());613viewState = { expanded };614this.tree.expandAll();615} else if (!pattern && viewState) {616this.tree.setFocus([]);617updateView(viewState);618viewState = undefined;619}620}));621622// populate tree model with source paths from all debug sessions623this.debugService.getModel().getSessions().forEach(session => addSourcePathsToSession(session));624}625626protected override layoutBody(height: number, width: number): void {627super.layoutBody(height, width);628this.tree.layout(height, width);629}630631collapseAll(): void {632this.tree.collapseAll();633}634635override dispose(): void {636dispose(this.tree);637dispose(this.treeLabels);638super.dispose();639}640}641642class LoadedScriptsDelegate implements IListVirtualDelegate<LoadedScriptsItem> {643644getHeight(element: LoadedScriptsItem): number {645return 22;646}647648getTemplateId(element: LoadedScriptsItem): string {649return LoadedScriptsRenderer.ID;650}651}652653interface ILoadedScriptsItemTemplateData {654label: IResourceLabel;655}656657class LoadedScriptsRenderer implements ICompressibleTreeRenderer<BaseTreeItem, FuzzyScore, ILoadedScriptsItemTemplateData> {658659static readonly ID = 'lsrenderer';660661constructor(662private labels: ResourceLabels663) {664}665666get templateId(): string {667return LoadedScriptsRenderer.ID;668}669670renderTemplate(container: HTMLElement): ILoadedScriptsItemTemplateData {671const label = this.labels.create(container, { supportHighlights: true });672return { label };673}674675renderElement(node: ITreeNode<BaseTreeItem, FuzzyScore>, index: number, data: ILoadedScriptsItemTemplateData): void {676677const element = node.element;678const label = element.getLabel();679680this.render(element, label, data, node.filterData);681}682683renderCompressedElements(node: ITreeNode<ICompressedTreeNode<BaseTreeItem>, FuzzyScore>, index: number, data: ILoadedScriptsItemTemplateData): void {684685const element = node.element.elements[node.element.elements.length - 1];686const labels = node.element.elements.map(e => e.getLabel());687688this.render(element, labels, data, node.filterData);689}690691private render(element: BaseTreeItem, labels: string | string[], data: ILoadedScriptsItemTemplateData, filterData: FuzzyScore | undefined) {692693const label: IResourceLabelProps = {694name: labels695};696const options: IResourceLabelOptions = {697title: element.getHoverLabel()698};699700if (element instanceof RootFolderTreeItem) {701702options.fileKind = FileKind.ROOT_FOLDER;703704} else if (element instanceof SessionTreeItem) {705706options.title = nls.localize('loadedScriptsSession', "Debug Session");707options.hideIcon = true;708709} else if (element instanceof BaseTreeItem) {710711const src = element.getSource();712if (src && src.uri) {713label.resource = src.uri;714options.fileKind = FileKind.FILE;715} else {716options.fileKind = FileKind.FOLDER;717}718}719options.matches = createMatches(filterData);720721data.label.setResource(label, options);722}723724disposeTemplate(templateData: ILoadedScriptsItemTemplateData): void {725templateData.label.dispose();726}727}728729class LoadedSciptsAccessibilityProvider implements IListAccessibilityProvider<LoadedScriptsItem> {730731getWidgetAriaLabel(): string {732return nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'loadedScriptsAriaLabel' }, "Debug Loaded Scripts");733}734735getAriaLabel(element: LoadedScriptsItem): string {736737if (element instanceof RootFolderTreeItem) {738return nls.localize('loadedScriptsRootFolderAriaLabel', "Workspace folder {0}, loaded script, debug", element.getLabel());739}740741if (element instanceof SessionTreeItem) {742return nls.localize('loadedScriptsSessionAriaLabel', "Session {0}, loaded script, debug", element.getLabel());743}744745if (element.hasChildren()) {746return nls.localize('loadedScriptsFolderAriaLabel', "Folder {0}, loaded script, debug", element.getLabel());747} else {748return nls.localize('loadedScriptsSourceAriaLabel', "{0}, loaded script, debug", element.getLabel());749}750}751}752753class LoadedScriptsFilter implements ITreeFilter<BaseTreeItem, FuzzyScore> {754755private filterText: string | undefined;756757setFilter(filterText: string) {758this.filterText = filterText;759}760761filter(element: BaseTreeItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {762763if (!this.filterText) {764return TreeVisibility.Visible;765}766767if (element.isLeaf()) {768const name = element.getLabel();769if (name.indexOf(this.filterText) >= 0) {770return TreeVisibility.Visible;771}772return TreeVisibility.Hidden;773}774return TreeVisibility.Recurse;775}776}777registerAction2(class Collapse extends ViewAction<LoadedScriptsView> {778constructor() {779super({780id: 'loadedScripts.collapse',781viewId: LOADED_SCRIPTS_VIEW_ID,782title: nls.localize('collapse', "Collapse All"),783f1: false,784icon: Codicon.collapseAll,785menu: {786id: MenuId.ViewTitle,787order: 30,788group: 'navigation',789when: ContextKeyExpr.equals('view', LOADED_SCRIPTS_VIEW_ID)790}791});792}793794runInView(_accessor: ServicesAccessor, view: LoadedScriptsView) {795view.collapseAll();796}797});798799800