From fbd2a25867f8a2b28bd1a0226cd3af4708d48af4 Mon Sep 17 00:00:00 2001 From: Cody Hoover Date: Mon, 20 Mar 2017 23:34:06 +0100 Subject: [PATCH] Initial Prototype of Find in HtmlPreviewPart --- .../editor/contrib/find/browser/findWidget.ts | 18 +- .../parts/html/browser/htmlPreviewPart.ts | 93 +++++- .../images/cancelSelectionFind-inverse.svg | 8 + .../browser/images/cancelSelectionFind.svg | 8 + .../parts/html/browser/images/close-dark.svg | 1 + .../parts/html/browser/images/close.svg | 1 + .../browser/images/expando-collapsed-dark.svg | 1 + .../html/browser/images/expando-collapsed.svg | 1 + .../browser/images/expando-expanded-dark.svg | 1 + .../html/browser/images/expando-expanded.svg | 1 + .../html/browser/images/next-inverse.svg | 5 + .../parts/html/browser/images/next.svg | 5 + .../html/browser/images/previous-inverse.svg | 5 + .../parts/html/browser/images/previous.svg | 5 + .../browser/images/replace-all-inverse.svg | 11 + .../parts/html/browser/images/replace-all.svg | 11 + .../html/browser/images/replace-inverse.svg | 13 + .../parts/html/browser/images/replace.svg | 13 + .../parts/html/browser/simpleFindModel.ts | 83 +++++ .../parts/html/browser/simpleFindState.ts | 108 +++++++ .../parts/html/browser/simpleFindWidget.css | 305 ++++++++++++++++++ .../parts/html/browser/simpleFindWidget.ts | 249 ++++++++++++++ .../workbench/parts/html/browser/webview.ts | 65 +++- 23 files changed, 997 insertions(+), 14 deletions(-) create mode 100644 src/vs/workbench/parts/html/browser/images/cancelSelectionFind-inverse.svg create mode 100644 src/vs/workbench/parts/html/browser/images/cancelSelectionFind.svg create mode 100644 src/vs/workbench/parts/html/browser/images/close-dark.svg create mode 100644 src/vs/workbench/parts/html/browser/images/close.svg create mode 100644 src/vs/workbench/parts/html/browser/images/expando-collapsed-dark.svg create mode 100644 src/vs/workbench/parts/html/browser/images/expando-collapsed.svg create mode 100644 src/vs/workbench/parts/html/browser/images/expando-expanded-dark.svg create mode 100644 src/vs/workbench/parts/html/browser/images/expando-expanded.svg create mode 100644 src/vs/workbench/parts/html/browser/images/next-inverse.svg create mode 100644 src/vs/workbench/parts/html/browser/images/next.svg create mode 100644 src/vs/workbench/parts/html/browser/images/previous-inverse.svg create mode 100644 src/vs/workbench/parts/html/browser/images/previous.svg create mode 100644 src/vs/workbench/parts/html/browser/images/replace-all-inverse.svg create mode 100644 src/vs/workbench/parts/html/browser/images/replace-all.svg create mode 100644 src/vs/workbench/parts/html/browser/images/replace-inverse.svg create mode 100644 src/vs/workbench/parts/html/browser/images/replace.svg create mode 100644 src/vs/workbench/parts/html/browser/simpleFindModel.ts create mode 100644 src/vs/workbench/parts/html/browser/simpleFindState.ts create mode 100644 src/vs/workbench/parts/html/browser/simpleFindWidget.css create mode 100644 src/vs/workbench/parts/html/browser/simpleFindWidget.ts diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index 5dc01c2872b15..157ed7f25ec6d 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -34,20 +34,20 @@ export interface IFindController { replaceAll(): void; } -const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); -const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); -const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous match"); -const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next match"); +export const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); +export const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); +export const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous match"); +export const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next match"); const NLS_TOGGLE_SELECTION_FIND_TITLE = nls.localize('label.toggleSelectionFind', "Find in selection"); -const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close"); +export const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close"); const NLS_REPLACE_INPUT_LABEL = nls.localize('label.replace', "Replace"); const NLS_REPLACE_INPUT_PLACEHOLDER = nls.localize('placeholder.replace', "Replace"); const NLS_REPLACE_BTN_LABEL = nls.localize('label.replaceButton', "Replace"); const NLS_REPLACE_ALL_BTN_LABEL = nls.localize('label.replaceAllButton', "Replace All"); const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace mode"); const NLS_MATCHES_COUNT_LIMIT_TITLE = nls.localize('title.matchesCountLimit', "Only the first 999 results are highlighted, but all find operations work on the entire text."); -const NLS_MATCHES_LOCATION = nls.localize('label.matchesLocation', "{0} of {1}"); -const NLS_NO_RESULTS = nls.localize('label.noResults', "No Results"); +export const NLS_MATCHES_LOCATION = nls.localize('label.matchesLocation', "{0} of {1}"); +export const NLS_NO_RESULTS = nls.localize('label.noResults', "No Results"); let MAX_MATCHES_COUNT_WIDTH = 69; const WIDGET_FIXED_WIDTH = 411 - 69; @@ -714,14 +714,14 @@ class SimpleCheckbox extends Widget { } } -interface ISimpleButtonOpts { +export interface ISimpleButtonOpts { label: string; className: string; onTrigger: () => void; onKeyDown: (e: IKeyboardEvent) => void; } -class SimpleButton extends Widget { +export class SimpleButton extends Widget { private _opts: ISimpleButtonOpts; private _domNode: HTMLElement; diff --git a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts index 5e10f2f5036dd..8cc04a48b7f03 100644 --- a/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts +++ b/src/vs/workbench/parts/html/browser/htmlPreviewPart.ts @@ -23,7 +23,24 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITextModelResolverService, ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { Parts, IPartService } from 'vs/workbench/services/part/common/partService'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { CommonEditorRegistry, Command } from 'vs/editor/common/editorCommonExtensions'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ContextKeyExpr, IContextKey, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; + import Webview from './webview'; +import { SimpleFindWidget } from './simpleFindWidget'; +import { FindModelBoundToWebview } from './simpleFindModel'; +import { SimpleFindState } from './simpleFindState'; + +// --- Register Context Keys + +/** A context key that is set when an html preview has focus. */ +export const KEYBINDING_CONTEXT_HTML_PREVIEW_FOCUS = new RawContextKey('htmlPreviewFocus', undefined); +/** A context key that is set when an html preview does not have focus. */ +export const KEYBINDING_CONTEXT_HTML_PREVIEW_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_HTML_PREVIEW_FOCUS.toNegated(); + /** * An implementation of editor for showing HTML content in an IFrame by leveraging the HTML input. @@ -36,7 +53,12 @@ export class HtmlPreviewPart extends BaseEditor { private _openerService: IOpenerService; private _webview: Webview; private _webviewDisposables: IDisposable[]; + private _findModel: FindModelBoundToWebview; + private _findState: SimpleFindState; private _container: HTMLDivElement; + // private headerContainer: HTMLElement; + private _findWidget: SimpleFindWidget; + private _htmlPreviewFocusContexKey: IContextKey; private _baseUrl: URI; @@ -51,13 +73,17 @@ export class HtmlPreviewPart extends BaseEditor { @IWorkbenchThemeService protected themeService: IWorkbenchThemeService, @IOpenerService openerService: IOpenerService, @IWorkspaceContextService contextService: IWorkspaceContextService, - @IPartService private partService: IPartService + @IPartService private partService: IPartService, + @IInstantiationService private instantiationService: IInstantiationService, + @IContextKeyService private _contextKeyService: IContextKeyService ) { super(HtmlPreviewPart.ID, telemetryService, themeService); this._textModelResolverService = textModelResolverService; this._openerService = openerService; this._baseUrl = contextService.toResource('/'); + + this._htmlPreviewFocusContexKey = KEYBINDING_CONTEXT_HTML_PREVIEW_FOCUS.bindTo(this._contextKeyService); } dispose(): void { @@ -68,6 +94,10 @@ export class HtmlPreviewPart extends BaseEditor { this._themeChangeSubscription.dispose(); this._modelChangeSubscription.dispose(); + if (this._findWidget) { + this._findWidget.dispose(); + } + // dipose model ref dispose(this._modelRef); super.dispose(); @@ -78,18 +108,38 @@ export class HtmlPreviewPart extends BaseEditor { this._container.style.paddingLeft = '20px'; this._container.style.position = 'absolute'; this._container.style.zIndex = '300'; + this._container.style.overflow = 'hidden'; parent.getHTMLElement().appendChild(this._container); + this._findState = this._register(new SimpleFindState()); + this._findState.addChangeListener((e) => { + if (e.isRevealed) { + if (!this._findState.isRevealed) { + this.webview.focus(); + } + } + }); + this._findWidget = this._register(this.instantiationService.createInstance(SimpleFindWidget, this._container, this._findState)); } private get webview(): Webview { if (!this._webview) { - this._webview = new Webview(this._container, this.partService.getContainer(Parts.EDITOR_PART)); + this._webview = new Webview(this._container, this.partService.getContainer(Parts.EDITOR_PART), this._htmlPreviewFocusContexKey); this._webview.baseUrl = this._baseUrl && this._baseUrl.toString(true); this._webviewDisposables = [ this._webview, this._webview.onDidClickLink(uri => this._openerService.open(uri)), - this._webview.onDidLoadContent(data => this.telemetryService.publicLog('previewHtml', data.stats)) + this._webview.onDidLoadContent(data => { + this.telemetryService.publicLog('previewHtml', data.stats); + if (this._findModel) { + this._findModel.dispose(); + } + this._findModel = this._register(new FindModelBoundToWebview(this._webview, this._findState)); + this._findWidget.findModel = this._findModel; + // Ideally, we would resume the find when re-focusing the editor. + // However, this returns no results as the content hasn't fully loaded yet. + // this._findModel.startFind(); + }) ]; } return this._webview; @@ -120,7 +170,9 @@ export class HtmlPreviewPart extends BaseEditor { this.webview.style(this.themeService.getColorTheme()); if (this._hasValidModel()) { - this._modelChangeSubscription = this.model.onDidChangeContent(() => this.webview.contents = this.model.getLinesContent()); + this._modelChangeSubscription = this.model.onDidChangeContent(() => { + this.webview.contents = this.model.getLinesContent(); + }); this.webview.contents = this.model.getLinesContent(); } } @@ -135,12 +187,21 @@ export class HtmlPreviewPart extends BaseEditor { // we take the padding we set on create into account this._container.style.width = `${Math.max(width - 20, 0)}px`; this._container.style.height = `${height}px`; + + if (this._findWidget) { + this._findWidget.layout(width); + } } public focus(): void { this.webview.focus(); } + public activateFind(): void { + this._findState.change({ isRevealed: true }); + this._findWidget.activate(); + } + public clearInput(): void { dispose(this._modelRef); this._modelRef = undefined; @@ -192,3 +253,27 @@ export class HtmlPreviewPart extends BaseEditor { }); } } + +class StartSearchHtmlPreviewPartCommand extends Command { + + public runCommand(accessor: ServicesAccessor, args: any): void { + const htmlPreviewPart = this.getHtmlPreviewPart(accessor); + if (htmlPreviewPart) { + htmlPreviewPart.activateFind(); + } + } + + private getHtmlPreviewPart(accessor: ServicesAccessor): HtmlPreviewPart { + const activeEditor = accessor.get(IWorkbenchEditorService).getActiveEditor(); + if (activeEditor instanceof HtmlPreviewPart) { + return activeEditor; + } + return null; + } +} +CommonEditorRegistry.registerEditorCommand(new StartSearchHtmlPreviewPartCommand({ + id: 'htmlPreview.action.search', + precondition: ContextKeyExpr.and(KEYBINDING_CONTEXT_HTML_PREVIEW_FOCUS), + kbOpts: { primary: KeyMod.CtrlCmd | KeyCode.KEY_F } +})); + diff --git a/src/vs/workbench/parts/html/browser/images/cancelSelectionFind-inverse.svg b/src/vs/workbench/parts/html/browser/images/cancelSelectionFind-inverse.svg new file mode 100644 index 0000000000000..0059c15d5ac06 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/cancelSelectionFind-inverse.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/vs/workbench/parts/html/browser/images/cancelSelectionFind.svg b/src/vs/workbench/parts/html/browser/images/cancelSelectionFind.svg new file mode 100644 index 0000000000000..cdff5731a8275 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/cancelSelectionFind.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/vs/workbench/parts/html/browser/images/close-dark.svg b/src/vs/workbench/parts/html/browser/images/close-dark.svg new file mode 100644 index 0000000000000..751e89b3b0215 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/close-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/html/browser/images/close.svg b/src/vs/workbench/parts/html/browser/images/close.svg new file mode 100644 index 0000000000000..fde34404d4eb8 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/html/browser/images/expando-collapsed-dark.svg b/src/vs/workbench/parts/html/browser/images/expando-collapsed-dark.svg new file mode 100644 index 0000000000000..6f3abfce784f7 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/expando-collapsed-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/html/browser/images/expando-collapsed.svg b/src/vs/workbench/parts/html/browser/images/expando-collapsed.svg new file mode 100644 index 0000000000000..5dcb87c772c21 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/expando-collapsed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/html/browser/images/expando-expanded-dark.svg b/src/vs/workbench/parts/html/browser/images/expando-expanded-dark.svg new file mode 100644 index 0000000000000..22dfac04f1583 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/expando-expanded-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/html/browser/images/expando-expanded.svg b/src/vs/workbench/parts/html/browser/images/expando-expanded.svg new file mode 100644 index 0000000000000..e55ccd923e52b --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/expando-expanded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/workbench/parts/html/browser/images/next-inverse.svg b/src/vs/workbench/parts/html/browser/images/next-inverse.svg new file mode 100644 index 0000000000000..7498a498bb652 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/next-inverse.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/vs/workbench/parts/html/browser/images/next.svg b/src/vs/workbench/parts/html/browser/images/next.svg new file mode 100644 index 0000000000000..4b176879f90af --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/next.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/vs/workbench/parts/html/browser/images/previous-inverse.svg b/src/vs/workbench/parts/html/browser/images/previous-inverse.svg new file mode 100644 index 0000000000000..0aabf393d9c2f --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/previous-inverse.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/vs/workbench/parts/html/browser/images/previous.svg b/src/vs/workbench/parts/html/browser/images/previous.svg new file mode 100644 index 0000000000000..f7acf0acbd999 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/previous.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/vs/workbench/parts/html/browser/images/replace-all-inverse.svg b/src/vs/workbench/parts/html/browser/images/replace-all-inverse.svg new file mode 100644 index 0000000000000..2744a6a4e81a4 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/replace-all-inverse.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/src/vs/workbench/parts/html/browser/images/replace-all.svg b/src/vs/workbench/parts/html/browser/images/replace-all.svg new file mode 100644 index 0000000000000..4c55b91d0e913 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/replace-all.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/src/vs/workbench/parts/html/browser/images/replace-inverse.svg b/src/vs/workbench/parts/html/browser/images/replace-inverse.svg new file mode 100644 index 0000000000000..a15ad9b4af194 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/replace-inverse.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/vs/workbench/parts/html/browser/images/replace.svg b/src/vs/workbench/parts/html/browser/images/replace.svg new file mode 100644 index 0000000000000..14da77d219892 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/images/replace.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/vs/workbench/parts/html/browser/simpleFindModel.ts b/src/vs/workbench/parts/html/browser/simpleFindModel.ts new file mode 100644 index 0000000000000..2c75fd06e5812 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/simpleFindModel.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import WebView, { FoundInPageResults } from './webview'; +import { SimpleFindState, SimpleFindStateChangedEvent } from './simpleFindState'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; + +export class FindModelBoundToWebview { + private _webview: WebView; + private _state: SimpleFindState; + private _toDispose: IDisposable[]; + private _isDisposed: boolean; + + constructor(webview: WebView, state: SimpleFindState) { + this._webview = webview; + this._state = state; + this._toDispose = []; + this._isDisposed = false; + + this._toDispose.push(this._state.addChangeListener((e) => this._onStateChanged(e))); + this._toDispose.push(this._webview.onFindResults((e) => this._onResults(e))); + } + + public dispose(): void { + this._isDisposed = true; + this._toDispose = dispose(this._toDispose); + } + + private _onStateChanged(e: SimpleFindStateChangedEvent): void { + if (this._isDisposed) { + return; + } + if (e.searchString) { + if (this._state.searchString) { + this.startFind(); + } else { + this.stopFind(false); + } + } + + if (e.isRevealed) { + if (!this._state.isRevealed) { + this.stopFind(true); + } + } + } + + private _onResults(e: FoundInPageResults): void { + if (this._isDisposed) { + return; + } + this._state.changeMatchInfo(e.activeMatchOrdinal, e.matches); + } + + public startFind(): void { + if (this._isDisposed) { + return; + } + this._webview.find(this._state.searchString); + } + + public moveToNextMatch(): void { + if (this._isDisposed) { + return; + } + this._webview.find(this._state.searchString, { findNext: true, forward: true }); + } + + public moveToPrevMatch(): void { + if (this._isDisposed) { + return; + } + this._webview.find(this._state.searchString, { findNext: true, forward: false }); + } + + public stopFind(keepSelection?: boolean): void { + if (this._isDisposed) { + return; + } + this._webview.stopFind(keepSelection); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/html/browser/simpleFindState.ts b/src/vs/workbench/parts/html/browser/simpleFindState.ts new file mode 100644 index 0000000000000..a5a4e91303622 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/simpleFindState.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { IDisposable } from 'vs/base/common/lifecycle'; +import { EventEmitter } from 'vs/base/common/eventEmitter'; + +export interface SimpleFindStateChangedEvent { + searchString: boolean; + isRevealed: boolean; + matchesPosition: boolean; + matchesCount: boolean; +} + +export interface INewSimpleFindState { + searchString?: string; + isRevealed?: boolean; +} + +export class SimpleFindState implements IDisposable { + private static _CHANGED_EVENT = 'changed'; + private _searchString: string; + private _isRevealed: boolean; + private _matchesCount: number; + private _matchesPosition: number; + private _eventEmitter: EventEmitter; + + public get searchString(): string { return this._searchString; } + public get isRevealed(): boolean { return this._isRevealed; } + public get matchesPosition(): number { return this._matchesPosition; } + public get matchesCount(): number { return this._matchesCount; } + + constructor() { + this._searchString = ''; + this._isRevealed = false; + this._matchesPosition = 0; + this._matchesCount = 0; + this._eventEmitter = new EventEmitter(); + } + + public dispose(): void { + this._eventEmitter.dispose(); + } + + public addChangeListener(listener: (e: SimpleFindStateChangedEvent) => void): IDisposable { + return this._eventEmitter.addListener2(SimpleFindState._CHANGED_EVENT, listener); + } + + public changeMatchInfo(matchesPosition: number, matchesCount: number): void { + let changeEvent: SimpleFindStateChangedEvent = { + searchString: false, + isRevealed: false, + matchesPosition: false, + matchesCount: false, + }; + let somethingChanged = false; + + if (matchesCount === 0) { + matchesPosition = 0; + } + if (matchesPosition > matchesCount) { + matchesPosition = matchesCount; + } + + if (this._matchesPosition !== matchesPosition) { + this._matchesPosition = matchesPosition; + changeEvent.matchesPosition = true; + somethingChanged = true; + } + if (this._matchesCount !== matchesCount) { + this._matchesCount = matchesCount; + changeEvent.matchesCount = true; + somethingChanged = true; + } + + if (somethingChanged) { + this._eventEmitter.emit(SimpleFindState._CHANGED_EVENT, changeEvent); + } + } + + public change(newState: INewSimpleFindState): void { + let changeEvent: SimpleFindStateChangedEvent = { + searchString: false, + isRevealed: false, + matchesPosition: false, + matchesCount: false, + }; + let somethingChanged = false; + + if (typeof newState.searchString !== 'undefined') { + if (this._searchString !== newState.searchString) { + this._searchString = newState.searchString; + changeEvent.searchString = true; + somethingChanged = true; + } + } + if (typeof newState.isRevealed !== 'undefined') { + if (this._isRevealed !== newState.isRevealed) { + this._isRevealed = newState.isRevealed; + changeEvent.isRevealed = true; + somethingChanged = true; + } + } + if (somethingChanged) { + this._eventEmitter.emit(SimpleFindState._CHANGED_EVENT, changeEvent); + } + } +} diff --git a/src/vs/workbench/parts/html/browser/simpleFindWidget.css b/src/vs/workbench/parts/html/browser/simpleFindWidget.css new file mode 100644 index 0000000000000..75fbf1322a9ac --- /dev/null +++ b/src/vs/workbench/parts/html/browser/simpleFindWidget.css @@ -0,0 +1,305 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Find widget */ +#workbench\.editor\.htmlPreviewPart .find-widget { + position: absolute; + z-index: 10; + right: 28px; + top: -44px; /* find input height + shadow (10px) */ + height: 34px; /* find input height */ + overflow: hidden; + line-height: 19px; + + -webkit-transition: top 200ms linear; + -o-transition: top 200ms linear; + -moz-transition: top 200ms linear; + -ms-transition: top 200ms linear; + transition: top 200ms linear; + + padding: 0 4px; +} + +#workbench\.editor\.htmlPreviewPart .find-widget.visible { + top: 0; +} + +#workbench\.editor\.htmlPreviewPart .find-widget .monaco-inputbox .input { + background-color: transparent; + /* Style to compensate for //winjs */ + min-height: 0; +} + +#workbench\.editor\.htmlPreviewPart .find-widget .monaco-findInput { + background-color: white; +} + +#workbench\.editor\.htmlPreviewPart .find-widget.visible.noanimation { + -webkit-transition: none; + -o-transition: none; + -moz-transition: none; + -ms-transition: none; + transition: none; +} + +#workbench\.editor\.htmlPreviewPart .find-widget > .find-part { + margin: 4px 0 0 4px; + font-size: 12px; +} + +#workbench\.editor\.htmlPreviewPart .find-widget > .find-part .monaco-inputbox { + height: 25px; +} + +#workbench\.editor\.htmlPreviewPart .find-widget > .find-part .monaco-inputbox > .wrapper > .input { + padding-top: 2px; + padding-bottom: 2px; +} + +#workbench\.editor\.htmlPreviewPart .find-widget .monaco-findInput { + display: inline-block; + vertical-align: middle; +} + +#workbench\.editor\.htmlPreviewPart .find-widget.no-results .matchesCount { + color: #A1260D; +} + +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget.no-results .matchesCount, +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget.no-results .matchesCount { + color: #F48771 +} + +#workbench\.editor\.htmlPreviewPart .find-widget .matchesCount { + display: inline-block; + margin: 0 1px 0 3px; + padding: 2px 2px 0 2px; + height: 25px; + vertical-align: middle; + box-sizing: border-box; + text-align: center; + line-height: 23px; +} + +#workbench\.editor\.htmlPreviewPart .find-widget .button { + width: 20px; + height: 20px; + display: inline-block; + vertical-align: middle; + margin-left: 3px; + background-position: center center; + background-repeat: no-repeat; + cursor: pointer; +} + +#workbench\.editor\.htmlPreviewPart .find-widget .button:not(.disabled):hover { + background-color: #DDD; +} + +#workbench\.editor\.htmlPreviewPart .find-widget .button.left { + margin-left: 0; + margin-right: 3px; +} + +#workbench\.editor\.htmlPreviewPart .find-widget .button.wide { + width: auto; + padding: 1px 6px; + top: -1px; +} + +#workbench\.editor\.htmlPreviewPart .find-widget .button.toggle { + position: absolute; + top: 0; + left: 0; + width: 18px; + height: 100%; + -webkit-box-sizing: border-box; + -o-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} + +#workbench\.editor\.htmlPreviewPart .find-widget .button.toggle.disabled { + display: none; +} + +#workbench\.editor\.htmlPreviewPart .find-widget .previous { + background-image: url('images/previous.svg'); +} + +#workbench\.editor\.htmlPreviewPart .find-widget .next { + background-image: url('images/next.svg'); +} + +#workbench\.editor\.htmlPreviewPart .find-widget .disabled { + opacity: 0.3; + cursor: default; +} + +#workbench\.editor\.htmlPreviewPart .find-widget .close-fw { + background-image: url('images/close.svg'); +} + +/* REDUCED */ +#workbench\.editor\.htmlPreviewPart .find-widget.reduced-find-widget .matchesCount { + display:none; +} + +/* NARROW (SMALLER THAN REDUCED) */ +#workbench\.editor\.htmlPreviewPart .find-widget.narrow-find-widget > .find-part .monaco-findInput { + width: 171px !important; +} + +/* COLLAPSED (SMALLER THAN NARROW) */ +#workbench\.editor\.htmlPreviewPart .find-widget.collapsed-find-widget .button.previous, +#workbench\.editor\.htmlPreviewPart .find-widget.collapsed-find-widget .button.next, +#workbench\.editor\.htmlPreviewPart .find-widget.collapsed-find-widget > .find-part .monaco-findInput .controls { + display:none; +} +#workbench\.editor\.htmlPreviewPart .find-widget.collapsed-find-widget > .find-part .monaco-findInput, +#workbench\.editor\.htmlPreviewPart .find-widget.collapsed-find-widget > .find-part .monaco-inputbox > .wrapper > .input { + width: 71px !important; +} + +#workbench\.editor\.htmlPreviewPart .findMatch { + background-color: rgba(234, 92, 0, 0.3); + -webkit-animation-duration: 0; + -webkit-animation-name: inherit !important; + -moz-animation-duration: 0; + -moz-animation-name: inherit !important; + -ms-animation-duration: 0; + -ms-animation-name: inherit !important; + animation-duration: 0; + animation-name: inherit !important; +} + +#workbench\.editor\.htmlPreviewPart .currentFindMatch { + background-color: #A8AC94; +} + +#workbench\.editor\.htmlPreviewPart .findScope { + background-color: rgba(180, 180, 180, 0.3); +} + +/* Theming */ +#workbench\.editor\.htmlPreviewPart .find-widget { + background-color: #EFEFF2; + box-shadow: 0 2px 8px #A8A8A8; +} + +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget { + background-color: #2D2D30; + box-shadow: 0 2px 8px #000; +} + +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .previous, +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .previous { + background-image: url('images/previous-inverse.svg'); +} + +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .next, +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .next { + background-image: url('images/next-inverse.svg'); +} + +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .monaco-checkbox .label, +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .monaco-checkbox .label { + background-image: url('images/cancelSelectionFind-inverse.svg'); +} + +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .monaco-checkbox .checkbox:not(:disabled):hover:before + .label { + background-color: #2f3334; +} + +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .monaco-checkbox .checkbox:checked + .label { + background-color: rgba(150, 150, 150, 0.3); +} + +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .close-fw, +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .close-fw { + background-image: url('images/close-dark.svg'); +} + +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .replace, +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .replace { + background-image: url('images/replace-inverse.svg'); +} + +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .replace-all, +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .replace-all { + background-image: url('images/replace-all-inverse.svg'); +} + +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .expand, +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .expand { + background-image: url('images/expando-expanded-dark.svg'); +} + +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .collapse, +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .collapse { + background-image: url('images/expando-collapsed-dark.svg'); +} + +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .button:not(.disabled):hover, +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .button:not(.disabled):hover { + background-color: #2f3334; +} + +.vs-dark #workbench\.editor\.htmlPreviewPart .currentFindMatch { + background-color: #515C6A; +} + +.vs-dark #workbench\.editor\.htmlPreviewPart .findScope { + background-color: rgba(58, 61, 65, 0.4); +} + +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .monaco-findInput, +.vs-dark #workbench\.editor\.htmlPreviewPart .find-widget .replace-input { + background-color: #3C3C3C; +} + +/* High Contrast Theming */ +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget { + border: 2px solid #6FC3DF; + background: #0C141F; + box-shadow: none; +} + +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .button:before { + position: relative; + top: 1px; + left: 2px; +} + +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .monaco-checkbox .checkbox:checked + .label { + background-color: rgba(150, 150, 150, 0.3); +} + +.hc-black #workbench\.editor\.htmlPreviewPart .findMatch { + background: none; + border: 1px dotted #f38518; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.hc-black #workbench\.editor\.htmlPreviewPart .currentFindMatch { + background: none; + padding: 1px; + border: 2px solid #f38518; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.hc-black #workbench\.editor\.htmlPreviewPart .findScope { + background: none; + border: 1px dashed #f38518; + opacity: .4; +} + +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .monaco-findInput, +.hc-black #workbench\.editor\.htmlPreviewPart .find-widget .replace-input { + background-color: #000; +} diff --git a/src/vs/workbench/parts/html/browser/simpleFindWidget.ts b/src/vs/workbench/parts/html/browser/simpleFindWidget.ts new file mode 100644 index 0000000000000..980844d9a5eb2 --- /dev/null +++ b/src/vs/workbench/parts/html/browser/simpleFindWidget.ts @@ -0,0 +1,249 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./simpleFindWidget'; +import * as DOM from 'vs/base/browser/dom'; +import { Widget } from 'vs/base/browser/ui/widget'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import * as strings from 'vs/base/common/strings'; +import { + SimpleButton, NLS_CLOSE_BTN_LABEL, NLS_FIND_INPUT_LABEL, NLS_FIND_INPUT_PLACEHOLDER, NLS_NEXT_MATCH_BTN_LABEL + , NLS_PREVIOUS_MATCH_BTN_LABEL, NLS_MATCHES_LOCATION, NLS_NO_RESULTS +} from 'vs/editor/contrib/find/browser/findWidget'; +import { SimpleFindState, SimpleFindStateChangedEvent } from './simpleFindState'; +import { FindModelBoundToWebview } from './simpleFindModel'; + +let MAX_MATCHES_COUNT_WIDTH = 69; +const WIDGET_FIXED_WIDTH = 411 - 69; + +export class SimpleFindWidget extends Widget { + + public domNode: HTMLElement; + + private countElement: HTMLElement; + private _findPart: HTMLElement; + private _findInput: InputBox; + + private nextButton: SimpleButton; + private prevButton: SimpleButton; + private closeButton: SimpleButton; + private isVisible: boolean; + private _state: SimpleFindState; + private _model: FindModelBoundToWebview; + + constructor(parent: HTMLElement, + state: SimpleFindState, + @IContextViewService private contextViewService: IContextViewService, + @IContextMenuService private contextMenuService: IContextMenuService, + @IInstantiationService private instantiationService: IInstantiationService) { + super(); + this._state = state; + this._register(this._state.addChangeListener((e) => this._onStateChanged(e))); + this.create(parent); + } + + private _onStateChanged(e: SimpleFindStateChangedEvent): void { + if (e.searchString) { + // this._findInput.value = this._state.searchString; + this.updateButtons(); + } + if (e.isRevealed) { + if (this._state.isRevealed) { + this.show(); + } else { + this.hide(); + } + } + + if (e.searchString || e.matchesCount || e.matchesPosition) { + let showRedOutline = (this._state.searchString.length > 0 && this._state.matchesCount === 0); + DOM.toggleClass(this.domNode, 'no-results', showRedOutline); + + this.showMessage(); + this.updateButtons(); + } + } + + public set findModel(model: FindModelBoundToWebview) { + this._model = model; + } + + private create(parent: HTMLElement) { + this.domNode = DOM.append(parent, DOM.$('div.find-widget')); + this.onkeyup(this.domNode, (e) => this._onKeyUp(e)); + this._buildFindPart(DOM.append(this.domNode, DOM.$('div.find-part'))); + this._findInput.inputElement.setAttribute('aria-live', 'assertive'); + + this.showMessage(); + this.updateButtons(); + } + + private _buildFindPart(findPart: HTMLElement) { + this._findPart = findPart; + + const input = DOM.append(this._findPart, DOM.$('div')); + this._findInput = this._register(new InputBox(input, this.contextViewService, { + ariaLabel: NLS_FIND_INPUT_LABEL, + placeholder: NLS_FIND_INPUT_PLACEHOLDER + })); + input.classList.add('monaco-findInput'); + this.countElement = DOM.append(this._findPart, DOM.$('.matchesCount')); + this._register(this._findInput.onDidChange(value => this._state.change({ searchString: value }))); + this.onkeyup(this._findInput.inputElement, (e) => this._onInputBoxKeyUp(e)); + + this.prevButton = this._register(new SimpleButton({ + label: NLS_PREVIOUS_MATCH_BTN_LABEL, + className: 'previous', + onTrigger: () => { + if (this._model) { + this._model.moveToPrevMatch(); + } + }, + onKeyDown: (e) => { } + })); + this.nextButton = this._register(new SimpleButton({ + label: NLS_NEXT_MATCH_BTN_LABEL, + className: 'next', + onTrigger: () => { + if (this._model) { + this._model.moveToNextMatch(); + } + }, + onKeyDown: (e) => { } + })); + this.closeButton = this._register(new SimpleButton({ + label: NLS_CLOSE_BTN_LABEL, + className: 'close-fw', + onTrigger: () => { + this._state.change({ isRevealed: false }); + }, + onKeyDown: (e) => { } + })); + this._findPart.appendChild(this.prevButton.domNode); + this._findPart.appendChild(this.nextButton.domNode); + this._findPart.appendChild(this.closeButton.domNode); + } + + public showMessage(): void { + this.countElement.style.minWidth = MAX_MATCHES_COUNT_WIDTH + 'px'; + + const message = this._state.matchesCount === 0 || this._findInput.value.length === 0 ? NLS_NO_RESULTS : + strings.format(NLS_MATCHES_LOCATION, this._state.matchesPosition, this._state.matchesCount); + this._findInput.inputElement.setAttribute('aria-label', message); + this.countElement.textContent = message; + + MAX_MATCHES_COUNT_WIDTH = Math.max(MAX_MATCHES_COUNT_WIDTH, this.countElement.clientWidth); + } + + public updateButtons(): void { + let findInputIsNonEmpty = (this._state.searchString.length > 0); + + this._findInput.setEnabled(this.isVisible); + this.nextButton.setEnabled(this.isVisible && this._state.matchesCount > 0 && findInputIsNonEmpty); + this.prevButton.setEnabled(this.isVisible && this._state.matchesCount > 0 && findInputIsNonEmpty); + this.closeButton.setEnabled(this.isVisible); + } + + public layout(editorWidth: number) { + let collapsedFindWidget = false; + let reducedFindWidget = false; + let narrowFindWidget = false; + if (WIDGET_FIXED_WIDTH + 28 >= editorWidth + 50) { + collapsedFindWidget = true; + } + if (WIDGET_FIXED_WIDTH + 28 >= editorWidth) { + narrowFindWidget = true; + } + if (WIDGET_FIXED_WIDTH + MAX_MATCHES_COUNT_WIDTH + 28 >= editorWidth) { + reducedFindWidget = true; + } + DOM.toggleClass(this.domNode, 'collapsed-find-widget', collapsedFindWidget); + DOM.toggleClass(this.domNode, 'reduced-find-widget', reducedFindWidget); + DOM.toggleClass(this.domNode, 'narrow-find-widget', narrowFindWidget); + } + + /** + * Activates the widget by focusing it, selecting the text, and starting a search. + * Separated from show() so it can be managed separate from state changes + * (i.e. focus is in the page, find is shown, ctrl+f is pressed again. + * Visibility hasn't changed, but the widget should be activated) + * This is consistent with the behavior in other find widgets in VS Code and with Chrome. + * + * @memberOf SimpleFindWidget + */ + public activate() { + this._findInput.focus(); + this._findInput.select({ start: 0, end: this._findInput.value.length }); + this._model.startFind(); + } + + /** + * Shows the widget by changing the visibility. + * Controlled by the isVisible state property. + * + * @private + * + * @memberOf SimpleFindWidget + */ + private show() { + if (!this.isVisible) { + this.isVisible = true; + this.updateButtons(); + this.domNode.classList.add('visible'); + } + } + + public hide() { + if (this.isVisible) { + this.isVisible = false; + this.domNode.classList.remove('visible'); + this.updateButtons(); + this.domNode.blur(); + } + } + + private _onKeyUp(keyboardEvent: IKeyboardEvent): void { + let handled = false; + switch (keyboardEvent.keyCode) { + case KeyCode.Escape: + this._state.change({ isRevealed: false }); + handled = true; + } + + if (handled) { + keyboardEvent.preventDefault(); + keyboardEvent.stopPropagation(); + } + } + + private _onInputBoxKeyUp(keyboardEvent: IKeyboardEvent): void { + let handled = false; + switch (keyboardEvent.keyCode) { + case KeyCode.Enter: + if (keyboardEvent.shiftKey) { + if (this._model) { + this._model.moveToPrevMatch(); + } + } else { + if (this._model) { + this._model.moveToNextMatch(); + } + } + handled = true; + break; + // case KeyCode.Escape: + // this._state.change({ isRevealed: false }); + // handled = true; + // break; + } + if (handled) { + keyboardEvent.preventDefault(); + keyboardEvent.stopPropagation(); + } + } +} diff --git a/src/vs/workbench/parts/html/browser/webview.ts b/src/vs/workbench/parts/html/browser/webview.ts index d7fe4a019ebf6..00a42a420a18d 100644 --- a/src/vs/workbench/parts/html/browser/webview.ts +++ b/src/vs/workbench/parts/html/browser/webview.ts @@ -15,6 +15,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { MenuRegistry } from 'vs/platform/actions/common/actions'; import { IColorTheme } from 'vs/workbench/services/themes/common/themeService'; import { editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; declare interface WebviewElement extends HTMLElement { src: string; @@ -23,6 +24,22 @@ declare interface WebviewElement extends HTMLElement { send(channel: string, ...args: any[]); openDevTools(): any; + findInPage(value: string, options?: WebviewElementFindInPageOptions); + stopFindInPage(action: string); +} + +export class StopFindInPageActions { + static clearSelection = 'clearSelection'; + static keepSelection = 'keepSelection'; + static activateSelection = 'activateSelection'; +} + +export interface WebviewElementFindInPageOptions { + forward?: boolean; + findNext?: boolean; + matchCase?: boolean; + wordStart?: boolean; + medialCapitalAsWordStart?: boolean; } CommandsRegistry.registerCommand('_webview.openDevTools', function () { @@ -50,8 +67,9 @@ export default class Webview { private _disposables: IDisposable[]; private _onDidClickLink = new Emitter(); private _onDidLoadContent = new Emitter<{ stats: any }>(); + private _onFoundInPageResults = new Emitter(); - constructor(parent: HTMLElement, private _styleElement: Element) { + constructor(parent: HTMLElement, private _styleElement: Element, private htmlPreviewFocusContexKey?: IContextKey) { this._webview = document.createElement('webview'); this._webview.style.width = '100%'; @@ -96,6 +114,19 @@ export default class Webview { this._onDidLoadContent.fire({ stats }); return; } + }), + addDisposableListener(this._webview, 'focus', (event: KeyboardEvent) => { + if (this.htmlPreviewFocusContexKey) { + this.htmlPreviewFocusContexKey.set(true); + } + }), + addDisposableListener(this._webview, 'blur', (event: KeyboardEvent) => { + if (this.htmlPreviewFocusContexKey) { + this.htmlPreviewFocusContexKey.reset(); + } + }), + addDisposableListener(this._webview, 'found-in-page', (event) => { + this._onFoundInPageResults.fire(event.result); }) ]; @@ -122,6 +153,10 @@ export default class Webview { return this._onDidLoadContent.event; } + get onFindResults(): Event { + return this._onFoundInPageResults.event; + } + private _send(channel: string, ...args: any[]): void { this._ready .then(() => this._webview.send(channel, ...args)) @@ -145,6 +180,27 @@ export default class Webview { this._send('message', data); } + /** + * Webviews expose a stateful find API. + * Successive calls to find will move forward or backward through onFindResults + * depending on the supplied options. + * + * @param {string} value The string to search for. Empty strings are ignored. + * @param {WebviewElementFindInPageOptions} [options] + * + * @memberOf Webview + */ + public find(value: string, options?: WebviewElementFindInPageOptions): void { + // Searching with an empty value will throw an exception + if (value) { + this._webview.findInPage(value, options); + } + } + + public stopFind(keepSelection?: boolean): void { + this._webview.stopFindInPage(keepSelection ? StopFindInPageActions.keepSelection : StopFindInPageActions.clearSelection); + } + style(theme: IColorTheme): void { const { fontFamily, fontWeight, fontSize } = window.getComputedStyle(this._styleElement); // TODO@theme avoid styleElement @@ -230,3 +286,10 @@ export default class Webview { this._send('styles', value, activeTheme); } } + +export interface FoundInPageResults { + requestId: number; + activeMatchOrdinal: number; + matches: number; + selectionArea: any; +} \ No newline at end of file