提交 a0f07e30 编写于 作者: J Jackson Kearl 提交者: Ramya Rao

Autocomplete for extension search @-operators (#53915)

* WIP

* WIP again

* Feature complete, refactor to Query class

* Add tests

* Spacing

* Add tests and refactor

* Maybe fix tests? Cant run locally.

* Use monaco editor for completions

* Remove escape handler

* Update coloring

* Add localizations, remove unused

* Fix spacing

* update serach ordering

* Remove enter handling

* Fix tab handling

* Improve autosuggest enablment condition

* Conditional styling of cursor
上级 7db5f960
......@@ -177,7 +177,7 @@ export const inputBackground = registerColor('input.background', { dark: '#3C3C3
export const inputForeground = registerColor('input.foreground', { dark: foreground, light: foreground, hc: foreground }, nls.localize('inputBoxForeground', "Input box foreground."));
export const inputBorder = registerColor('input.border', { dark: null, light: null, hc: contrastBorder }, nls.localize('inputBoxBorder', "Input box border."));
export const inputActiveOptionBorder = registerColor('inputOption.activeBorder', { dark: '#007ACC', light: '#007ACC', hc: activeContrastBorder }, nls.localize('inputBoxActiveOptionBorder', "Border color of activated options in input fields."));
export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { dark: null, light: null, hc: null }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text."));
export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { light: transparent(foreground, 0.5), dark: transparent(foreground, 0.5), hc: transparent(foreground, 0.7) }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text."));
export const inputValidationInfoBackground = registerColor('inputValidation.infoBackground', { dark: '#063B49', light: '#D6ECF2', hc: Color.black }, nls.localize('inputValidationInfoBackground', "Input validation background color for information severity."));
export const inputValidationInfoBorder = registerColor('inputValidation.infoBorder', { dark: '#007acc', light: '#007acc', hc: contrastBorder }, nls.localize('inputValidationInfoBorder', "Input validation border color for information severity."));
......
......@@ -3,12 +3,27 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { flatten } from 'vs/base/common/arrays';
export class Query {
constructor(public value: string, public sortBy: string, public groupBy: string) {
this.value = value.trim();
}
static autocompletions(): string[] {
const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'recommended', 'sort', 'category', 'tag', 'ext'];
const subcommands = {
'sort': ['installs', 'rating', 'name'],
'category': ['"programming languages"', 'snippets', 'linters', 'themes', 'debuggers', 'formatters', 'keymaps', '"scm providers"', 'other', '"extension packs"', '"language packs"'],
'tag': [''],
'ext': ['']
};
return flatten(commands.map(command => subcommands[command] ? subcommands[command].map(subcommand => `${command}:${subcommand}`) : [command]));
}
static parse(value: string): Query {
let sortBy = '';
value = value.replace(/@sort:(\w+)(-\w*)?/g, (match, by: string, order: string) => {
......
......@@ -6,21 +6,21 @@
'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, mapEvent, chain } from 'vs/base/common/event';
import { Event as EventOf, Emitter, chain } from 'vs/base/common/event';
import { IAction } from 'vs/base/common/actions';
import { domEvent } from 'vs/base/browser/event';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
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, $, addStandardDisposableListener, EventType, addClass, removeClass, toggleClass, Dimension } from 'vs/base/browser/dom';
import { append, $, addClass, removeClass, 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 +39,7 @@ 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 } from 'vs/platform/theme/common/colorRegistry';
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';
......@@ -58,6 +58,13 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle
import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/extensionsWorkbenchService';
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 { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { Range } from 'vs/editor/common/core/range';
import { Position } from 'vs/editor/common/core/position';
import { ITextModel } from 'vs/editor/common/model';
interface SearchInputEvent extends Event {
target: HTMLInputElement;
......@@ -252,12 +259,14 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
private searchDelayer: ThrottledDelayer<any>;
private root: HTMLElement;
private searchBox: HTMLInputElement;
private searchBox: CodeEditorWidget;
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,
......@@ -275,7 +284,8 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
@IContextKeyService contextKeyService: IContextKeyService,
@IContextMenuService contextMenuService: IContextMenuService,
@IExtensionService extensionService: IExtensionService,
@IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService
@IExtensionManagementServerService private extensionManagementServerService: IExtensionManagementServerService,
@IModelService private modelService: IModelService,
) {
super(VIEWLET_ID, `${VIEWLET_ID}.state`, true, partService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService);
......@@ -299,6 +309,28 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue<boolean>(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: <modes.SuggestionType>'keyword'
}))
};
}
});
}
create(parent: HTMLElement): TPromise<void> {
......@@ -306,32 +338,36 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
this.root = parent;
const header = append(this.root, $('.header'));
this.searchBox = append(header, $<HTMLInputElement>('input.search-box'));
this.searchBox.placeholder = localize('searchExtensions', "Search Extensions in Marketplace");
this.disposables.push(addStandardDisposableListener(this.searchBox, EventType.FOCUS, () => addClass(this.searchBox, 'synthetic-focus')));
this.disposables.push(addStandardDisposableListener(this.searchBox, EventType.BLUR, () => removeClass(this.searchBox, 'synthetic-focus')));
this.monacoStyleContainer = append(header, $('.monaco-container'));
this.searchBox = this.instantiationService.createInstance(CodeEditorWidget, this.monacoStyleContainer, SEARCH_INPUT_OPTIONS, { isSimpleWidget: true });
this.placeholderText = append(this.monacoStyleContainer, $('.search-placeholder', null, localize('searchExtensions', "Search Extensions in Marketplace")));
this.extensionsBox = append(this.root, $('.extensions'));
const onKeyDown = chain(domEvent(this.searchBox, 'keydown'))
.map(e => new StandardKeyboardEvent(e));
onKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(this.onEscape, this, this.disposables);
this.searchBox.setModel(this.modelService.createModel('', null, uri.parse('extensions:searchinput'), true));
const onKeyDownForList = onKeyDown.filter(() => this.count() > 0);
onKeyDownForList.filter(e => e.keyCode === KeyCode.Enter).on(this.onEnter, this, this.disposables);
onKeyDownForList.filter(e => e.keyCode === KeyCode.DownArrow).on(this.focusListView, this, this.disposables);
this.disposables.push(this.searchBox.onDidFocusEditorText(() => addClass(this.monacoStyleContainer, 'synthetic-focus')));
this.disposables.push(this.searchBox.onDidBlurEditorText(() => removeClass(this.monacoStyleContainer, 'synthetic-focus')));
const onSearchInput = domEvent(this.searchBox, 'input') as EventOf<SearchInputEvent>;
onSearchInput(e => this.triggerSearch(e.immediate), null, this.disposables);
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).on(() => this.focusListView(), this, this.disposables);
this.onSearchChange = mapEvent(onSearchInput, e => e.target.value);
const searchChangeEvent = new Emitter<string>();
this.onSearchChange = searchChangeEvent.event;
this.disposables.push(this.searchBox.getModel().onDidChangeContent(() => {
this.triggerSearch();
const content = this.searchBox.getValue();
searchChangeEvent.fire(content);
this.placeholderText.style.visibility = content ? 'hidden' : 'visible';
}));
return super.create(this.extensionsBox)
.then(() => this.extensionManagementService.getInstalled(LocalExtensionType.User))
.then(installed => {
if (installed.length === 0) {
this.searchBox.value = '@sort:installs';
this.searchBox.setValue('@sort:installs');
this.searchExtensionsContextKey.set(true);
}
});
......@@ -340,13 +376,19 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
public updateStyles(): void {
super.updateStyles();
this.searchBox.style.backgroundColor = this.getColor(inputBackground);
this.searchBox.style.color = this.getColor(inputForeground);
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.searchBox.style.borderWidth = inputBorderColor ? '1px' : null;
this.searchBox.style.borderStyle = inputBorderColor ? 'solid' : null;
this.searchBox.style.borderColor = inputBorderColor;
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);
}
}
setVisible(visible: boolean): TPromise<void> {
......@@ -355,7 +397,7 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
if (isVisibilityChanged) {
if (visible) {
this.searchBox.focus();
this.searchBox.setSelectionRange(0, this.searchBox.value.length);
this.searchBox.setSelection(new Range(1, 1, 1, this.searchBox.getValue().length + 1));
}
}
});
......@@ -367,6 +409,9 @@ 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 - 30 });
this.placeholderText.style.width = '' + (dimension.width - 30) + 'px';
super.layout(new Dimension(dimension.width, dimension.height - 38));
}
......@@ -421,17 +466,19 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
const event = new Event('input', { bubbles: true }) as SearchInputEvent;
event.immediate = true;
this.searchBox.value = value;
this.searchBox.dispatchEvent(event);
this.searchBox.setValue(value);
}
private triggerSearch(immediate = false): void {
this.searchDelayer.trigger(() => this.doSearch(), immediate || !this.searchBox.value ? 0 : 500)
.done(null, err => this.onError(err));
this.searchDelayer.trigger(() => this.doSearch(), immediate || !this.searchBox.getValue() ? 0 : 500).done(null, err => this.onError(err));
}
private normalizedQuery(): string {
return (this.searchBox.getValue() || '').replace(/@category/g, 'category').replace(/@tag:/g, 'tag:').replace(/@ext:/g, 'ext:');
}
private doSearch(): TPromise<any> {
const value = this.searchBox.value || '';
const value = this.normalizedQuery();
this.searchExtensionsContextKey.set(!!value);
this.searchInstalledExtensionsContextKey.set(InstalledExtensionsView.isInstalledExtensionsQuery(value));
this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value));
......@@ -440,14 +487,14 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
this.nonEmptyWorkspaceContextKey.set(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY);
if (value) {
return this.progress(TPromise.join(this.panels.map(view => (<ExtensionsListView>view).show(this.searchBox.value))));
return this.progress(TPromise.join(this.panels.map(view => (<ExtensionsListView>view).show(this.normalizedQuery()))));
}
return TPromise.as(null);
}
protected onDidAddViews(added: IAddedViewDescriptorRef[]): ViewletPanel[] {
const addedViews = super.onDidAddViews(added);
this.progress(TPromise.join(addedViews.map(addedView => (<ExtensionsListView>addedView).show(this.searchBox.value))));
this.progress(TPromise.join(addedViews.map(addedView => (<ExtensionsListView>addedView).show(this.normalizedQuery()))));
return addedViews;
}
......@@ -465,20 +512,23 @@ export class ExtensionsViewlet extends ViewContainerViewlet implements IExtensio
return this.instantiationService.createInstance(viewDescriptor.ctor, options) as ViewletPanel;
}
private count(): number {
return this.panels.reduce((count, view) => (<ExtensionsListView>view).count() + count, 0);
}
private autoComplete(query: string, position: number): { fullText: string, overwrite: number }[] {
if (query.lastIndexOf('@', position - 1) !== query.lastIndexOf(' ', position - 1) + 1) { return []; }
let wordStart = query.lastIndexOf('@', position - 1) + 1;
let alreadyTypedCount = position - wordStart - 1;
private onEscape(): void {
this.search('');
return Query.autocompletions().map(replacement => ({ fullText: replacement, overwrite: alreadyTypedCount }));
}
private onEnter(): void {
(<ExtensionsListView>this.panels[0]).select();
private count(): number {
return this.panels.reduce((count, view) => (<ExtensionsListView>view).count() + count, 0);
}
private focusListView(): void {
this.panels[0].focus();
if (this.count() > 0) {
this.panels[0].focus();
}
}
private onViewletOpen(viewlet: IViewlet): void {
......@@ -612,4 +662,34 @@ export class MaliciousExtensionChecker implements IWorkbenchContribution {
dispose(): void {
this.disposables = dispose(this.disposables);
}
}
\ No newline at end of file
}
let SEARCH_INPUT_OPTIONS: IEditorOptions =
{
fontSize: 13,
lineHeight: 22,
wordWrap: 'off',
overviewRulerLanes: 0,
glyphMargin: false,
lineNumbers: 'off',
folding: false,
selectOnLineNumbers: false,
hideCursorInOverviewRuler: true,
selectionHighlight: false,
scrollbar: {
horizontal: 'hidden',
vertical: 'hidden'
},
ariaLabel: localize('searchExtensions', "Search Extensions in Marketplace"),
cursorWidth: 1,
lineDecorationsWidth: 0,
overviewRulerBorder: false,
scrollBeyondLastLine: false,
renderLineHighlight: 'none',
fixedOverflowWidgets: true,
acceptSuggestionOnEnter: 'smart',
minimap: {
enabled: false
},
fontFamily: ' -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif'
};
......@@ -197,6 +197,32 @@
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,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册