Path: blob/main/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts
5241 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, DisposableMap, DisposableStore } 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};542543// Track listeners per session to avoid leaking disposables544const sessionListeners = this._register(new DisposableMap<string, DisposableStore>());545546const registerSessionListeners = (session: IDebugSession) => {547const store = new DisposableStore();548sessionListeners.set(session.getId(), store);549550store.add(session.onDidChangeName(async () => {551const sessionRoot = root.find(session);552if (sessionRoot) {553sessionRoot.updateLabel(session.getLabel());554scheduleRefreshOnVisible();555}556}));557store.add(session.onDidLoadedSource(async event => {558let sessionRoot: SessionTreeItem;559switch (event.reason) {560case 'new':561case 'changed':562sessionRoot = root.add(session);563await sessionRoot.addPath(event.source);564scheduleRefreshOnVisible();565if (event.reason === 'changed') {566DebugContentProvider.refreshDebugContent(event.source.uri);567}568break;569case 'removed':570sessionRoot = root.find(session);571if (sessionRoot && sessionRoot.removePath(event.source)) {572scheduleRefreshOnVisible();573}574break;575default:576this.filter.setFilter(event.source.name);577this.tree.refilter();578break;579}580}));581};582583this._register(this.debugService.onDidNewSession(registerSessionListeners));584this.debugService.getModel().getSessions().forEach(registerSessionListeners);585586this._register(this.debugService.onDidEndSession(({ session }) => {587sessionListeners.deleteAndDispose(session.getId());588root.remove(session.getId());589this.changeScheduler.schedule();590}));591592this.changeScheduler.schedule(0);593594this._register(this.onDidChangeBodyVisibility(visible => {595if (visible && this.treeNeedsRefreshOnVisible) {596this.changeScheduler.schedule();597}598}));599600// feature: expand all nodes when filtering (not when finding)601let viewState: IViewState | undefined;602this._register(this.tree.onDidChangeFindPattern(pattern => {603if (this.tree.findMode === TreeFindMode.Highlight) {604return;605}606607if (!viewState && pattern) {608const expanded = new Set<string>();609const visit = (node: ITreeNode<BaseTreeItem | null, FuzzyScore>) => {610if (node.element && !node.collapsed) {611expanded.add(node.element.getId());612}613614for (const child of node.children) {615visit(child);616}617};618619visit(this.tree.getNode());620viewState = { expanded };621this.tree.expandAll();622} else if (!pattern && viewState) {623this.tree.setFocus([]);624updateView(viewState);625viewState = undefined;626}627}));628629// populate tree model with source paths from all debug sessions630this.debugService.getModel().getSessions().forEach(session => addSourcePathsToSession(session));631}632633protected override layoutBody(height: number, width: number): void {634super.layoutBody(height, width);635this.tree.layout(height, width);636}637638collapseAll(): void {639this.tree.collapseAll();640}641642override dispose(): void {643dispose(this.tree);644dispose(this.treeLabels);645super.dispose();646}647}648649class LoadedScriptsDelegate implements IListVirtualDelegate<LoadedScriptsItem> {650651getHeight(element: LoadedScriptsItem): number {652return 22;653}654655getTemplateId(element: LoadedScriptsItem): string {656return LoadedScriptsRenderer.ID;657}658}659660interface ILoadedScriptsItemTemplateData {661label: IResourceLabel;662}663664class LoadedScriptsRenderer implements ICompressibleTreeRenderer<BaseTreeItem, FuzzyScore, ILoadedScriptsItemTemplateData> {665666static readonly ID = 'lsrenderer';667668constructor(669private labels: ResourceLabels670) {671}672673get templateId(): string {674return LoadedScriptsRenderer.ID;675}676677renderTemplate(container: HTMLElement): ILoadedScriptsItemTemplateData {678const label = this.labels.create(container, { supportHighlights: true });679return { label };680}681682renderElement(node: ITreeNode<BaseTreeItem, FuzzyScore>, index: number, data: ILoadedScriptsItemTemplateData): void {683684const element = node.element;685const label = element.getLabel();686687this.render(element, label, data, node.filterData);688}689690renderCompressedElements(node: ITreeNode<ICompressedTreeNode<BaseTreeItem>, FuzzyScore>, index: number, data: ILoadedScriptsItemTemplateData): void {691692const element = node.element.elements[node.element.elements.length - 1];693const labels = node.element.elements.map(e => e.getLabel());694695this.render(element, labels, data, node.filterData);696}697698private render(element: BaseTreeItem, labels: string | string[], data: ILoadedScriptsItemTemplateData, filterData: FuzzyScore | undefined) {699700const label: IResourceLabelProps = {701name: labels702};703const options: IResourceLabelOptions = {704title: element.getHoverLabel()705};706707if (element instanceof RootFolderTreeItem) {708709options.fileKind = FileKind.ROOT_FOLDER;710711} else if (element instanceof SessionTreeItem) {712713options.title = nls.localize('loadedScriptsSession', "Debug Session");714options.hideIcon = true;715716} else if (element instanceof BaseTreeItem) {717718const src = element.getSource();719if (src && src.uri) {720label.resource = src.uri;721options.fileKind = FileKind.FILE;722} else {723options.fileKind = FileKind.FOLDER;724}725}726options.matches = createMatches(filterData);727728data.label.setResource(label, options);729}730731disposeTemplate(templateData: ILoadedScriptsItemTemplateData): void {732templateData.label.dispose();733}734}735736class LoadedSciptsAccessibilityProvider implements IListAccessibilityProvider<LoadedScriptsItem> {737738getWidgetAriaLabel(): string {739return nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'loadedScriptsAriaLabel' }, "Debug Loaded Scripts");740}741742getAriaLabel(element: LoadedScriptsItem): string {743744if (element instanceof RootFolderTreeItem) {745return nls.localize('loadedScriptsRootFolderAriaLabel', "Workspace folder {0}, loaded script, debug", element.getLabel());746}747748if (element instanceof SessionTreeItem) {749return nls.localize('loadedScriptsSessionAriaLabel', "Session {0}, loaded script, debug", element.getLabel());750}751752if (element.hasChildren()) {753return nls.localize('loadedScriptsFolderAriaLabel', "Folder {0}, loaded script, debug", element.getLabel());754} else {755return nls.localize('loadedScriptsSourceAriaLabel', "{0}, loaded script, debug", element.getLabel());756}757}758}759760class LoadedScriptsFilter implements ITreeFilter<BaseTreeItem, FuzzyScore> {761762private filterText: string | undefined;763764setFilter(filterText: string) {765this.filterText = filterText;766}767768filter(element: BaseTreeItem, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {769770if (!this.filterText) {771return TreeVisibility.Visible;772}773774if (element.isLeaf()) {775const name = element.getLabel();776if (name.indexOf(this.filterText) >= 0) {777return TreeVisibility.Visible;778}779return TreeVisibility.Hidden;780}781return TreeVisibility.Recurse;782}783}784registerAction2(class Collapse extends ViewAction<LoadedScriptsView> {785constructor() {786super({787id: 'loadedScripts.collapse',788viewId: LOADED_SCRIPTS_VIEW_ID,789title: nls.localize('collapse', "Collapse All"),790f1: false,791icon: Codicon.collapseAll,792menu: {793id: MenuId.ViewTitle,794order: 30,795group: 'navigation',796when: ContextKeyExpr.equals('view', LOADED_SCRIPTS_VIEW_ID)797}798});799}800801runInView(_accessor: ServicesAccessor, view: LoadedScriptsView) {802view.collapseAll();803}804});805806807