Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adopt input box for port naming #85766

Merged
merged 2 commits into from
Nov 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/vs/workbench/browser/parts/views/media/views.css
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@
padding-left: 3px;
}

.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .monaco-inputbox {
line-height: normal;
flex: 1;
}

.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .custom-view-tree-node-item-resourceLabel {
flex: 1;
text-overflow: ellipsis;
Expand Down
7 changes: 7 additions & 0 deletions src/vs/workbench/common/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,10 @@ export interface ITreeViewDataProvider {
getChildren(element?: ITreeItem): Promise<ITreeItem[]>;

}

export interface IEditableData {
validationMessage: (value: string) => string | null;
placeholder?: string | null;
startingValue?: string | null;
onFinish: (value: string, success: boolean) => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSou
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration, IExplorerService, IEditableData } from 'vs/workbench/contrib/files/common/files';
import { IFilesConfiguration, IExplorerService } from 'vs/workbench/contrib/files/common/files';
import { dirname, joinPath, isEqualOrParent, basename, hasToIgnoreCase, distinctParents } from 'vs/base/common/resources';
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { localize } from 'vs/nls';
Expand Down Expand Up @@ -54,6 +54,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { ILabelService } from 'vs/platform/label/common/label';
import { isNumber } from 'vs/base/common/types';
import { domEvent } from 'vs/base/browser/event';
import { IEditableData } from 'vs/workbench/common/views';

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

Expand Down
3 changes: 2 additions & 1 deletion src/vs/workbench/contrib/files/common/explorerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { Event, Emitter } from 'vs/base/common/event';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IExplorerService, IEditableData, IFilesConfiguration, SortOrder, SortOrderConfiguration, IContextProvider } from 'vs/workbench/contrib/files/common/files';
import { IExplorerService, IFilesConfiguration, SortOrder, SortOrderConfiguration, IContextProvider } from 'vs/workbench/contrib/files/common/files';
import { ExplorerItem, ExplorerModel } from 'vs/workbench/contrib/files/common/explorerModel';
import { URI } from 'vs/base/common/uri';
import { FileOperationEvent, FileOperation, IFileStat, IFileService, FileChangesEvent, FILES_EXCLUDE_CONFIG, FileChangeType, IResolveFileOptions } from 'vs/platform/files/common/files';
Expand All @@ -18,6 +18,7 @@ import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/co
import { IExpression } from 'vs/base/common/glob';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditableData } from 'vs/workbench/common/views';

function getFileEventsExcludes(configurationService: IConfigurationService, root?: URI): IExpression {
const scope = root ? { resource: root } : undefined;
Expand Down
7 changes: 1 addition & 6 deletions src/vs/workbench/contrib/files/common/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { IModeService, ILanguageSelection } from 'vs/editor/common/services/mode
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys';
import { Registry } from 'vs/platform/registry/common/platform';
import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer } from 'vs/workbench/common/views';
import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer, IEditableData } from 'vs/workbench/common/views';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel';
Expand All @@ -35,11 +35,6 @@ export const VIEWLET_ID = 'workbench.view.explorer';
*/
export const VIEW_CONTAINER: ViewContainer = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID);

export interface IEditableData {
validationMessage: (value: string) => string | null;
onFinish: (value: string, success: boolean) => void;
}

export interface IExplorerService {
_serviceBrand: undefined;
readonly roots: ExplorerItem[];
Expand Down
162 changes: 132 additions & 30 deletions src/vs/workbench/contrib/remote/browser/tunnelView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import 'vs/css!./media/tunnelView';
import * as nls from 'vs/nls';
import * as dom from 'vs/base/browser/dom';
import { IViewDescriptor } from 'vs/workbench/common/views';
import { IViewDescriptor, IEditableData } from 'vs/workbench/common/views';
import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IContextKeyService, IContextKey, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
Expand All @@ -20,7 +20,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { Disposable, IDisposable, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose } from 'vs/base/common/lifecycle';
import { ViewletPane, IViewletPaneOptions } from 'vs/workbench/browser/parts/views/paneViewlet';
import { ActionBar, ActionViewItem, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
Expand All @@ -31,6 +31,12 @@ import { IRemoteExplorerService, TunnelModel } from 'vs/workbench/services/remot
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { URI } from 'vs/workbench/workbench.web.api';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { once } from 'vs/base/common/functional';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';

class TunnelTreeVirtualDelegate implements IListVirtualDelegate<ITunnelItem> {
getHeight(element: ITunnelItem): number {
Expand Down Expand Up @@ -142,7 +148,10 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
private readonly viewId: string,
@IMenuService private readonly menuService: IMenuService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IInstantiationService private readonly instantiationService: IInstantiationService
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService,
@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService
) {
super();
}
Expand Down Expand Up @@ -181,30 +190,99 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer<ITunnelGrou
renderElement(element: ITreeNode<ITunnelGroup | ITunnelItem, ITunnelGroup | ITunnelItem>, index: number, templateData: ITunnelTemplateData): void {
templateData.elementDisposable.dispose();
const node = element.element;

// reset
templateData.actionBar.clear();
if (this.isTunnelItem(node)) {
templateData.iconLabel.setLabel(node.label, node.description, { title: node.label + ' - ' + node.description, extraClasses: ['tunnel-view-label'] });
templateData.actionBar.context = node;
const contextKeyService = this.contextKeyService.createScoped();
contextKeyService.createKey('view', this.viewId);
contextKeyService.createKey('tunnelType', node.tunnelType);
contextKeyService.createKey('tunnelCloseable', node.closeable);
const menu = this.menuService.createMenu(MenuId.TunnelInline, contextKeyService);
this._register(menu);
const actions: IAction[] = [];
this._register(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions));
if (actions) {
templateData.actionBar.push(actions, { icon: true, label: false });
if (this._actionRunner) {
templateData.actionBar.actionRunner = this._actionRunner;
}

const editableData = this.remoteExplorerService.getEditableData(node.remote);
if (editableData) {
templateData.iconLabel.element.style.display = 'none';
this.renderInputBox(templateData.container, node, editableData);
} else {
templateData.iconLabel.element.style.display = 'flex';
this.renderTunnel(node, templateData);
}
} else {
templateData.iconLabel.setLabel(node.label);
}
}

private renderTunnel(node: ITunnelItem, templateData: ITunnelTemplateData) {
templateData.iconLabel.setLabel(node.label, node.description, { title: node.label + ' - ' + node.description, extraClasses: ['tunnel-view-label'] });
templateData.actionBar.context = node;
const contextKeyService = this.contextKeyService.createScoped();
contextKeyService.createKey('view', this.viewId);
contextKeyService.createKey('tunnelType', node.tunnelType);
contextKeyService.createKey('tunnelCloseable', node.closeable);
const menu = this.menuService.createMenu(MenuId.TunnelInline, contextKeyService);
this._register(menu);
const actions: IAction[] = [];
this._register(createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, actions));
if (actions) {
templateData.actionBar.push(actions, { icon: true, label: false });
if (this._actionRunner) {
templateData.actionBar.actionRunner = this._actionRunner;
}
}
}

private renderInputBox(container: HTMLElement, item: ITunnelItem, editableData: IEditableData): IDisposable {
const value = editableData.startingValue || '';
const inputBox = new InputBox(container, this.contextViewService, {
ariaLabel: nls.localize('remote.tunnelsView.input', "Press Enter to confirm or Escape to cancel."),
validationOptions: {
validation: (value) => {
const content = editableData.validationMessage(value);
if (!content) {
return null;
}

return {
content,
formatContent: true,
type: MessageType.ERROR
};
}
},
placeholder: editableData.placeholder || ''
});
const styler = attachInputBoxStyler(inputBox, this.themeService);

inputBox.value = value;
inputBox.focus();

const done = once((success: boolean, finishEditing: boolean) => {
inputBox.element.style.display = 'none';
const value = inputBox.value;
dispose(toDispose);
if (finishEditing) {
editableData.onFinish(value, success);
}
});

const toDispose = [
inputBox,
dom.addStandardDisposableListener(inputBox.inputElement, dom.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
if (e.equals(KeyCode.Enter)) {
if (inputBox.validate()) {
done(true, true);
}
} else if (e.equals(KeyCode.Escape)) {
done(false, true);
}
}),
dom.addDisposableListener(inputBox.inputElement, dom.EventType.BLUR, () => {
done(inputBox.isInputValid(), true);
}),
styler
];

return toDisposable(() => {
done(false, false);
});
}

disposeElement(resource: ITreeNode<ITunnelGroup | ITunnelItem, ITunnelGroup | ITunnelItem>, index: number, templateData: ITunnelTemplateData): void {
templateData.elementDisposable.dispose();
}
Expand Down Expand Up @@ -321,7 +399,10 @@ export class TunnelPanel extends ViewletPane {
@IQuickInputService protected quickInputService: IQuickInputService,
@ICommandService protected commandService: ICommandService,
@IMenuService private readonly menuService: IMenuService,
@INotificationService private readonly notificationService: INotificationService
@INotificationService private readonly notificationService: INotificationService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService,
@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService);
this.tunnelTypeContext = TunnelTypeContextKey.bindTo(contextKeyService);
Expand Down Expand Up @@ -353,7 +434,7 @@ export class TunnelPanel extends ViewletPane {
dom.addClass(treeContainer, 'file-icon-themable-tree');
dom.addClass(treeContainer, 'show-file-icons');
container.appendChild(treeContainer);
const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService);
const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService);
this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree,
'RemoteTunnels',
treeContainer,
Expand Down Expand Up @@ -382,13 +463,29 @@ export class TunnelPanel extends ViewletPane {
this.tree.updateChildren(undefined, true);
}));

const helpItemNavigator = this._register(new TreeResourceNavigator2(this.tree, { openOnFocus: false, openOnSelection: false }));
const navigator = this._register(new TreeResourceNavigator2(this.tree, { openOnFocus: false, openOnSelection: false }));

this._register(Event.debounce(helpItemNavigator.onDidOpenResource, (last, event) => event, 75, true)(e => {
if (e.element.tunnelType === TunnelType.Add) {
this._register(Event.debounce(navigator.onDidOpenResource, (last, event) => event, 75, true)(e => {
if (e.element && (e.element.tunnelType === TunnelType.Add)) {
this.commandService.executeCommand(ForwardPortAction.ID);
}
}));

this._register(this.remoteExplorerService.onDidChangeEditable(async e => {
const isEditing = !!this.remoteExplorerService.getEditableData(e);

if (!isEditing) {
dom.removeClass(treeContainer, 'highlight');
}

await this.tree.updateChildren(undefined, false);

if (isEditing) {
dom.addClass(treeContainer, 'highlight');
} else {
this.tree.domFocus();
}
}));
}

private get contributedContextMenu(): IMenu {
Expand Down Expand Up @@ -471,12 +568,17 @@ namespace NameTunnelAction {
return async (accessor, arg) => {
if (arg instanceof TunnelItem) {
const remoteExplorerService = accessor.get(IRemoteExplorerService);
const quickInputService = accessor.get(IQuickInputService);
const name = await quickInputService.input({ placeHolder: nls.localize('remote.tunnelView.pickName', 'Port name, or leave blank for no name') });
if (name === undefined) {
return;
}
remoteExplorerService.tunnelModel.name(arg.remote, name);
remoteExplorerService.setEditable(arg.remote, {
onFinish: (value, success) => {
if (success) {
remoteExplorerService.tunnelModel.name(arg.remote, value);
}
remoteExplorerService.setEditable(arg.remote, null);
},
validationMessage: () => null,
placeholder: nls.localize('remote.tunnelsView.namePlaceholder', "Name port"),
startingValue: arg.name
});
}
return;
};
Expand Down
24 changes: 22 additions & 2 deletions src/vs/workbench/services/remote/common/remoteExplorerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/e
import { URI } from 'vs/base/common/uri';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { Disposable } from 'vs/base/common/lifecycle';
import { IEditableData } from 'vs/workbench/common/views';

export const IRemoteExplorerService = createDecorator<IRemoteExplorerService>('remoteExplorerService');
export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType';
Expand Down Expand Up @@ -115,6 +116,9 @@ export interface IRemoteExplorerService {
targetType: string;
readonly helpInformation: HelpInformation[];
readonly tunnelModel: TunnelModel;
onDidChangeEditable: Event<number>;
setEditable(remote: number, data: IEditableData | null): void;
getEditableData(remote: number): IEditableData | undefined;
}

export interface HelpInformation {
Expand Down Expand Up @@ -155,10 +159,13 @@ const remoteHelpExtPoint = ExtensionsRegistry.registerExtensionPoint<HelpInforma
class RemoteExplorerService implements IRemoteExplorerService {
public _serviceBrand: undefined;
private _targetType: string = '';
private _onDidChangeTargetType: Emitter<string> = new Emitter<string>();
public onDidChangeTargetType: Event<string> = this._onDidChangeTargetType.event;
private readonly _onDidChangeTargetType: Emitter<string> = new Emitter<string>();
public readonly onDidChangeTargetType: Event<string> = this._onDidChangeTargetType.event;
private _helpInformation: HelpInformation[] = [];
private _tunnelModel: TunnelModel;
private editable: { remote: number, data: IEditableData } | undefined;
private readonly _onDidChangeEditable: Emitter<number> = new Emitter<number>();
public readonly onDidChangeEditable: Event<number> = this._onDidChangeEditable.event;

constructor(
@IStorageService private readonly storageService: IStorageService,
Expand Down Expand Up @@ -212,6 +219,19 @@ class RemoteExplorerService implements IRemoteExplorerService {
get tunnelModel(): TunnelModel {
return this._tunnelModel;
}

setEditable(remote: number, data: IEditableData | null): void {
if (!data) {
this.editable = undefined;
} else {
this.editable = { remote, data };
}
this._onDidChangeEditable.fire(remote);
}

getEditableData(remote: number): IEditableData | undefined {
return this.editable && this.editable.remote === remote ? this.editable.data : undefined;
}
}

registerSingleton(IRemoteExplorerService, RemoteExplorerService, true);