<commit_before>/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import * as lifecycle from 'vs/base/common/lifecycle';
import { KeyCode } from 'vs/base/common/keyCodes';
import * as paths from 'vs/base/common/paths';
import * as errors from 'vs/base/common/errors';
import { equalsIgnoreCase } from 'vs/base/common/strings';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import * as dom from 'vs/base/browser/dom';
import { IMouseEvent, DragMouseEvent } from 'vs/base/browser/mouseEvent';
import { getPathLabel } from 'vs/base/common/labels';
import { IAction, IActionRunner } from 'vs/base/common/actions';
import { IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { ITree, IAccessibilityProvider, ContextMenuEvent, IDataSource, IRenderer, DRAG_OVER_REJECT, IDragAndDropData, IDragOverReaction, IActionProvider } from 'vs/base/parts/tree/browser/tree';
import { InputBox, IInputValidationOptions } from 'vs/base/browser/ui/inputbox/inputBox';
import { DefaultController, DefaultDragAndDrop, ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults';
import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions';
import { fillInActions } from 'vs/platform/actions/browser/menuItemActionItem';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import * as debug from 'vs/workbench/parts/debug/common/debug';
import { Expression, Variable, FunctionBreakpoint, StackFrame, Thread, Process, Breakpoint, ExceptionBreakpoint, Model, Scope, ThreadAndProcessIds } from 'vs/workbench/parts/debug/common/debugModel';
import { ViewModel } from 'vs/workbench/parts/debug/common/debugViewModel';
import { ContinueAction, StepOverAction, PauseAction, ReapplyBreakpointsAction, DisableAllBreakpointsAction, RemoveBreakpointAction, RemoveWatchExpressionAction, AddWatchExpressionAction, EditWatchExpressionAction, RemoveAllBreakpointsAction, EnableAllBreakpointsAction, StepOutAction, StepIntoAction, SetValueAction, RemoveAllWatchExpressionsAction, RestartFrameAction, AddToWatchExpressionsAction, StopAction, RestartAction } from 'vs/workbench/parts/debug/browser/debugActions';
import { CopyValueAction, CopyStackTraceAction } from 'vs/workbench/parts/debug/electron-browser/electronDebugActions';
import { Source } from 'vs/workbench/parts/debug/common/debugSource';
import { once } from 'vs/base/common/functional';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';

const $ = dom.$;
const booleanRegex = /^true|false$/i;
const stringRegex = /^(['"]).*\1$/;
const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024;

export interface IRenderValueOptions {
	preserveWhitespace?: boolean;
	showChanged?: boolean;
	maxValueLength?: number;
	showHover?: boolean;
}

function replaceWhitespace(value: string): string {
	const map = { '\n': '\\n', '\r': '\\r', '\t': '\\t' };
	return value.replace(/[\n\r\t]/g, char => map[char]);
}

export function renderExpressionValue(expressionOrValue: debug.IExpression | string, container: HTMLElement, options: IRenderValueOptions): void {
	let value = typeof expressionOrValue === 'string' ? expressionOrValue : expressionOrValue.value;

	// remove stale classes
	container.className = 'value';
	// when resolving expressions we represent errors from the server as a variable with name === null.
	if (value === null || ((expressionOrValue instanceof Expression || expressionOrValue instanceof Variable) && !expressionOrValue.available)) {
		dom.addClass(container, 'unavailable');
		if (value !== Expression.DEFAULT_VALUE) {
			dom.addClass(container, 'error');
		}
	} else if (!isNaN(+value)) {
		dom.addClass(container, 'number');
	} else if (booleanRegex.test(value)) {
		dom.addClass(container, 'boolean');
	} else if (stringRegex.test(value)) {
		dom.addClass(container, 'string');
	}

	if (options.showChanged && (<any>expressionOrValue).valueChanged && value !== Expression.DEFAULT_VALUE) {
		// value changed color has priority over other colors.
		container.className = 'value changed';
	}

	if (options.maxValueLength && value.length > options.maxValueLength) {
		value = value.substr(0, options.maxValueLength) + '...';
	}
	if (value && !options.preserveWhitespace) {
		container.textContent = replaceWhitespace(value);
	} else {
		container.textContent = value;
	}
	if (options.showHover) {
		container.title = value;
	}
}

export function renderVariable(tree: ITree, variable: Variable, data: IVariableTemplateData, showChanged: boolean): void {
	if (variable.available) {
		data.name.textContent = replaceWhitespace(variable.name);
		data.name.title = variable.type ? variable.type : variable.name;
	}

	if (variable.value) {
		data.name.textContent += variable.name ? ':' : '';
		renderExpressionValue(variable, data.value, {
			showChanged,
			maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET,
			preserveWhitespace: false,
			showHover: true
		});
	} else {
		data.value.textContent = '';
		data.value.title = '';
	}
}

interface IRenameBoxOptions {
	initialValue: string;
	ariaLabel: string;
	placeholder?: string;
	validationOptions?: IInputValidationOptions;
}

function renderRenameBox(debugService: debug.IDebugService, contextViewService: IContextViewService, themeService: IThemeService, tree: ITree, element: any, container: HTMLElement, options: IRenameBoxOptions): void {
	let inputBoxContainer = dom.append(container, $('.inputBoxContainer'));
	let inputBox = new InputBox(inputBoxContainer, contextViewService, {
		validationOptions: options.validationOptions,
		placeholder: options.placeholder,
		ariaLabel: options.ariaLabel
	});
	const styler = attachInputBoxStyler(inputBox, themeService);

	tree.setHighlight();
	inputBox.value = options.initialValue ? options.initialValue : '';
	inputBox.focus();
	inputBox.select();

	let disposed = false;
	const toDispose: [lifecycle.IDisposable] = [inputBox, styler];

	const wrapUp = once((renamed: boolean) => {
		if (!disposed) {
			disposed = true;
			if (element instanceof Expression && renamed && inputBox.value) {
				debugService.renameWatchExpression(element.getId(), inputBox.value).done(null, errors.onUnexpectedError);
			} else if (element instanceof Expression && !element.name) {
				debugService.removeWatchExpressions(element.getId());
			} else if (element instanceof FunctionBreakpoint && inputBox.value) {
				debugService.renameFunctionBreakpoint(element.getId(), renamed ? inputBox.value : element.name).done(null, errors.onUnexpectedError);
			} else if (element instanceof FunctionBreakpoint && !element.name) {
				debugService.removeFunctionBreakpoints(element.getId()).done(null, errors.onUnexpectedError);
			} else if (element instanceof Variable) {
				element.errorMessage = null;
				if (renamed && element.value !== inputBox.value) {
					element.setVariable(inputBox.value)
						// if everything went fine we need to refresh ui elements since the variable update can change watch and variables view
						.done(() => tree.refresh(element, false), errors.onUnexpectedError);
				}
			}

			tree.clearHighlight();
			tree.DOMFocus();
			tree.setFocus(element);

			// need to remove the input box since this template will be reused.
			container.removeChild(inputBoxContainer);
			lifecycle.dispose(toDispose);
		}
	});

	toDispose.push(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => {
		const isEscape = e.equals(KeyCode.Escape);
		const isEnter = e.equals(KeyCode.Enter);
		if (isEscape || isEnter) {
			e.preventDefault();
			e.stopPropagation();
			wrapUp(isEnter);
		}
	}));
	toDispose.push(dom.addDisposableListener(inputBox.inputElement, 'blur', () => {
		wrapUp(true);
	}));
}

function getSourceName(source: Source, contextService: IWorkspaceContextService): string {
	if (source.name) {
		return source.name;
	}

	return getPathLabel(paths.basename(source.uri.fsPath), contextService);
}

export class BaseDebugController extends DefaultController {

	private contributedContextMenu: IMenu;

	constructor(
		private actionProvider: IActionProvider,
		menuId: MenuId,
		@debug.IDebugService protected debugService: debug.IDebugService,
		@IWorkbenchEditorService protected editorService: IWorkbenchEditorService,
		@IContextMenuService private contextMenuService: IContextMenuService,
		@IContextKeyService contextKeyService: IContextKeyService,
		@IMenuService menuService: IMenuService
	) {
		super({ clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: false });

		this.contributedContextMenu = menuService.createMenu(menuId, contextKeyService);
	}

	public onContextMenu(tree: ITree, element: debug.IEnablement, event: ContextMenuEvent): boolean {
		if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
			return false;
		}

		event.preventDefault();
		event.stopPropagation();

		tree.setFocus(element);

		if (this.actionProvider.hasSecondaryActions(tree, element)) {
			const anchor = { x: event.posx + 1, y: event.posy };
			this.contextMenuService.showContextMenu({
				getAnchor: () => anchor,
				getActions: () => this.actionProvider.getSecondaryActions(tree, element).then(actions => {
					fillInActions(this.contributedContextMenu, { arg: this.getContext(element) }, actions);
					return actions;
				}),
				onHide: (wasCancelled?: boolean) => {
					if (wasCancelled) {
						tree.DOMFocus();
					}
				},
				getActionsContext: () => element
			});

			return true;
		}

		return false;
	}

	protected getContext(element: any): any {
		return undefined;
	}
}

// call stack

export class CallStackController extends BaseDebugController {

	protected onLeftClick(tree: ITree, element: any, event: IMouseEvent): boolean {
		if (element instanceof ThreadAndProcessIds) {
			return this.showMoreStackFrames(tree, element);
		}
		if (element instanceof StackFrame) {
			this.focusStackFrame(element, event, event.detail !== 2);
		}

		return super.onLeftClick(tree, element, event);
	}

	protected getContext(element: any): any {
		if (element instanceof StackFrame) {
			if (element.source.inMemory) {
				return element.source.raw.path || element.source.reference;
			}

			return element.source.uri.toString();
		}
	}

	// user clicked / pressed on 'Load More Stack Frames', get those stack frames and refresh the tree.
	public showMoreStackFrames(tree: ITree, threadAndProcessIds: ThreadAndProcessIds): boolean {
		const process = this.debugService.getModel().getProcesses().filter(p => p.getId() === threadAndProcessIds.processId).pop();
		const thread = process && process.getThread(threadAndProcessIds.threadId);
		if (thread) {
			(<Thread>thread).fetchCallStack(true)
				.done(() => tree.refresh(), errors.onUnexpectedError);
		}

		return true;
	}

	public focusStackFrame(stackFrame: debug.IStackFrame, event: IKeyboardEvent | IMouseEvent, preserveFocus: boolean): void {
		this.debugService.focusStackFrameAndEvaluate(stackFrame).then(() => {
			const sideBySide = (event && (event.ctrlKey || event.metaKey));
			return stackFrame.openInEditor(this.editorService, preserveFocus, sideBySide);
		}, errors.onUnexpectedError);
	}
}


export class CallStackActionProvider implements IActionProvider {

	constructor( @IInstantiationService private instantiationService: IInstantiationService, @debug.IDebugService private debugService: debug.IDebugService) {
		// noop
	}

	public hasActions(tree: ITree, element: any): boolean {
		return false;
	}

	public getActions(tree: ITree, element: any): TPromise<IAction[]> {
		return TPromise.as([]);
	}

	public hasSecondaryActions(tree: ITree, element: any): boolean {
		return element !== tree.getInput();
	}

	public getSecondaryActions(tree: ITree, element: any): TPromise<IAction[]> {
		const actions: IAction[] = [];
		if (element instanceof Process) {
			actions.push(this.instantiationService.createInstance(RestartAction, RestartAction.ID, RestartAction.LABEL));
			actions.push(this.instantiationService.createInstance(StopAction, StopAction.ID, StopAction.LABEL));
		} else if (element instanceof Thread) {
			const thread = <Thread>element;
			if (thread.stopped) {
				actions.push(this.instantiationService.createInstance(ContinueAction, ContinueAction.ID, ContinueAction.LABEL));
				actions.push(this.instantiationService.createInstance(StepOverAction, StepOverAction.ID, StepOverAction.LABEL));
				actions.push(this.instantiationService.createInstance(StepIntoAction, StepIntoAction.ID, StepIntoAction.LABEL));
				actions.push(this.instantiationService.createInstance(StepOutAction, StepOutAction.ID, StepOutAction.LABEL));
			} else {
				actions.push(this.instantiationService.createInstance(PauseAction, PauseAction.ID, PauseAction.LABEL));
			}
		} else if (element instanceof StackFrame) {
			if (element.thread.process.session.capabilities.supportsRestartFrame) {
				actions.push(this.instantiationService.createInstance(RestartFrameAction, RestartFrameAction.ID, RestartFrameAction.LABEL));
			}
			actions.push(new CopyStackTraceAction(CopyStackTraceAction.ID, CopyStackTraceAction.LABEL));
		}

		return TPromise.as(actions);
	}

	public getActionItem(tree: ITree, element: any, action: IAction): IActionItem {
		return null;
	}
}

export class CallStackDataSource implements IDataSource {

	public getId(tree: ITree, element: any): string {
		if (typeof element === 'string') {
			return element;
		}

		return element.getId();
	}

	public hasChildren(tree: ITree, element: any): boolean {
		return element instanceof Model || element instanceof Process || (element instanceof Thread && (<Thread>element).stopped);
	}

	public getChildren(tree: ITree, element: any): TPromise<any> {
		if (element instanceof Thread) {
			return this.getThreadChildren(element);
		}
		if (element instanceof Model) {
			return TPromise.as(element.getProcesses());
		}

		const process = <debug.IProcess>element;
		return TPromise.as(process.getAllThreads());
	}

	private getThreadChildren(thread: Thread): TPromise<any> {
		return thread.fetchCallStack().then((callStack: any[]) => {
			if (thread.stoppedDetails && thread.stoppedDetails.framesErrorMessage) {
				return callStack.concat([thread.stoppedDetails.framesErrorMessage]);
			}
			if (thread.stoppedDetails && thread.stoppedDetails.totalFrames > callStack.length) {
				return callStack.concat([new ThreadAndProcessIds(thread.process.getId(), thread.threadId)]);
			}

			return callStack;
		});
	}

	public getParent(tree: ITree, element: any): TPromise<any> {
		return TPromise.as(null);
	}
}

interface IThreadTemplateData {
	thread: HTMLElement;
	name: HTMLElement;
	state: HTMLElement;
	stateLabel: HTMLSpanElement;
}

interface IProcessTemplateData {
	process: HTMLElement;
	name: HTMLElement;
	state: HTMLElement;
	stateLabel: HTMLSpanElement;
}

interface IErrorTemplateData {
	label: HTMLElement;
}

interface ILoadMoreTemplateData {
	label: HTMLElement;
}

interface IStackFrameTemplateData {
	stackFrame: HTMLElement;
	label: HTMLElement;
	file: HTMLElement;
	fileName: HTMLElement;
	lineNumber: HTMLElement;
}

export class CallStackRenderer implements IRenderer {

	private static THREAD_TEMPLATE_ID = 'thread';
	private static STACK_FRAME_TEMPLATE_ID = 'stackFrame';
	private static ERROR_TEMPLATE_ID = 'error';
	private static LOAD_MORE_TEMPLATE_ID = 'loadMore';
	private static PROCESS_TEMPLATE_ID = 'process';

	constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService) {
		// noop
	}

	public getHeight(tree: ITree, element: any): number {
		return 22;
	}

	public getTemplateId(tree: ITree, element: any): string {
		if (element instanceof Process) {
			return CallStackRenderer.PROCESS_TEMPLATE_ID;
		}
		if (element instanceof Thread) {
			return CallStackRenderer.THREAD_TEMPLATE_ID;
		}
		if (element instanceof StackFrame) {
			return CallStackRenderer.STACK_FRAME_TEMPLATE_ID;
		}
		if (typeof element === 'string') {
			return CallStackRenderer.ERROR_TEMPLATE_ID;
		}

		return CallStackRenderer.LOAD_MORE_TEMPLATE_ID;
	}

	public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any {
		if (templateId === CallStackRenderer.PROCESS_TEMPLATE_ID) {
			let data: IProcessTemplateData = Object.create(null);
			data.process = dom.append(container, $('.process'));
			data.name = dom.append(data.process, $('.name'));
			data.state = dom.append(data.process, $('.state'));
			data.stateLabel = dom.append(data.state, $('span.label'));

			return data;
		}

		if (templateId === CallStackRenderer.LOAD_MORE_TEMPLATE_ID) {
			let data: ILoadMoreTemplateData = Object.create(null);
			data.label = dom.append(container, $('.load-more'));

			return data;
		}
		if (templateId === CallStackRenderer.ERROR_TEMPLATE_ID) {
			let data: ILoadMoreTemplateData = Object.create(null);
			data.label = dom.append(container, $('.error'));

			return data;
		}
		if (templateId === CallStackRenderer.THREAD_TEMPLATE_ID) {
			let data: IThreadTemplateData = Object.create(null);
			data.thread = dom.append(container, $('.thread'));
			data.name = dom.append(data.thread, $('.name'));
			data.state = dom.append(data.thread, $('.state'));
			data.stateLabel = dom.append(data.state, $('span.label'));

			return data;
		}

		let data: IStackFrameTemplateData = Object.create(null);
		data.stackFrame = dom.append(container, $('.stack-frame'));
		data.label = dom.append(data.stackFrame, $('span.label.expression'));
		data.file = dom.append(data.stackFrame, $('.file'));
		data.fileName = dom.append(data.file, $('span.file-name'));
		data.lineNumber = dom.append(data.file, $('span.line-number'));

		return data;
	}

	public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void {
		if (templateId === CallStackRenderer.PROCESS_TEMPLATE_ID) {
			this.renderProcess(element, templateData);
		} else if (templateId === CallStackRenderer.THREAD_TEMPLATE_ID) {
			this.renderThread(element, templateData);
		} else if (templateId === CallStackRenderer.STACK_FRAME_TEMPLATE_ID) {
			this.renderStackFrame(element, templateData);
		} else if (templateId === CallStackRenderer.ERROR_TEMPLATE_ID) {
			this.renderError(element, templateData);
		} else if (templateId === CallStackRenderer.LOAD_MORE_TEMPLATE_ID) {
			this.renderLoadMore(element, templateData);
		}
	}

	private renderProcess(process: debug.IProcess, data: IProcessTemplateData): void {
		data.process.title = nls.localize({ key: 'process', comment: ['Process is a noun'] }, "Process");
		data.name.textContent = process.name;
		const stoppedThread = process.getAllThreads().filter(t => t.stopped).pop();

		data.stateLabel.textContent = stoppedThread ? nls.localize('paused', "Paused")
			: nls.localize({ key: 'running', comment: ['indicates state'] }, "Running");
	}

	private renderThread(thread: debug.IThread, data: IThreadTemplateData): void {
		data.thread.title = nls.localize('thread', "Thread");
		data.name.textContent = thread.name;

		if (thread.stopped) {
			data.stateLabel.textContent = thread.stoppedDetails.description || nls.localize({ key: 'pausedOn', comment: ['indicates reason for program being paused'] }, "Paused on {0}", thread.stoppedDetails.reason);
		} else {
			data.stateLabel.textContent = nls.localize({ key: 'running', comment: ['indicates state'] }, "Running");
		}
	}

	private renderError(element: string, data: IErrorTemplateData) {
		data.label.textContent = element;
		data.label.title = element;
	}

	private renderLoadMore(element: any, data: ILoadMoreTemplateData): void {
		data.label.textContent = nls.localize('loadMoreStackFrames', "Load More Stack Frames");
	}

	private renderStackFrame(stackFrame: debug.IStackFrame, data: IStackFrameTemplateData): void {
		stackFrame.source.presenationHint === 'deemphasize' ? dom.addClass(data.stackFrame, 'disabled') : dom.removeClass(data.stackFrame, 'disabled');
		stackFrame.source.presenationHint === 'label' ? dom.addClass(data.stackFrame, 'label') : dom.removeClass(data.stackFrame, 'label');
		data.file.title = stackFrame.source.raw.path || stackFrame.source.name;
		if (stackFrame.source.raw.origin) {
			data.file.title += `\n${stackFrame.source.raw.origin}`;
		}
		data.label.textContent = stackFrame.name;
		data.label.title = stackFrame.name;
		data.fileName.textContent = getSourceName(stackFrame.source, this.contextService);
		if (stackFrame.lineNumber !== undefined) {
			data.lineNumber.textContent = `${stackFrame.lineNumber}`;
			dom.removeClass(data.lineNumber, 'unavailable');
		} else {
			dom.addClass(data.lineNumber, 'unavailable');
		}
	}

	public disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
		// noop
	}
}

export class CallstackAccessibilityProvider implements IAccessibilityProvider {

	constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService) {
		// noop
	}

	public getAriaLabel(tree: ITree, element: any): string {
		if (element instanceof Thread) {
			return nls.localize('threadAriaLabel', "Thread {0}, callstack, debug", (<Thread>element).name);
		}
		if (element instanceof StackFrame) {
			return nls.localize('stackFrameAriaLabel', "Stack Frame {0} line {1} {2}, callstack, debug", (<StackFrame>element).name, (<StackFrame>element).lineNumber, getSourceName((<StackFrame>element).source, this.contextService));
		}

		return null;<commit_msg>debug: show columns in callstack view<commit_after>/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import * as lifecycle from 'vs/base/common/lifecycle';
import { KeyCode } from 'vs/base/common/keyCodes';
import * as paths from 'vs/base/common/paths';
import * as errors from 'vs/base/common/errors';
import { equalsIgnoreCase } from 'vs/base/common/strings';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import * as dom from 'vs/base/browser/dom';
import { IMouseEvent, DragMouseEvent } from 'vs/base/browser/mouseEvent';
import { getPathLabel } from 'vs/base/common/labels';
import { IAction, IActionRunner } from 'vs/base/common/actions';
import { IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { ITree, IAccessibilityProvider, ContextMenuEvent, IDataSource, IRenderer, DRAG_OVER_REJECT, IDragAndDropData, IDragOverReaction, IActionProvider } from 'vs/base/parts/tree/browser/tree';
import { InputBox, IInputValidationOptions } from 'vs/base/browser/ui/inputbox/inputBox';
import { DefaultController, DefaultDragAndDrop, ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults';
import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions';
import { fillInActions } from 'vs/platform/actions/browser/menuItemActionItem';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import * as debug from 'vs/workbench/parts/debug/common/debug';
import { Expression, Variable, FunctionBreakpoint, StackFrame, Thread, Process, Breakpoint, ExceptionBreakpoint, Model, Scope, ThreadAndProcessIds } from 'vs/workbench/parts/debug/common/debugModel';
import { ViewModel } from 'vs/workbench/parts/debug/common/debugViewModel';
import { ContinueAction, StepOverAction, PauseAction, ReapplyBreakpointsAction, DisableAllBreakpointsAction, RemoveBreakpointAction, RemoveWatchExpressionAction, AddWatchExpressionAction, EditWatchExpressionAction, RemoveAllBreakpointsAction, EnableAllBreakpointsAction, StepOutAction, StepIntoAction, SetValueAction, RemoveAllWatchExpressionsAction, RestartFrameAction, AddToWatchExpressionsAction, StopAction, RestartAction } from 'vs/workbench/parts/debug/browser/debugActions';
import { CopyValueAction, CopyStackTraceAction } from 'vs/workbench/parts/debug/electron-browser/electronDebugActions';
import { Source } from 'vs/workbench/parts/debug/common/debugSource';
import { once } from 'vs/base/common/functional';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';

const $ = dom.$;
const booleanRegex = /^true|false$/i;
const stringRegex = /^(['"]).*\1$/;
const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024;

export interface IRenderValueOptions {
	preserveWhitespace?: boolean;
	showChanged?: boolean;
	maxValueLength?: number;
	showHover?: boolean;
}

function replaceWhitespace(value: string): string {
	const map = { '\n': '\\n', '\r': '\\r', '\t': '\\t' };
	return value.replace(/[\n\r\t]/g, char => map[char]);
}

export function renderExpressionValue(expressionOrValue: debug.IExpression | string, container: HTMLElement, options: IRenderValueOptions): void {
	let value = typeof expressionOrValue === 'string' ? expressionOrValue : expressionOrValue.value;

	// remove stale classes
	container.className = 'value';
	// when resolving expressions we represent errors from the server as a variable with name === null.
	if (value === null || ((expressionOrValue instanceof Expression || expressionOrValue instanceof Variable) && !expressionOrValue.available)) {
		dom.addClass(container, 'unavailable');
		if (value !== Expression.DEFAULT_VALUE) {
			dom.addClass(container, 'error');
		}
	} else if (!isNaN(+value)) {
		dom.addClass(container, 'number');
	} else if (booleanRegex.test(value)) {
		dom.addClass(container, 'boolean');
	} else if (stringRegex.test(value)) {
		dom.addClass(container, 'string');
	}

	if (options.showChanged && (<any>expressionOrValue).valueChanged && value !== Expression.DEFAULT_VALUE) {
		// value changed color has priority over other colors.
		container.className = 'value changed';
	}

	if (options.maxValueLength && value.length > options.maxValueLength) {
		value = value.substr(0, options.maxValueLength) + '...';
	}
	if (value && !options.preserveWhitespace) {
		container.textContent = replaceWhitespace(value);
	} else {
		container.textContent = value;
	}
	if (options.showHover) {
		container.title = value;
	}
}

export function renderVariable(tree: ITree, variable: Variable, data: IVariableTemplateData, showChanged: boolean): void {
	if (variable.available) {
		data.name.textContent = replaceWhitespace(variable.name);
		data.name.title = variable.type ? variable.type : variable.name;
	}

	if (variable.value) {
		data.name.textContent += variable.name ? ':' : '';
		renderExpressionValue(variable, data.value, {
			showChanged,
			maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET,
			preserveWhitespace: false,
			showHover: true
		});
	} else {
		data.value.textContent = '';
		data.value.title = '';
	}
}

interface IRenameBoxOptions {
	initialValue: string;
	ariaLabel: string;
	placeholder?: string;
	validationOptions?: IInputValidationOptions;
}

function renderRenameBox(debugService: debug.IDebugService, contextViewService: IContextViewService, themeService: IThemeService, tree: ITree, element: any, container: HTMLElement, options: IRenameBoxOptions): void {
	let inputBoxContainer = dom.append(container, $('.inputBoxContainer'));
	let inputBox = new InputBox(inputBoxContainer, contextViewService, {
		validationOptions: options.validationOptions,
		placeholder: options.placeholder,
		ariaLabel: options.ariaLabel
	});
	const styler = attachInputBoxStyler(inputBox, themeService);

	tree.setHighlight();
	inputBox.value = options.initialValue ? options.initialValue : '';
	inputBox.focus();
	inputBox.select();

	let disposed = false;
	const toDispose: [lifecycle.IDisposable] = [inputBox, styler];

	const wrapUp = once((renamed: boolean) => {
		if (!disposed) {
			disposed = true;
			if (element instanceof Expression && renamed && inputBox.value) {
				debugService.renameWatchExpression(element.getId(), inputBox.value).done(null, errors.onUnexpectedError);
			} else if (element instanceof Expression && !element.name) {
				debugService.removeWatchExpressions(element.getId());
			} else if (element instanceof FunctionBreakpoint && inputBox.value) {
				debugService.renameFunctionBreakpoint(element.getId(), renamed ? inputBox.value : element.name).done(null, errors.onUnexpectedError);
			} else if (element instanceof FunctionBreakpoint && !element.name) {
				debugService.removeFunctionBreakpoints(element.getId()).done(null, errors.onUnexpectedError);
			} else if (element instanceof Variable) {
				element.errorMessage = null;
				if (renamed && element.value !== inputBox.value) {
					element.setVariable(inputBox.value)
						// if everything went fine we need to refresh ui elements since the variable update can change watch and variables view
						.done(() => tree.refresh(element, false), errors.onUnexpectedError);
				}
			}

			tree.clearHighlight();
			tree.DOMFocus();
			tree.setFocus(element);

			// need to remove the input box since this template will be reused.
			container.removeChild(inputBoxContainer);
			lifecycle.dispose(toDispose);
		}
	});

	toDispose.push(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => {
		const isEscape = e.equals(KeyCode.Escape);
		const isEnter = e.equals(KeyCode.Enter);
		if (isEscape || isEnter) {
			e.preventDefault();
			e.stopPropagation();
			wrapUp(isEnter);
		}
	}));
	toDispose.push(dom.addDisposableListener(inputBox.inputElement, 'blur', () => {
		wrapUp(true);
	}));
}

function getSourceName(source: Source, contextService: IWorkspaceContextService): string {
	if (source.name) {
		return source.name;
	}

	return getPathLabel(paths.basename(source.uri.fsPath), contextService);
}

export class BaseDebugController extends DefaultController {

	private contributedContextMenu: IMenu;

	constructor(
		private actionProvider: IActionProvider,
		menuId: MenuId,
		@debug.IDebugService protected debugService: debug.IDebugService,
		@IWorkbenchEditorService protected editorService: IWorkbenchEditorService,
		@IContextMenuService private contextMenuService: IContextMenuService,
		@IContextKeyService contextKeyService: IContextKeyService,
		@IMenuService menuService: IMenuService
	) {
		super({ clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: false });

		this.contributedContextMenu = menuService.createMenu(menuId, contextKeyService);
	}

	public onContextMenu(tree: ITree, element: debug.IEnablement, event: ContextMenuEvent): boolean {
		if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
			return false;
		}

		event.preventDefault();
		event.stopPropagation();

		tree.setFocus(element);

		if (this.actionProvider.hasSecondaryActions(tree, element)) {
			const anchor = { x: event.posx + 1, y: event.posy };
			this.contextMenuService.showContextMenu({
				getAnchor: () => anchor,
				getActions: () => this.actionProvider.getSecondaryActions(tree, element).then(actions => {
					fillInActions(this.contributedContextMenu, { arg: this.getContext(element) }, actions);
					return actions;
				}),
				onHide: (wasCancelled?: boolean) => {
					if (wasCancelled) {
						tree.DOMFocus();
					}
				},
				getActionsContext: () => element
			});

			return true;
		}

		return false;
	}

	protected getContext(element: any): any {
		return undefined;
	}
}

// call stack

export class CallStackController extends BaseDebugController {

	protected onLeftClick(tree: ITree, element: any, event: IMouseEvent): boolean {
		if (element instanceof ThreadAndProcessIds) {
			return this.showMoreStackFrames(tree, element);
		}
		if (element instanceof StackFrame) {
			this.focusStackFrame(element, event, event.detail !== 2);
		}

		return super.onLeftClick(tree, element, event);
	}

	protected getContext(element: any): any {
		if (element instanceof StackFrame) {
			if (element.source.inMemory) {
				return element.source.raw.path || element.source.reference;
			}

			return element.source.uri.toString();
		}
	}

	// user clicked / pressed on 'Load More Stack Frames', get those stack frames and refresh the tree.
	public showMoreStackFrames(tree: ITree, threadAndProcessIds: ThreadAndProcessIds): boolean {
		const process = this.debugService.getModel().getProcesses().filter(p => p.getId() === threadAndProcessIds.processId).pop();
		const thread = process && process.getThread(threadAndProcessIds.threadId);
		if (thread) {
			(<Thread>thread).fetchCallStack(true)
				.done(() => tree.refresh(), errors.onUnexpectedError);
		}

		return true;
	}

	public focusStackFrame(stackFrame: debug.IStackFrame, event: IKeyboardEvent | IMouseEvent, preserveFocus: boolean): void {
		this.debugService.focusStackFrameAndEvaluate(stackFrame).then(() => {
			const sideBySide = (event && (event.ctrlKey || event.metaKey));
			return stackFrame.openInEditor(this.editorService, preserveFocus, sideBySide);
		}, errors.onUnexpectedError);
	}
}


export class CallStackActionProvider implements IActionProvider {

	constructor( @IInstantiationService private instantiationService: IInstantiationService, @debug.IDebugService private debugService: debug.IDebugService) {
		// noop
	}

	public hasActions(tree: ITree, element: any): boolean {
		return false;
	}

	public getActions(tree: ITree, element: any): TPromise<IAction[]> {
		return TPromise.as([]);
	}

	public hasSecondaryActions(tree: ITree, element: any): boolean {
		return element !== tree.getInput();
	}

	public getSecondaryActions(tree: ITree, element: any): TPromise<IAction[]> {
		const actions: IAction[] = [];
		if (element instanceof Process) {
			actions.push(this.instantiationService.createInstance(RestartAction, RestartAction.ID, RestartAction.LABEL));
			actions.push(this.instantiationService.createInstance(StopAction, StopAction.ID, StopAction.LABEL));
		} else if (element instanceof Thread) {
			const thread = <Thread>element;
			if (thread.stopped) {
				actions.push(this.instantiationService.createInstance(ContinueAction, ContinueAction.ID, ContinueAction.LABEL));
				actions.push(this.instantiationService.createInstance(StepOverAction, StepOverAction.ID, StepOverAction.LABEL));
				actions.push(this.instantiationService.createInstance(StepIntoAction, StepIntoAction.ID, StepIntoAction.LABEL));
				actions.push(this.instantiationService.createInstance(StepOutAction, StepOutAction.ID, StepOutAction.LABEL));
			} else {
				actions.push(this.instantiationService.createInstance(PauseAction, PauseAction.ID, PauseAction.LABEL));
			}
		} else if (element instanceof StackFrame) {
			if (element.thread.process.session.capabilities.supportsRestartFrame) {
				actions.push(this.instantiationService.createInstance(RestartFrameAction, RestartFrameAction.ID, RestartFrameAction.LABEL));
			}
			actions.push(new CopyStackTraceAction(CopyStackTraceAction.ID, CopyStackTraceAction.LABEL));
		}

		return TPromise.as(actions);
	}

	public getActionItem(tree: ITree, element: any, action: IAction): IActionItem {
		return null;
	}
}

export class CallStackDataSource implements IDataSource {

	public getId(tree: ITree, element: any): string {
		if (typeof element === 'string') {
			return element;
		}

		return element.getId();
	}

	public hasChildren(tree: ITree, element: any): boolean {
		return element instanceof Model || element instanceof Process || (element instanceof Thread && (<Thread>element).stopped);
	}

	public getChildren(tree: ITree, element: any): TPromise<any> {
		if (element instanceof Thread) {
			return this.getThreadChildren(element);
		}
		if (element instanceof Model) {
			return TPromise.as(element.getProcesses());
		}

		const process = <debug.IProcess>element;
		return TPromise.as(process.getAllThreads());
	}

	private getThreadChildren(thread: Thread): TPromise<any> {
		return thread.fetchCallStack().then((callStack: any[]) => {
			if (thread.stoppedDetails && thread.stoppedDetails.framesErrorMessage) {
				return callStack.concat([thread.stoppedDetails.framesErrorMessage]);
			}
			if (thread.stoppedDetails && thread.stoppedDetails.totalFrames > callStack.length) {
				return callStack.concat([new ThreadAndProcessIds(thread.process.getId(), thread.threadId)]);
			}

			return callStack;
		});
	}

	public getParent(tree: ITree, element: any): TPromise<any> {
		return TPromise.as(null);
	}
}

interface IThreadTemplateData {
	thread: HTMLElement;
	name: HTMLElement;
	state: HTMLElement;
	stateLabel: HTMLSpanElement;
}

interface IProcessTemplateData {
	process: HTMLElement;
	name: HTMLElement;
	state: HTMLElement;
	stateLabel: HTMLSpanElement;
}

interface IErrorTemplateData {
	label: HTMLElement;
}

interface ILoadMoreTemplateData {
	label: HTMLElement;
}

interface IStackFrameTemplateData {
	stackFrame: HTMLElement;
	label: HTMLElement;
	file: HTMLElement;
	fileName: HTMLElement;
	lineNumber: HTMLElement;
}

export class CallStackRenderer implements IRenderer {

	private static THREAD_TEMPLATE_ID = 'thread';
	private static STACK_FRAME_TEMPLATE_ID = 'stackFrame';
	private static ERROR_TEMPLATE_ID = 'error';
	private static LOAD_MORE_TEMPLATE_ID = 'loadMore';
	private static PROCESS_TEMPLATE_ID = 'process';

	constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService) {
		// noop
	}

	public getHeight(tree: ITree, element: any): number {
		return 22;
	}

	public getTemplateId(tree: ITree, element: any): string {
		if (element instanceof Process) {
			return CallStackRenderer.PROCESS_TEMPLATE_ID;
		}
		if (element instanceof Thread) {
			return CallStackRenderer.THREAD_TEMPLATE_ID;
		}
		if (element instanceof StackFrame) {
			return CallStackRenderer.STACK_FRAME_TEMPLATE_ID;
		}
		if (typeof element === 'string') {
			return CallStackRenderer.ERROR_TEMPLATE_ID;
		}

		return CallStackRenderer.LOAD_MORE_TEMPLATE_ID;
	}

	public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any {
		if (templateId === CallStackRenderer.PROCESS_TEMPLATE_ID) {
			let data: IProcessTemplateData = Object.create(null);
			data.process = dom.append(container, $('.process'));
			data.name = dom.append(data.process, $('.name'));
			data.state = dom.append(data.process, $('.state'));
			data.stateLabel = dom.append(data.state, $('span.label'));

			return data;
		}

		if (templateId === CallStackRenderer.LOAD_MORE_TEMPLATE_ID) {
			let data: ILoadMoreTemplateData = Object.create(null);
			data.label = dom.append(container, $('.load-more'));

			return data;
		}
		if (templateId === CallStackRenderer.ERROR_TEMPLATE_ID) {
			let data: ILoadMoreTemplateData = Object.create(null);
			data.label = dom.append(container, $('.error'));

			return data;
		}
		if (templateId === CallStackRenderer.THREAD_TEMPLATE_ID) {
			let data: IThreadTemplateData = Object.create(null);
			data.thread = dom.append(container, $('.thread'));
			data.name = dom.append(data.thread, $('.name'));
			data.state = dom.append(data.thread, $('.state'));
			data.stateLabel = dom.append(data.state, $('span.label'));

			return data;
		}

		let data: IStackFrameTemplateData = Object.create(null);
		data.stackFrame = dom.append(container, $('.stack-frame'));
		data.label = dom.append(data.stackFrame, $('span.label.expression'));
		data.file = dom.append(data.stackFrame, $('.file'));
		data.fileName = dom.append(data.file, $('span.file-name'));
		data.lineNumber = dom.append(data.file, $('span.line-number'));

		return data;
	}

	public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void {
		if (templateId === CallStackRenderer.PROCESS_TEMPLATE_ID) {
			this.renderProcess(element, templateData);
		} else if (templateId === CallStackRenderer.THREAD_TEMPLATE_ID) {
			this.renderThread(element, templateData);
		} else if (templateId === CallStackRenderer.STACK_FRAME_TEMPLATE_ID) {
			this.renderStackFrame(element, templateData);
		} else if (templateId === CallStackRenderer.ERROR_TEMPLATE_ID) {
			this.renderError(element, templateData);
		} else if (templateId === CallStackRenderer.LOAD_MORE_TEMPLATE_ID) {
			this.renderLoadMore(element, templateData);
		}
	}

	private renderProcess(process: debug.IProcess, data: IProcessTemplateData): void {
		data.process.title = nls.localize({ key: 'process', comment: ['Process is a noun'] }, "Process");
		data.name.textContent = process.name;
		const stoppedThread = process.getAllThreads().filter(t => t.stopped).pop();

		data.stateLabel.textContent = stoppedThread ? nls.localize('paused', "Paused")
			: nls.localize({ key: 'running', comment: ['indicates state'] }, "Running");
	}

	private renderThread(thread: debug.IThread, data: IThreadTemplateData): void {
		data.thread.title = nls.localize('thread', "Thread");
		data.name.textContent = thread.name;

		if (thread.stopped) {
			data.stateLabel.textContent = thread.stoppedDetails.description || nls.localize({ key: 'pausedOn', comment: ['indicates reason for program being paused'] }, "Paused on {0}", thread.stoppedDetails.reason);
		} else {
			data.stateLabel.textContent = nls.localize({ key: 'running', comment: ['indicates state'] }, "Running");
		}
	}

	private renderError(element: string, data: IErrorTemplateData) {
		data.label.textContent = element;
		data.label.title = element;
	}

	private renderLoadMore(element: any, data: ILoadMoreTemplateData): void {
		data.label.textContent = nls.localize('loadMoreStackFrames', "Load More Stack Frames");
	}

	private renderStackFrame(stackFrame: debug.IStackFrame, data: IStackFrameTemplateData): void {
		stackFrame.source.presenationHint === 'deemphasize' ? dom.addClass(data.stackFrame, 'disabled') : dom.removeClass(data.stackFrame, 'disabled');
		stackFrame.source.presenationHint === 'label' ? dom.addClass(data.stackFrame, 'label') : dom.removeClass(data.stackFrame, 'label');
		data.file.title = stackFrame.source.raw.path || stackFrame.source.name;
		if (stackFrame.source.raw.origin) {
			data.file.title += `\n${stackFrame.source.raw.origin}`;
		}
		data.label.textContent = stackFrame.name;
		data.label.title = stackFrame.name;
		data.fileName.textContent = getSourceName(stackFrame.source, this.contextService);
		if (stackFrame.lineNumber !== undefined) {
			data.lineNumber.textContent = `${stackFrame.lineNumber}`;
			if (stackFrame.column) {
				data.lineNumber.textContent += `:${stackFrame.column}`;
			}
			dom.removeClass(data.lineNumber, 'unavailable');
		} else {
			dom.addClass(data.lineNumber, 'unavailable');
		}
	}

	public disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
		// noop
	}
}

export class CallstackAccessibilityProvider implements IAccessibilityProvider {

	constructor( @IWorkspaceContextService private contextService: IWorkspaceContextService) {
		// noop
	}

	public getAriaLabel(tree: ITree, element: any): string {
		if (element instanceof Thread) {
			return nls.localize('threadAriaLabel', "Thread {0}, callstack, debug", (<Thread>element).name);
		}
		if (element instanceof StackFrame) {
			return nls.localize('stackFrameAriaLabel', "Stack Frame {0} line {1} {2}, callstack, debug", (<StackFrame>element).name, (<StackFrame>element).lineNumber, getSourceName((<StackFrame>element).source, this.contextService));
		}

		return null;
{'fraction_non_alphanumeric': 0.13295436601926416, 'fraction_numerical': 0.0006767273465520742, 'mean_word_length': 5.088724076363137, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 65, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2141106', 'n_tokens_mistral': 14153, 'n_tokens_neox': 13105, 'n_words': 2822}