builtin loaded scripts view; fixes #37767

This commit is contained in:
Andre Weinand
2018-07-24 23:15:36 +02:00
committed by Andre Weinand
parent 11567e39f5
commit e5c3c0f37e
7 changed files with 463 additions and 20 deletions
@@ -7,18 +7,293 @@ import * as nls from 'vs/nls';
import { TreeViewsViewletPanel, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet';
import { TPromise } from 'vs/base/common/winjs.base';
import * as dom from 'vs/base/browser/dom';
import * as errors from 'vs/base/common/errors';
import { normalize, isAbsolute, sep } from 'vs/base/common/paths';
import { IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { WorkbenchTree } from 'vs/platform/list/browser/listService';
import { WorkbenchTree, TreeResourceNavigator } from 'vs/platform/list/browser/listService';
import { renderViewTree, twistiePixels } from 'vs/workbench/parts/debug/browser/baseDebugView';
import { IAccessibilityProvider, ITree, IRenderer, IDataSource } from 'vs/base/parts/tree/browser/tree';
import { ISession, IDebugService, IModel, CONTEXT_LOADED_SCRIPTS_ITEM_TYPE } from 'vs/workbench/parts/debug/common/debug';
import { Source } from 'vs/workbench/parts/debug/common/debugSource';
import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { tildify } from 'vs/base/common/labels';
import { isWindows } from 'vs/base/common/platform';
import URI from 'vs/base/common/uri';
import { ltrim } from 'vs/base/common/strings';
const SMART = true;
const $ = dom.$;
const SESSION_TEMPLATE_ID = 'session';
const SOURCE_TEMPLATE_ID = 'source';
const ROOT_FOLDER_TEMPLATE_ID = 'node';
class BaseTreeItem {
private _id: string;
private _children: { [key: string]: BaseTreeItem; };
private _source: Source;
constructor(private _parent: BaseTreeItem, private _label: string) {
this._id = this._parent ? `${this._parent._id}/${this._label}` : this._label;
this._children = {};
}
getLabel() {
const child = this.oneChild();
if (child) {
const sep = this instanceof RootFolderTreeItem ? ' • ' : '/';
return `${this._label}${sep}${child.getLabel()}`;
}
return this._label;
}
getId(): string {
return this._id;
}
getTemplateId(): string {
return SOURCE_TEMPLATE_ID;
}
getChildren(): TPromise<BaseTreeItem[]> {
const child = this.oneChild();
if (child) {
return child.getChildren();
}
const array = Object.keys(this._children).map(key => this._children[key]);
return TPromise.as(array.sort((a, b) => this.compare(a, b)));
}
hasChildren(): boolean {
const child = this.oneChild();
if (child) {
return child.hasChildren();
}
return Object.keys(this._children).length > 0;
}
getSource() {
const child = this.oneChild();
if (child) {
return child.getSource();
}
return this._source;
}
setSource(session: ISession, source: Source): void {
this._source = source;
}
createIfNeeded<T extends BaseTreeItem>(key: string, factory: (parent: BaseTreeItem, label: string) => T): T {
let child = <T>this._children[key];
if (!child) {
child = factory(this, key);
this._children[key] = child;
}
return child;
}
remove(key: string): void {
delete this._children[key];
}
protected compare(a: BaseTreeItem, b: BaseTreeItem): number {
if (a._label && b._label) {
return a._label.localeCompare(b._label);
}
return 0;
}
private oneChild(): BaseTreeItem {
if (SMART && !(this instanceof RootTreeItem)) {
const keys = Object.keys(this._children);
if (keys.length === 1) {
return this._children[keys[0]];
}
}
return undefined;
}
}
class RootFolderTreeItem extends BaseTreeItem {
constructor(parent: BaseTreeItem, public folder: IWorkspaceFolder) {
super(parent, folder.name);
}
getTemplateId(): string {
return ROOT_FOLDER_TEMPLATE_ID;
}
}
class RootTreeItem extends BaseTreeItem {
private _showedMoreThanOne: boolean;
constructor(private _debugModel: IModel, private _environmentService: IEnvironmentService, private _contextService: IWorkspaceContextService) {
super(undefined, 'Root');
this._showedMoreThanOne = false;
this._debugModel.getSessions().forEach(session => {
this.add(session);
});
}
hasChildren(): boolean {
return true;
}
getChildren(): TPromise<BaseTreeItem[]> {
return super.getChildren().then(children => {
const size = children.length;
if (!this._showedMoreThanOne && size === 1) {
// skip session if there is only one
return children[0].getChildren();
}
this._showedMoreThanOne = size > 1;
return children;
});
}
add(session: ISession): SessionTreeItem {
return this.createIfNeeded(session.getId(), () => new SessionTreeItem(this, session, this._environmentService, this._contextService));
}
}
class SessionTreeItem extends BaseTreeItem {
private static URL_REGEXP = /^(https?:\/\/[^/]+)(\/.*)$/;
private _session: ISession;
private _initialized: boolean;
constructor(parent: BaseTreeItem, session: ISession, private _environmentService: IEnvironmentService, private rootProvider: IWorkspaceContextService) {
super(parent, session.getName(true));
this._initialized = false;
this._session = session;
}
getTemplateId(): string {
return SESSION_TEMPLATE_ID;
}
hasChildren(): boolean {
return true;
}
getChildren(): TPromise<BaseTreeItem[]> {
if (!this._initialized) {
this._initialized = true;
return this._session.getLoadedSources().then(paths => {
paths.forEach(path => this.addPath(path));
return super.getChildren();
});
}
return super.getChildren();
}
protected compare(a: BaseTreeItem, b: BaseTreeItem): number {
const acat = this.category(a);
const bcat = this.category(b);
if (acat !== bcat) {
return acat - bcat;
}
return super.compare(a, b);
}
/**
* Return an ordinal number for folders
*/
private category(item: BaseTreeItem): number {
// workspace scripts come at the beginning in "folder" order
if (item instanceof RootFolderTreeItem) {
return item.folder.index;
}
// <...> come at the very end
const l = item.getLabel();
if (l && /^<.+>$/.test(l)) {
return 1000;
}
// everything else in between
return 999;
}
addPath(source: Source): void {
let folder: IWorkspaceFolder;
let url: string;
let path = source.raw.path;
const match = SessionTreeItem.URL_REGEXP.exec(path);
if (match && match.length === 3) {
url = match[1];
path = decodeURI(match[2]);
} else {
if (isAbsolute(path)) {
const resource = URI.file(path);
// return early if we can resolve a relative path label from the root folder
folder = this.rootProvider ? this.rootProvider.getWorkspaceFolder(resource) : null;
if (folder) {
// strip off the root folder path
path = normalize(ltrim(resource.path.substr(folder.uri.path.length), sep), true);
const hasMultipleRoots = this.rootProvider.getWorkspace().folders.length > 1;
if (hasMultipleRoots) {
path = '/' + path;
} else {
// don't show root folder
folder = undefined;
}
} else {
// on unix try to tildify absolute paths
path = normalize(path, true);
if (!isWindows) {
path = tildify(path, this._environmentService.userHome);
}
}
}
}
let x: BaseTreeItem = this;
path.split(/[\/\\]/).forEach((segment, i) => {
if (segment.length === 0) { // macOS or unix path
segment = '/';
}
if (i === 0 && folder) {
x = x.createIfNeeded(folder.name, parent => new RootFolderTreeItem(parent, folder));
} else if (i === 0 && url) {
x = x.createIfNeeded(url, parent => new BaseTreeItem(parent, url));
} else {
x = x.createIfNeeded(segment, parent => new BaseTreeItem(parent, segment));
}
});
x.setSource(this._session, source);
}
}
export class LoadedScriptsView extends TreeViewsViewletPanel {
private static readonly MEMENTO = 'loadedscriptsview.memento';
private treeContainer: HTMLElement;
private loadedScriptsItemType: IContextKey<string>;
private settings: any;
constructor(
options: IViewletViewOptions,
@@ -26,22 +301,77 @@ export class LoadedScriptsView extends TreeViewsViewletPanel {
@IKeybindingService keybindingService: IKeybindingService,
@IInstantiationService private instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@IEditorService private editorService: IEditorService,
@IContextKeyService contextKeyService: IContextKeyService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IDebugService private debugService: IDebugService
) {
super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService);
this.settings = options.viewletSettings;
this.loadedScriptsItemType = CONTEXT_LOADED_SCRIPTS_ITEM_TYPE.bindTo(contextKeyService);
}
protected renderBody(container: HTMLElement): void {
dom.addClass(container, 'debug-loaded-scripts');
this.treeContainer = renderViewTree(container);
this.tree = this.instantiationService.createInstance(WorkbenchTree, this.treeContainer, {
dataSource: new LoadedScriptsDataSource(),
renderer: this.instantiationService.createInstance(LoadedScriptsRenderer),
accessibilityProvider: new LoadedSciptsAccessibilityProvider(),
}, {
this.tree = this.instantiationService.createInstance(WorkbenchTree, this.treeContainer,
{
dataSource: new LoadedScriptsDataSource(),
renderer: this.instantiationService.createInstance(LoadedScriptsRenderer),
accessibilityProvider: new LoadedSciptsAccessibilityProvider(),
},
{
ariaLabel: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'loadedScriptsAriaLabel' }, "Debug Loaded Scripts"),
twistiePixels
});
}
);
const callstackNavigator = new TreeResourceNavigator(this.tree);
this.disposables.push(callstackNavigator);
this.disposables.push(callstackNavigator.openResource(e => {
const element = e.element;
if (element instanceof BaseTreeItem) {
const source = element.getSource();
if (source && source.available) {
const nullRange = { startLineNumber: 0, startColumn: 0, endLineNumber: 0, endColumn: 0 };
source.openInEditor(this.editorService, nullRange, e.editorOptions.preserveFocus, e.sideBySide, e.editorOptions.pinned).done(undefined, errors.onUnexpectedError);
}
}
}));
this.disposables.push(this.tree.onDidChangeFocus(() => {
const focus = this.tree.getFocus();
if (focus instanceof SessionTreeItem) {
this.loadedScriptsItemType.set('session');
} else {
this.loadedScriptsItemType.reset();
}
}));
const root = new RootTreeItem(this.debugService.getModel(), this.environmentService, this.contextService);
this.tree.setInput(root);
let timeout: number;
this.disposables.push(this.debugService.onDidLoadedSource(event => {
const sessionRoot = root.add(event.session);
sessionRoot.addPath(event.source);
clearTimeout(timeout);
timeout = setTimeout(() => {
this.tree.refresh(root, true);
}, 300);
}));
this.disposables.push(this.debugService.onDidEndSession(session => {
root.remove(session.getId());
this.tree.refresh(root, false);
}));
}
layoutBody(size: number): void {
@@ -50,6 +380,11 @@ export class LoadedScriptsView extends TreeViewsViewletPanel {
}
super.layoutBody(size);
}
public shutdown(): void {
this.settings[LoadedScriptsView.MEMENTO] = !this.isExpanded();
super.shutdown();
}
}
// A good example of data source, renderers, action providers and accessibilty providers can be found in the callStackView.ts
@@ -57,42 +392,94 @@ export class LoadedScriptsView extends TreeViewsViewletPanel {
class LoadedScriptsDataSource implements IDataSource {
getId(tree: ITree, element: any): string {
throw new Error('Method not implemented.');
return element.getId();
}
hasChildren(tree: ITree, element: any): boolean {
throw new Error('Method not implemented.');
return element.hasChildren();
}
getChildren(tree: ITree, element: any): TPromise<any> {
throw new Error('Method not implemented.');
return element.getChildren();
}
getParent(tree: ITree, element: any): TPromise<any> {
throw new Error('Method not implemented.');
return TPromise.as(null);
}
shouldAutoexpand?(tree: ITree, element: any): boolean {
return element instanceof RootTreeItem || element instanceof SessionTreeItem;
}
}
interface ISessionTemplateData {
session: HTMLElement;
}
interface ISourceTemplateData {
source: HTMLElement;
}
interface INodeTemplateData {
node: HTMLElement;
}
class LoadedScriptsRenderer implements IRenderer {
getHeight(tree: ITree, element: any): number {
throw new Error('Method not implemented.');
return 22;
}
getTemplateId(tree: ITree, element: any): string {
throw new Error('Method not implemented.');
return element.getTemplateId();
}
renderTemplate(tree: ITree, templateId: string, container: HTMLElement) {
throw new Error('Method not implemented.');
if (templateId === SESSION_TEMPLATE_ID) {
let data: ISessionTemplateData = Object.create(null);
data.session = dom.append(container, $('.session'));
return data;
}
if (templateId === SOURCE_TEMPLATE_ID) {
let data: ISourceTemplateData = Object.create(null);
data.source = dom.append(container, $('.source'));
return data;
}
let data: INodeTemplateData = Object.create(null);
data.node = dom.append(container, $('.node'));
return data;
}
renderElement(tree: ITree, element: any, templateId: string, templateData: any): void {
throw new Error('Method not implemented.');
if (templateId === SESSION_TEMPLATE_ID) {
this.renderSession(element, templateData);
} else if (templateId === SOURCE_TEMPLATE_ID) {
this.renderSource(element, templateData);
} else if (templateId === ROOT_FOLDER_TEMPLATE_ID) {
this.renderNode(element, templateData);
}
}
disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
throw new Error('Method not implemented.');
// noop
}
private renderSession(session: SessionTreeItem, data: ISessionTemplateData): void {
data.session.title = 'session';
data.session.textContent = session.getLabel();
}
private renderSource(source: BaseTreeItem, data: ISourceTemplateData): void {
data.source.title = 'source';
data.source.textContent = source.getLabel();
}
private renderNode(node: BaseTreeItem, data: INodeTemplateData): void {
data.node.title = 'node';
data.node.textContent = node.getLabel();
}
}
@@ -49,6 +49,7 @@ export const CONTEXT_EXPRESSION_SELECTED = new RawContextKey<boolean>('expressio
export const CONTEXT_BREAKPOINT_SELECTED = new RawContextKey<boolean>('breakpointSelected', false);
export const CONTEXT_CALLSTACK_ITEM_TYPE = new RawContextKey<string>('callStackItemType', undefined);
export const CONTEXT_LOADED_SCRIPTS_SUPPORTED = new RawContextKey<boolean>('loadedScriptsSupported', false);
export const CONTEXT_LOADED_SCRIPTS_ITEM_TYPE = new RawContextKey<string>('loadedScriptsItemType', undefined);
export const EDITOR_CONTRIBUTION_ID = 'editor.contrib.debug';
export const DEBUG_SCHEME = 'debug';
@@ -135,6 +136,8 @@ export interface IRawSession {
completions(args: DebugProtocol.CompletionsArguments): TPromise<DebugProtocol.CompletionsResponse>;
setVariable(args: DebugProtocol.SetVariableArguments): TPromise<DebugProtocol.SetVariableResponse>;
source(args: DebugProtocol.SourceArguments): TPromise<DebugProtocol.SourceResponse>;
loadedSources(args: DebugProtocol.LoadedSourcesArguments): TPromise<DebugProtocol.LoadedSourcesResponse>;
}
export enum SessionState {
@@ -151,6 +154,7 @@ export interface ISession extends ITreeElement {
getThread(threadId: number): IThread;
getAllThreads(): ReadonlyArray<IThread>;
getSource(raw: DebugProtocol.Source): Source;
getLoadedSources(): TPromise<Source[]>;
completions(frameId: number, text: string, position: Position, overwriteBefore: number): TPromise<ISuggestion[]>;
}
@@ -572,6 +576,12 @@ export interface DebugEvent extends DebugProtocol.Event {
sessionId?: string;
}
export interface LoadedSourceEvent {
session: ISession;
reason: string;
source: Source;
}
export interface IDebugService {
_serviceBrand: any;
@@ -595,6 +605,11 @@ export interface IDebugService {
*/
onDidEndSession: Event<ISession>;
/**
* Allows to register on loaded source events.
*/
onDidLoadedSource: Event<LoadedSourceEvent>;
/**
* Allows to register on custom DAP events.
*/
@@ -616,6 +616,14 @@ export class Session implements ISession {
return result;
}
public getLoadedSources(): TPromise<Source[]> {
return this.raw.loadedSources({}).then(response => {
return response.body.sources.map(src => this.getSource(src));
}, error => {
return [];
});
}
public getId(): string {
return this.session.getId();
}
@@ -69,8 +69,6 @@ export class ViewModel implements IViewModel {
this._focusedStackFrame = stackFrame;
this.loadedScriptsSupportedContextKey.set(session && session.raw.capabilities.supportsLoadedSourcesRequest);
// @weinand remove the next line which always disables the context for the view to be shown
this.loadedScriptsSupportedContextKey.set(false);
if (shouldEmit) {
this._onDidFocusStackFrame.fire({ stackFrame, explicit });
@@ -69,6 +69,7 @@ export class DebugService implements debug.IDebugService {
private readonly _onDidChangeState: Emitter<debug.State>;
private readonly _onDidNewSession: Emitter<debug.ISession>;
private readonly _onDidEndSession: Emitter<debug.ISession>;
private readonly _onDidLoadedSource: Emitter<debug.LoadedSourceEvent>;
private readonly _onDidCustomEvent: Emitter<debug.DebugEvent>;
private model: Model;
private viewModel: ViewModel;
@@ -113,6 +114,7 @@ export class DebugService implements debug.IDebugService {
this._onDidChangeState = new Emitter<debug.State>();
this._onDidNewSession = new Emitter<debug.ISession>();
this._onDidEndSession = new Emitter<debug.ISession>();
this._onDidLoadedSource = new Emitter<debug.LoadedSourceEvent>();
this._onDidCustomEvent = new Emitter<debug.DebugEvent>();
this.sessionStates = new Map<string, debug.State>();
this.allSessions = new Map<string, debug.ISession>();
@@ -450,6 +452,14 @@ export class DebugService implements debug.IDebugService {
}
}));
this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidLoadedSource(event => {
this._onDidLoadedSource.fire({
session: session,
reason: event.body.reason,
source: session.getSource(event.body.source)
});
}));
this.toDisposeOnSessionEnd.get(session.getId()).push(raw.onDidCustomEvent(event => {
this._onDidCustomEvent.fire(event);
}));
@@ -542,6 +552,10 @@ export class DebugService implements debug.IDebugService {
return this._onDidEndSession.event;
}
public get onDidLoadedSource(): Event<debug.LoadedSourceEvent> {
return this._onDidLoadedSource.event;
}
public get onDidCustomEvent(): Event<debug.DebugEvent> {
return this._onDidCustomEvent.event;
}
@@ -58,6 +58,7 @@ export class RawDebugSession implements IRawSession {
private readonly _onDidThread: Emitter<DebugProtocol.ThreadEvent>;
private readonly _onDidOutput: Emitter<DebugProtocol.OutputEvent>;
private readonly _onDidBreakpoint: Emitter<DebugProtocol.BreakpointEvent>;
private readonly _onDidLoadedSource: Emitter<DebugProtocol.LoadedSourceEvent>;
private readonly _onDidCustomEvent: Emitter<DebugEvent>;
private readonly _onDidEvent: Emitter<DebugProtocol.Event>;
@@ -85,6 +86,7 @@ export class RawDebugSession implements IRawSession {
this._onDidThread = new Emitter<DebugProtocol.ThreadEvent>();
this._onDidOutput = new Emitter<DebugProtocol.OutputEvent>();
this._onDidBreakpoint = new Emitter<DebugProtocol.BreakpointEvent>();
this._onDidLoadedSource = new Emitter<DebugProtocol.LoadedSourceEvent>();
this._onDidCustomEvent = new Emitter<DebugEvent>();
this._onDidEvent = new Emitter<DebugProtocol.Event>();
}
@@ -129,6 +131,10 @@ export class RawDebugSession implements IRawSession {
return this._onDidBreakpoint.event;
}
public get onDidLoadedSource(): Event<DebugProtocol.LoadedSourceEvent> {
return this._onDidLoadedSource.event;
}
public get onDidCustomEvent(): Event<DebugEvent> {
return this._onDidCustomEvent.event;
}
@@ -234,7 +240,9 @@ export class RawDebugSession implements IRawSession {
private onDapEvent(event: DebugEvent): void {
event.sessionId = this.id;
if (event.event === 'initialized') {
if (event.event === 'loadedSource') { // most frequent comes first
this._onDidLoadedSource.fire(<DebugProtocol.LoadedSourceEvent>event);
} else if (event.event === 'initialized') {
this.readyForBreakpoints = true;
this._onDidInitialize.fire(event);
} else if (event.event === 'capabilities' && event.body) {
@@ -387,6 +395,11 @@ export class RawDebugSession implements IRawSession {
return this.send<DebugProtocol.SourceResponse>('source', args);
}
public loadedSources(args: DebugProtocol.LoadedSourcesArguments): TPromise<DebugProtocol.LoadedSourcesResponse> {
return this.send<DebugProtocol.LoadedSourcesResponse>('loadedSources', args);
}
public threads(): TPromise<DebugProtocol.ThreadsResponse> {
return this.send<DebugProtocol.ThreadsResponse>('threads', null);
}
@@ -7,7 +7,7 @@ import uri from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { ILaunch, IDebugService, State, DebugEvent, ISession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IModel, IViewModel, IRawSession, IBreakpoint } from 'vs/workbench/parts/debug/common/debug';
import { ILaunch, IDebugService, State, DebugEvent, ISession, IConfigurationManager, IStackFrame, IBreakpointData, IBreakpointUpdateData, IConfig, IModel, IViewModel, IRawSession, IBreakpoint, LoadedSourceEvent } from 'vs/workbench/parts/debug/common/debug';
export class MockDebugService implements IDebugService {
public _serviceBrand: any;
@@ -32,6 +32,10 @@ export class MockDebugService implements IDebugService {
return null;
}
public get onDidLoadedSource(): Event<LoadedSourceEvent> {
return null;
}
public getConfigurationManager(): IConfigurationManager {
return null;
}
@@ -242,6 +246,10 @@ export class MockSession implements IRawSession {
return TPromise.as(null);
}
public loadedSources(args: DebugProtocol.LoadedSourcesArguments): TPromise<DebugProtocol.LoadedSourcesResponse> {
return TPromise.as(null);
}
public setBreakpoints(args: DebugProtocol.SetBreakpointsArguments): TPromise<DebugProtocol.SetBreakpointsResponse> {
return TPromise.as(null);
}