diff --git a/src/vs/workbench/parts/codeEditor/browser/media/suggestEnabledInput.css b/src/vs/workbench/parts/codeEditor/browser/media/suggestEnabledInput.css new file mode 100644 index 0000000000000..4cce699dee82b --- /dev/null +++ b/src/vs/workbench/parts/codeEditor/browser/media/suggestEnabledInput.css @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.suggest-input-container { + padding: 3px 4px 5px; +} + +.suggest-input-container .monaco-editor-background, +.suggest-input-container .monaco-editor, +.suggest-input-container .mtk1 { + /* allow the embedded monaco to be styled from the outer context */ + background-color: inherit; + color: inherit; +} + +.suggest-input-container .suggest-input-placeholder { + position: absolute; + z-index: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + pointer-events: none; + margin-top: 2px; + margin-left: 1px; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/menuPreventer.ts b/src/vs/workbench/parts/codeEditor/browser/menuPreventer.ts similarity index 100% rename from src/vs/workbench/parts/codeEditor/electron-browser/menuPreventer.ts rename to src/vs/workbench/parts/codeEditor/browser/menuPreventer.ts diff --git a/src/vs/workbench/parts/codeEditor/browser/simpleEditorOptions.ts b/src/vs/workbench/parts/codeEditor/browser/simpleEditorOptions.ts new file mode 100644 index 0000000000000..a18c1f0479d9d --- /dev/null +++ b/src/vs/workbench/parts/codeEditor/browser/simpleEditorOptions.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; + +export function getSimpleEditorOptions(): IEditorOptions { + return { + wordWrap: 'on', + overviewRulerLanes: 0, + glyphMargin: false, + lineNumbers: 'off', + folding: false, + selectOnLineNumbers: false, + hideCursorInOverviewRuler: true, + selectionHighlight: false, + scrollbar: { + horizontal: 'hidden' + }, + lineDecorationsWidth: 0, + overviewRulerBorder: false, + scrollBeyondLastLine: false, + renderLineHighlight: 'none', + fixedOverflowWidgets: true, + acceptSuggestionOnEnter: 'smart', + minimap: { + enabled: false + } + }; +} diff --git a/src/vs/workbench/parts/codeEditor/browser/suggestEnabledInput.ts b/src/vs/workbench/parts/codeEditor/browser/suggestEnabledInput.ts new file mode 100644 index 0000000000000..d85455c40dcca --- /dev/null +++ b/src/vs/workbench/parts/codeEditor/browser/suggestEnabledInput.ts @@ -0,0 +1,232 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./media/suggestEnabledInput'; +import { $, addClass, append, removeClass, Dimension } from 'vs/base/browser/dom'; +import { chain, Emitter, Event } from 'vs/base/common/event'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { isMacintosh } from 'vs/base/common/platform'; +import uri from 'vs/base/common/uri'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import * as modes from 'vs/editor/common/modes'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { inputBackground, inputBorder, inputForeground, inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { Component } from 'vs/workbench/common/component'; +import { MenuPreventer } from 'vs/workbench/parts/codeEditor/browser/menuPreventer'; +import { getSimpleEditorOptions } from 'vs/workbench/parts/codeEditor/browser/simpleEditorOptions'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextModel } from 'vs/editor/common/model'; +import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; + +interface SuggestResultsProvider { + /** + * Provider function for suggestion results. + * + * @param query the full text of the input. + */ + provideResults: (query: string) => string[]; + + /** + * Trigger characters for this input. Suggestions will appear when one of these is typed, + * or upon `ctrl+space` triggering at a word boundary. + * + * Defaults to the empty array. + */ + triggerCharacters?: string[]; + + /** + * Defines the sorting function used when showing results. + * + * Defaults to the identity function. + */ + sortKey?: (result: string) => string; +} + +interface SuggestEnabledInputOptions { + /** + * The text to show when no input is present. + * + * Defaults to the empty string. + */ + placeholderText?: string; + + /** + * Context key tracking the focus state of this element + */ + focusContextKey?: IContextKey; +} + +export class SuggestEnabledInput extends Component { + + private _onShouldFocusResults = new Emitter(); + readonly onShouldFocusResults: Event = this._onShouldFocusResults.event; + + private _onEnter = new Emitter(); + readonly onEnter: Event = this._onEnter.event; + + private _onInputDidChange = new Emitter(); + readonly onInputDidChange: Event = this._onInputDidChange.event; + + private disposables: IDisposable[] = []; + private inputWidget: CodeEditorWidget; + private stylingContainer: HTMLDivElement; + private placeholderText: HTMLDivElement; + + constructor( + id: string, + parent: HTMLElement, + suggestionProvider: SuggestResultsProvider, + ariaLabel: string, + resourceHandle: string, + options: SuggestEnabledInputOptions, + @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService modelService: IModelService, + ) { + super(id, themeService); + + this.stylingContainer = append(parent, $('.suggest-input-container')); + this.placeholderText = append(this.stylingContainer, $('.suggest-input-placeholder', null, options.placeholderText || '')); + + this.inputWidget = instantiationService.createInstance(CodeEditorWidget, this.stylingContainer, + mixinHTMLInputStyleOptions(getSimpleEditorOptions(), ariaLabel), + { + contributions: [SuggestController, SnippetController2, ContextMenuController, MenuPreventer], + isSimpleWidget: true, + }); + + let scopeHandle = uri.parse(resourceHandle); + this.inputWidget.setModel(modelService.createModel('', null, scopeHandle, true)); + + this.disposables.push(this.inputWidget.onDidPaste(() => this.setValue(this.getValue()))); // setter cleanses + + this.disposables.push((this.inputWidget.onDidFocusEditorText(() => { + if (options.focusContextKey) { options.focusContextKey.set(true); } + addClass(this.stylingContainer, 'synthetic-focus'); + }))); + this.disposables.push((this.inputWidget.onDidBlurEditorText(() => { + if (options.focusContextKey) { options.focusContextKey.set(false); } + removeClass(this.stylingContainer, 'synthetic-focus'); + }))); + + const onKeyDownMonaco = chain(this.inputWidget.onKeyDown); + onKeyDownMonaco.filter(e => e.keyCode === KeyCode.Enter).on(e => { e.preventDefault(); this._onEnter.fire(); }, this, this.disposables); + onKeyDownMonaco.filter(e => e.keyCode === KeyCode.DownArrow && (isMacintosh ? e.metaKey : e.ctrlKey)).on(() => this._onShouldFocusResults.fire(), this, this.disposables); + + let preexistingContent = this.getValue(); + this.disposables.push(this.inputWidget.getModel().onDidChangeContent(() => { + let content = this.getValue(); + this.placeholderText.style.visibility = content ? 'hidden' : 'visible'; + if (preexistingContent.trim() === content.trim()) { return; } + this._onInputDidChange.fire(); + preexistingContent = content; + })); + + let validatedSuggestProvider = { + provideResults: suggestionProvider.provideResults, + sortKey: suggestionProvider.sortKey || (a => a), + triggerCharacters: suggestionProvider.triggerCharacters || [] + }; + + this.disposables.push(modes.SuggestRegistry.register({ scheme: scopeHandle.scheme, pattern: '**/' + scopeHandle.path, hasAccessToAllModels: true }, { + triggerCharacters: validatedSuggestProvider.triggerCharacters, + provideCompletionItems: (model: ITextModel, position: Position, _context: modes.SuggestContext) => { + let query = model.getValue(); + + let wordStart = query.lastIndexOf(' ', position.column - 1) + 1; + let alreadyTypedCount = position.column - wordStart - 1; + + // dont show suggestions if the user has typed something, but hasn't used the trigger character + if (alreadyTypedCount > 0 && (validatedSuggestProvider.triggerCharacters).indexOf(query[wordStart]) === -1) { return { suggestions: [] }; } + + return { + suggestions: suggestionProvider.provideResults(query).map(result => { + return { + label: result, + insertText: result, + overwriteBefore: alreadyTypedCount, + sortText: validatedSuggestProvider.sortKey(result), + type: 'keyword' + }; + }) + }; + } + })); + } + + public setValue(val: string) { + val = val.replace(/\s/g, ' '); + this.inputWidget.setValue(val); + this.inputWidget.setScrollTop(0); + this.inputWidget.setPosition(new Position(1, val.length + 1)); + } + + public getValue(): string { + return this.inputWidget.getValue(); + } + + + public updateStyles(): void { + super.updateStyles(); + + this.stylingContainer.style.backgroundColor = this.getColor(inputBackground); + this.stylingContainer.style.color = this.getColor(inputForeground); + this.placeholderText.style.color = this.getColor(inputPlaceholderForeground); + + const inputBorderColor = this.getColor(inputBorder); + this.stylingContainer.style.borderWidth = inputBorderColor ? '1px' : null; + this.stylingContainer.style.borderStyle = inputBorderColor ? 'solid' : null; + this.stylingContainer.style.borderColor = inputBorderColor; + + let cursor = this.stylingContainer.getElementsByClassName('cursor')[0] as HTMLDivElement; + if (cursor) { + cursor.style.backgroundColor = this.getColor(inputForeground); + } + } + + public focus(): void { + this.inputWidget.focus(); + } + + public layout(dimension: Dimension): void { + this.inputWidget.layout(dimension); + this.placeholderText.style.width = `${dimension.width}px`; + } + + public selectAll(): void { + this.inputWidget.setSelection(new Range(1, 1, 1, this.getValue().length + 1)); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + super.dispose(); + } +} + + +function mixinHTMLInputStyleOptions(config: IEditorOptions, ariaLabel?: string): IEditorOptions { + config.fontSize = 13; + config.lineHeight = 22; + config.wordWrap = 'off'; + config.scrollbar.vertical = 'hidden'; + config.roundedSelection = false; + config.ariaLabel = ariaLabel || ''; + config.renderIndentGuides = false; + config.cursorWidth = 1; + config.snippetSuggestions = 'none'; + config.suggest = { filterGraceful: false }; + config.fontFamily = ' -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif'; + return config; +} diff --git a/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts b/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts index cc4c07f981a2a..d66877173a11e 100644 --- a/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts +++ b/src/vs/workbench/parts/codeEditor/codeEditor.contribution.ts @@ -6,7 +6,7 @@ import './electron-browser/accessibility'; import './electron-browser/inspectKeybindings'; import './electron-browser/largeFileOptimizations'; -import './electron-browser/menuPreventer'; +import './browser/menuPreventer'; import './electron-browser/selectionClipboard'; import './electron-browser/textMate/inspectTMScopes'; import './electron-browser/toggleMinimap'; diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions.ts b/src/vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions.ts index 809dbd0584d60..4e3a0f1a14ca2 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; -import { MenuPreventer } from 'vs/workbench/parts/codeEditor/electron-browser/menuPreventer'; +import { MenuPreventer } from 'vs/workbench/parts/codeEditor/browser/menuPreventer'; import { SelectionClipboard } from 'vs/workbench/parts/codeEditor/electron-browser/selectionClipboard'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; @@ -24,29 +23,4 @@ export function getSimpleCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { TabCompletionController, ] }; -} - -export function getSimpleEditorOptions(): IEditorOptions { - return { - wordWrap: 'on', - overviewRulerLanes: 0, - glyphMargin: false, - lineNumbers: 'off', - folding: false, - selectOnLineNumbers: false, - hideCursorInOverviewRuler: true, - selectionHighlight: false, - scrollbar: { - horizontal: 'hidden' - }, - lineDecorationsWidth: 0, - overviewRulerBorder: false, - scrollBeyondLastLine: false, - renderLineHighlight: 'none', - fixedOverflowWidgets: true, - acceptSuggestionOnEnter: 'smart', - minimap: { - enabled: false - } - }; -} +} \ No newline at end of file diff --git a/src/vs/workbench/parts/comments/electron-browser/simpleCommentEditor.ts b/src/vs/workbench/parts/comments/electron-browser/simpleCommentEditor.ts index 3bbb410d3f528..a103105c6412e 100644 --- a/src/vs/workbench/parts/comments/electron-browser/simpleCommentEditor.ts +++ b/src/vs/workbench/parts/comments/electron-browser/simpleCommentEditor.ts @@ -12,7 +12,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ICommandService } from 'vs/platform/commands/common/commands'; // Allowed Editor Contributions: -import { MenuPreventer } from 'vs/workbench/parts/codeEditor/electron-browser/menuPreventer'; +import { MenuPreventer } from 'vs/workbench/parts/codeEditor/browser/menuPreventer'; import { SelectionClipboard } from 'vs/workbench/parts/codeEditor/electron-browser/selectionClipboard'; import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; diff --git a/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts b/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts index d9bc400ab101d..fe86cf067fdb8 100644 --- a/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts +++ b/src/vs/workbench/parts/debug/electron-browser/breakpointWidget.ts @@ -33,7 +33,8 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IDecorationOptions } from 'vs/editor/common/editorCommon'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { getSimpleEditorOptions, getSimpleCodeEditorWidgetOptions } from 'vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions'; +import { getSimpleCodeEditorWidgetOptions } from 'vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions'; +import { getSimpleEditorOptions } from 'vs/workbench/parts/codeEditor/browser/simpleEditorOptions'; const $ = dom.$; const IPrivateBreakpointWidgetService = createDecorator('privateBreakopintWidgetService'); diff --git a/src/vs/workbench/parts/debug/electron-browser/repl.ts b/src/vs/workbench/parts/debug/electron-browser/repl.ts index 453e622d41e39..68d06e337a44d 100644 --- a/src/vs/workbench/parts/debug/electron-browser/repl.ts +++ b/src/vs/workbench/parts/debug/electron-browser/repl.ts @@ -47,7 +47,8 @@ import { HistoryNavigator } from 'vs/base/common/history'; import { IHistoryNavigationWidget } from 'vs/base/browser/history'; import { createAndBindHistoryNavigationWidgetScopedContextKeyService } from 'vs/platform/widget/browser/contextScopedHistoryWidget'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { getSimpleEditorOptions, getSimpleCodeEditorWidgetOptions } from 'vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions'; +import { getSimpleCodeEditorWidgetOptions } from 'vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions'; +import { getSimpleEditorOptions } from 'vs/workbench/parts/codeEditor/browser/simpleEditorOptions'; const $ = dom.$; diff --git a/src/vs/workbench/parts/extensions/common/extensionQuery.ts b/src/vs/workbench/parts/extensions/common/extensionQuery.ts index b966d43802870..5f85a0ee0e022 100644 --- a/src/vs/workbench/parts/extensions/common/extensionQuery.ts +++ b/src/vs/workbench/parts/extensions/common/extensionQuery.ts @@ -12,7 +12,7 @@ export class Query { this.value = value.trim(); } - static autocompletions(query: string): string[] { + static suggestions(query: string): string[] { const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'recommended', 'sort', 'category', 'tag', 'ext']; const subcommands = { 'sort': ['installs', 'rating', 'name'], diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts index 334c02d10d753..307fae07cc06a 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -6,21 +6,18 @@ 'use strict'; import 'vs/css!./media/extensionsViewlet'; -import uri from 'vs/base/common/uri'; -import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; import { ThrottledDelayer, always } from 'vs/base/common/async'; import { TPromise } from 'vs/base/common/winjs.base'; import { isPromiseCanceledError, onUnexpectedError, create as createError } from 'vs/base/common/errors'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Event as EventOf, Emitter, chain } from 'vs/base/common/event'; +import { Event as EventOf, Emitter } from 'vs/base/common/event'; import { IAction } from 'vs/base/common/actions'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { KeyCode } from 'vs/base/common/keyCodes'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { append, $, addClass, removeClass, toggleClass, Dimension } from 'vs/base/browser/dom'; +import { append, $, addClass, toggleClass, Dimension } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -39,7 +36,6 @@ import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorG import Severity from 'vs/base/common/severity'; import { IActivityService, ProgressBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { inputForeground, inputBackground, inputBorder, inputPlaceholderForeground } from 'vs/platform/theme/common/colorRegistry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ViewsRegistry, IViewDescriptor } from 'vs/workbench/common/views'; import { ViewContainerViewlet, IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; @@ -59,18 +55,7 @@ import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/e import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { SingleServerExtensionManagementServerService } from 'vs/workbench/services/extensions/node/extensionManagementServerService'; import { Query } from 'vs/workbench/parts/extensions/common/extensionQuery'; -import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { Range } from 'vs/editor/common/core/range'; -import { Position } from 'vs/editor/common/core/position'; -import { ITextModel } from 'vs/editor/common/model'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { getSimpleEditorOptions } from 'vs/workbench/parts/codeEditor/electron-browser/simpleEditorOptions'; -import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; -import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu'; -import { MenuPreventer } from 'vs/workbench/parts/codeEditor/electron-browser/menuPreventer'; -import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; -import { isMacintosh } from 'vs/base/common/platform'; +import { SuggestEnabledInput } from 'vs/workbench/parts/codeEditor/browser/suggestEnabledInput'; interface SearchInputEvent extends Event { target: HTMLInputElement; @@ -265,14 +250,12 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio private searchDelayer: ThrottledDelayer; private root: HTMLElement; - private searchBox: CodeEditorWidget; + private searchBox: SuggestEnabledInput; private extensionsBox: HTMLElement; private primaryActions: IAction[]; private secondaryActions: IAction[]; private groupByServerAction: IAction; private disposables: IDisposable[] = []; - private monacoStyleContainer: HTMLDivElement; - private placeholderText: HTMLDivElement; constructor( @IPartService partService: IPartService, @@ -290,8 +273,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, @IExtensionService extensionService: IExtensionService, - @IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService, - @IModelService private modelService: IModelService, + @IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService ) { super(VIEWLET_ID, `${VIEWLET_ID}.state`, true, partService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); @@ -315,28 +297,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)); } }, this, this.disposables); - - modes.SuggestRegistry.register({ scheme: 'extensions', pattern: '**/searchinput', hasAccessToAllModels: true }, { - triggerCharacters: ['@'], - provideCompletionItems: (model: ITextModel, position: Position, _context: modes.SuggestContext) => { - const sortKey = (item: string) => { - if (item.indexOf(':') === -1) { return 'a'; } - else if (/ext:/.test(item) || /tag:/.test(item)) { return 'b'; } - else if (/sort:/.test(item)) { return 'c'; } - else { return 'd'; } - }; - return { - suggestions: this.autoComplete(model.getValue(), position.column).map(item => ( - { - label: item.fullText, - insertText: item.fullText, - overwriteBefore: item.overwrite, - sortText: sortKey(item.fullText), - type: 'keyword' - })) - }; - } - }); } create(parent: HTMLElement): TPromise { @@ -344,51 +304,32 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio this.root = parent; const header = append(this.root, $('.header')); - this.monacoStyleContainer = append(header, $('.monaco-container')); - this.searchBox = this.instantiationService.createInstance(CodeEditorWidget, this.monacoStyleContainer, - mixinHTMLInputStyleOptions(getSimpleEditorOptions(), localize('searchExtensions', "Search Extensions in Marketplace")), - { - isSimpleWidget: true, contributions: [ - SuggestController, - SnippetController2, - ContextMenuController, - MenuPreventer - ] - }); - this.placeholderText = append(this.monacoStyleContainer, $('.search-placeholder', null, localize('searchExtensions', "Search Extensions in Marketplace"))); + const placeholder = localize('searchExtensions', "Search Extensions in Marketplace"); - this.extensionsBox = append(this.root, $('.extensions')); - - this.searchBox.setModel(this.modelService.createModel('', null, uri.parse('extensions:searchinput'), true)); - - this.disposables.push(this.searchBox.onDidPaste(() => { - let trimmed = this.searchBox.getValue().replace(/\s+/g, ' '); - this.searchBox.setValue(trimmed); - this.searchBox.setScrollTop(0); - this.searchBox.setPosition(new Position(1, trimmed.length + 1)); - })); - - this.disposables.push(this.searchBox.onDidFocusEditorText(() => addClass(this.monacoStyleContainer, 'synthetic-focus'))); - this.disposables.push(this.searchBox.onDidBlurEditorText(() => removeClass(this.monacoStyleContainer, 'synthetic-focus'))); - - const onKeyDownMonaco = chain(this.searchBox.onKeyDown); - onKeyDownMonaco.filter(e => e.keyCode === KeyCode.Enter).on(e => e.preventDefault(), this, this.disposables); - onKeyDownMonaco.filter(e => e.keyCode === KeyCode.DownArrow && (isMacintosh ? e.metaKey : e.ctrlKey)).on(() => this.focusListView(), this, this.disposables); - - const searchChangeEvent = new Emitter(); - this.onSearchChange = searchChangeEvent.event; - - let existingContent = this.searchBox.getValue().trim(); - this.disposables.push(this.searchBox.getModel().onDidChangeContent(() => { - this.placeholderText.style.visibility = this.searchBox.getValue() ? 'hidden' : 'visible'; - let content = this.searchBox.getValue().trim(); - if (existingContent === content) { return; } + this.searchBox = this.instantiationService.createInstance(SuggestEnabledInput, `${VIEWLET_ID}.searchbox`, header, { + triggerCharacters: ['@'], + sortKey: item => { + if (item.indexOf(':') === -1) { return 'a'; } + else if (/ext:/.test(item) || /tag:/.test(item)) { return 'b'; } + else if (/sort:/.test(item)) { return 'c'; } + else { return 'd'; } + }, + provideResults: (query) => Query.suggestions(query) + }, placeholder, 'extensions:searchinput', { placeholderText: placeholder }); + + this.disposables.push(this.searchBox); + + const _searchChange = new Emitter(); + this.onSearchChange = _searchChange.event; + this.searchBox.onInputDidChange(() => { this.triggerSearch(); - searchChangeEvent.fire(content); - existingContent = content; - })); + _searchChange.fire(this.searchBox.getValue()); + }, this, this.disposables); + + this.searchBox.onShouldFocusResults(() => this.focusListView(), this, this.disposables); + this.extensionsBox = append(this.root, $('.extensions')); return super.create(this.extensionsBox) .then(() => this.extensionManagementService.getInstalled(LocalExtensionType.User)) .then(installed => { @@ -401,20 +342,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio public updateStyles(): void { super.updateStyles(); - - this.monacoStyleContainer.style.backgroundColor = this.getColor(inputBackground); - this.monacoStyleContainer.style.color = this.getColor(inputForeground); - this.placeholderText.style.color = this.getColor(inputPlaceholderForeground); - - const inputBorderColor = this.getColor(inputBorder); - this.monacoStyleContainer.style.borderWidth = inputBorderColor ? '1px' : null; - this.monacoStyleContainer.style.borderStyle = inputBorderColor ? 'solid' : null; - this.monacoStyleContainer.style.borderColor = inputBorderColor; - - let cursor = this.monacoStyleContainer.getElementsByClassName('cursor')[0] as HTMLDivElement; - if (cursor) { - cursor.style.backgroundColor = this.getColor(inputForeground); - } + this.searchBox.updateStyles(); } setVisible(visible: boolean): TPromise { @@ -423,7 +351,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio if (isVisibilityChanged) { if (visible) { this.searchBox.focus(); - this.searchBox.setSelection(new Range(1, 1, 1, this.searchBox.getValue().length + 1)); + this.searchBox.selectAll(); } } }); @@ -436,8 +364,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio layout(dimension: Dimension): void { toggleClass(this.root, 'narrow', dimension.width <= 300); this.searchBox.layout({ height: 20, width: dimension.width - 34 }); - this.placeholderText.style.width = '' + (dimension.width - 30) + 'px'; - super.layout(new Dimension(dimension.width, dimension.height - 38)); } @@ -493,7 +419,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio event.immediate = true; this.searchBox.setValue(value); - this.searchBox.setPosition(new Position(1, value.length + 1)); } private triggerSearch(immediate = false): void { @@ -501,7 +426,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio } private normalizedQuery(): string { - return (this.searchBox.getValue() || '').replace(/@category/g, 'category').replace(/@tag:/g, 'tag:').replace(/@ext:/g, 'ext:'); + return this.searchBox.getValue().replace(/@category/g, 'category').replace(/@tag:/g, 'tag:').replace(/@ext:/g, 'ext:'); } private doSearch(): TPromise { @@ -539,16 +464,6 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio return this.instantiationService.createInstance(viewDescriptor.ctor, options) as ViewletPanel; } - private autoComplete(query: string, position: number): { fullText: string, overwrite: number }[] { - let wordStart = query.lastIndexOf(' ', position - 1) + 1; - let alreadyTypedCount = position - wordStart - 1; - - // dont show autosuggestions if the user has typed something, but hasn't used the trigger character - if (alreadyTypedCount > 0 && query[wordStart] !== '@') { return []; } - - return Query.autocompletions(query).map(replacement => ({ fullText: replacement, overwrite: alreadyTypedCount })); - } - private count(): number { return this.panels.reduce((count, view) => (view).count() + count, 0); } @@ -690,18 +605,4 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution { dispose(): void { this.disposables = dispose(this.disposables); } -} - -function mixinHTMLInputStyleOptions(config: IEditorOptions, ariaLabel?: string): IEditorOptions { - config.fontSize = 13; - config.lineHeight = 22; - config.wordWrap = 'off'; - config.scrollbar.vertical = 'hidden'; - config.ariaLabel = ariaLabel || ''; - config.renderIndentGuides = false; - config.cursorWidth = 1; - config.snippetSuggestions = 'none'; - config.suggest = { filterGraceful: false }; - config.fontFamily = ' -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif'; - return config; -} +} \ No newline at end of file diff --git a/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css index 98e419e22e27d..e8e113386891b 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css +++ b/src/vs/workbench/parts/extensions/electron-browser/media/extensionsViewlet.css @@ -202,32 +202,6 @@ opacity: 0.9; } -.extensions-viewlet .header .monaco-container { - padding: 3px 4px 5px; -} - -.extensions-viewlet .header .monaco-container .suggest-widget { - width: 275px; -} - -.extensions-viewlet .header .monaco-container .monaco-editor-background, -.extensions-viewlet .header .monaco-container .monaco-editor, -.extensions-viewlet .header .monaco-container .mtk1 { - /* allow the embedded monaco to be styled from the outer context */ - background-color: inherit; - color: inherit; -} - -.extensions-viewlet .header .search-placeholder { - position: absolute; - z-index: 1; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - pointer-events: none; - margin-top: 2px; -} - .vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .bookmark, .vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .bookmark, .vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension > .icon, diff --git a/src/vs/workbench/parts/extensions/test/common/extensionQuery.test.ts b/src/vs/workbench/parts/extensions/test/common/extensionQuery.test.ts index a3ba946041fe8..f107018c53da9 100644 --- a/src/vs/workbench/parts/extensions/test/common/extensionQuery.test.ts +++ b/src/vs/workbench/parts/extensions/test/common/extensionQuery.test.ts @@ -142,10 +142,10 @@ suite('Extension query', () => { }); test('autocomplete', () => { - Query.autocompletions('@sort:in').some(x => x === '@sort:installs '); - Query.autocompletions('@sort:installs').every(x => x !== '@sort:rating '); + Query.suggestions('@sort:in').some(x => x === '@sort:installs '); + Query.suggestions('@sort:installs').every(x => x !== '@sort:rating '); - Query.autocompletions('@category:blah').some(x => x === '@category:"extension packs" '); - Query.autocompletions('@category:"extension packs"').every(x => x !== '@category:formatters '); + Query.suggestions('@category:blah').some(x => x === '@category:"extension packs" '); + Query.suggestions('@category:"extension packs"').every(x => x !== '@category:formatters '); }); }); \ No newline at end of file diff --git a/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css index 19651c227136d..055c1e1be764c 100644 --- a/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/parts/preferences/browser/media/settingsEditor2.css @@ -39,24 +39,6 @@ margin-right: 7px; } -.settings-editor > .settings-header > .search-container { - position: relative; -} - -.settings-editor > .settings-header .search-container > .settings-search-input { - vertical-align: middle; -} - -.settings-editor > .settings-header .search-container > .settings-search-input > .monaco-inputbox { - height: 30px; - width: 100%; -} - -.settings-editor > .settings-header .search-container .settings-search-input > .monaco-inputbox .input { - font-size: 14px; - padding-left: 7px; -} - .settings-editor > .settings-header > .settings-header-controls { height: 32px; display: flex; diff --git a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts index 27c14ebd24a73..8eb1dbb281cd2 100644 --- a/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/parts/preferences/browser/settingsEditor2.ts @@ -31,7 +31,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorOptions, IEditor } from 'vs/workbench/common/editor'; import { PreferencesEditor } from 'vs/workbench/parts/preferences/browser/preferencesEditor'; -import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; +import { SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; import { commonlyUsedData, tocData } from 'vs/workbench/parts/preferences/browser/settingsLayout'; import { ISettingsEditorViewState, MODIFIED_SETTING_TAG, ONLINE_SERVICES_SETTING_TAG, resolveExtensionsSettings, resolveSettingsTree, SearchResultIdx, SearchResultModel, SettingsRenderer, SettingsTree, SettingsTreeElement, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/parts/preferences/browser/settingsTree'; import { TOCRenderer, TOCTree, TOCTreeModel } from 'vs/workbench/parts/preferences/browser/tocTree'; @@ -39,6 +39,8 @@ import { CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_FIRST_ROW_FOCUS, CONTEXT_SETT import { IPreferencesService, ISearchResult, ISettingsEditorModel } from 'vs/workbench/services/preferences/common/preferences'; import { SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { DefaultSettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; +import { SuggestEnabledInput } from 'vs/workbench/parts/codeEditor/browser/suggestEnabledInput'; +import { IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService'; const $ = DOM.$; @@ -50,7 +52,7 @@ export class SettingsEditor2 extends BaseEditor { private rootElement: HTMLElement; private headerContainer: HTMLElement; - private searchWidget: SearchWidget; + private searchWidget: SuggestEnabledInput; private settingsTargetsWidget: SettingsTargetsWidget; private toolbar: ToolBar; @@ -127,6 +129,7 @@ export class SettingsEditor2 extends BaseEditor { this.createHeader(this.rootElement); this.createBody(this.rootElement); + this.updateStyles(); } setInput(input: SettingsEditor2Input, options: EditorOptions, token: CancellationToken): Thenable { @@ -143,9 +146,12 @@ export class SettingsEditor2 extends BaseEditor { } layout(dimension: DOM.Dimension): void { - this.searchWidget.layout(dimension); this.layoutTrees(dimension); + let innerWidth = dimension.width - 24 * 2; // 24px padding on left and right + let monacoWidth = (innerWidth > 1000 ? 1000 : innerWidth - 10); + this.searchWidget.layout({ height: 20, width: monacoWidth }); + DOM.toggleClass(this.rootElement, 'narrow', dimension.width < 600); } @@ -177,7 +183,7 @@ export class SettingsEditor2 extends BaseEditor { } clearSearchResults(): void { - this.searchWidget.clear(); + this.searchWidget.setValue(''); } search(text: string): void { @@ -199,13 +205,20 @@ export class SettingsEditor2 extends BaseEditor { previewTextLabel.textContent = localize('previewLabel', "This is a preview of our new settings editor"); const searchContainer = DOM.append(this.headerContainer, $('.search-container')); - this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, searchContainer, { - ariaLabel: localize('SearchSettings.AriaLabel', "Search settings"), - placeholder: localize('SearchSettings.Placeholder', "Search settings"), - focusKey: this.searchFocusContextKey, - ariaLive: 'assertive' - })); - this._register(this.searchWidget.onDidChange(() => this.onSearchInputChanged())); + + let searchBoxLabel = localize('SearchSettings.AriaLabel', "Search settings"); + this.searchWidget = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${SettingsEditor2.ID}.searchbox`, searchContainer, { + triggerCharacters: ['@'], + provideResults: (query: string) => { + return ['@modified', '@tag:usesOnlineServices'].filter(tag => query.indexOf(tag) === -1).map(tag => tag + ' '); + } + }, searchBoxLabel, 'settingseditor:searchinput', { + placeholderText: searchBoxLabel, + focusContextKey: this.searchFocusContextKey, + // TODO: Aria-live + })); + + this._register(this.searchWidget.onInputDidChange(() => this.onSearchInputChanged())); const headerControlsContainer = DOM.append(this.headerContainer, $('.settings-header-controls')); const targetWidgetContainer = DOM.append(headerControlsContainer, $('.settings-target-container')); @@ -807,6 +820,20 @@ export class SettingsEditor2 extends BaseEditor { this.tocTreeContainer.style.height = `${tocTreeHeight}px`; this.tocTree.layout(tocTreeHeight, 175); } + + public updateStyles(): void { + super.updateStyles(); + this.searchWidget.updateStyles(); + } + + setVisible(visible: boolean, group?: IEditorGroup): TPromise { + if (visible) { + this.searchWidget.focus(); + this.searchWidget.selectAll(); + } + + return TPromise.as(super.setVisible(visible, group)); + } } interface ISettingsToolbarContext { diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts b/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts index 00ee94dd84544..de8af54a5598e 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferences.contribution.ts @@ -35,6 +35,7 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { PreferencesSearchService } from 'vs/workbench/parts/preferences/electron-browser/preferencesSearch'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { Command } from 'vs/editor/browser/editorExtensions'; +import { Context as SuggestContext } from 'vs/editor/contrib/suggest/suggest'; registerSingleton(IPreferencesSearchService, PreferencesSearchService); @@ -401,14 +402,14 @@ class FocusSettingsFileEditorCommand extends SettingsCommand { } const focusSettingsFileEditorCommand = new FocusSettingsFileEditorCommand({ id: SETTINGS_EDITOR_COMMAND_FOCUS_FILE, - precondition: CONTEXT_SETTINGS_SEARCH_FOCUS, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_SEARCH_FOCUS, SuggestContext.Visible.toNegated()), kbOpts: { primary: KeyCode.DownArrow, weight: KeybindingWeight.EditorContrib } }); focusSettingsFileEditorCommand.register(); const focusSettingsFromSearchCommand = new FocusSettingsFileEditorCommand({ id: SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH, - precondition: CONTEXT_SETTINGS_SEARCH_FOCUS, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_SEARCH_FOCUS, SuggestContext.Visible.toNegated()), kbOpts: { primary: KeyCode.DownArrow, weight: KeybindingWeight.WorkbenchContrib } }); focusSettingsFromSearchCommand.register();