diff --git a/src/vs/editor/browser/config/configuration.ts b/src/vs/editor/browser/config/configuration.ts index f4175c0bf63..560e591a878 100644 --- a/src/vs/editor/browser/config/configuration.ts +++ b/src/vs/editor/browser/config/configuration.ts @@ -314,6 +314,7 @@ export class Configuration extends CommonEditorConfiguration { } private readonly _elementSizeObserver: ElementSizeObserver; + private _reservedHeight: number = 0; constructor( isSimpleWidget: boolean, @@ -365,7 +366,7 @@ export class Configuration extends CommonEditorConfiguration { return { extraEditorClassName: Configuration._getExtraEditorClassName(), outerWidth: this._elementSizeObserver.getWidth(), - outerHeight: this._elementSizeObserver.getHeight(), + outerHeight: this._elementSizeObserver.getHeight() - this._reservedHeight, emptySelectionClipboard: browser.isWebKit || browser.isFirefox, pixelRatio: browser.getPixelRatio(), zoomLevel: browser.getZoomLevel(), @@ -380,4 +381,9 @@ export class Configuration extends CommonEditorConfiguration { protected readConfiguration(bareFontInfo: BareFontInfo): FontInfo { return CSSBasedConfiguration.INSTANCE.readConfiguration(bareFontInfo); } + + public reserveHeight(height: number) { + this._reservedHeight = height; + this._recomputeOptions(); + } } diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 5ae4919d192..6243c0b9dd6 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -899,6 +899,8 @@ export interface ICodeEditor extends editorCommon.IEditor { * @internal */ hasModel(): this is IActiveCodeEditor; + + setBanner(bannerDomNode: HTMLElement | null, height: number): void; } /** diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index acb8fb98c0c..19b22fd66a6 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -244,6 +244,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private _decorationTypeKeysToIds: { [decorationTypeKey: string]: string[] }; private _decorationTypeSubtypes: { [decorationTypeKey: string]: { [subtype: string]: boolean } }; + private _bannerDomNode: HTMLElement | null = null; + constructor( domElement: HTMLElement, _options: Readonly, @@ -1490,6 +1492,19 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE Configuration.applyFontInfoSlow(target, this._configuration.options.get(EditorOption.fontInfo)); } + public setBanner(domNode: HTMLElement | null, height: number): void { + if (this._bannerDomNode && this._domElement.contains(this._bannerDomNode)) { + this._domElement.removeChild(this._bannerDomNode); + } + + this._bannerDomNode = domNode; + this._configuration.reserveHeight(height); + + if (this._bannerDomNode) { + this._domElement.prepend(this._bannerDomNode); + } + } + protected _attachModel(model: ITextModel | null): void { if (!model) { this._modelData = null; @@ -1703,6 +1718,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (removeDomNode && this._domElement.contains(removeDomNode)) { this._domElement.removeChild(removeDomNode); } + if (this._bannerDomNode && this._domElement.contains(this._bannerDomNode)) { + this._domElement.removeChild(this._bannerDomNode); + } return model; } diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 507fb4c8b88..b874b8a70f8 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -458,6 +458,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC protected abstract readConfiguration(styling: BareFontInfo): FontInfo; + public abstract reserveHeight(height: number): void; } export const editorConfigurationBaseNode = Object.freeze({ diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 6e693abdf1a..15e7550b29d 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -162,6 +162,7 @@ export interface IConfiguration extends IDisposable { observeReferenceElement(dimension?: IDimension): void; updatePixelRatio(): void; setIsDominatedByLongLines(isDominatedByLongLines: boolean): void; + reserveHeight(height: number): void; } // --- view diff --git a/src/vs/editor/common/modes/unicodeTextModelHighlighter.ts b/src/vs/editor/common/modes/unicodeTextModelHighlighter.ts index 209542ea39f..8ca0649553e 100644 --- a/src/vs/editor/common/modes/unicodeTextModelHighlighter.ts +++ b/src/vs/editor/common/modes/unicodeTextModelHighlighter.ts @@ -6,9 +6,11 @@ import { IRange, Range } from 'vs/editor/common/core/range'; import { Searcher } from 'vs/editor/common/model/textModelSearch'; import * as strings from 'vs/base/common/strings'; +import { IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorkerService'; +import { assertNever } from 'vs/base/common/types'; export class UnicodeTextModelHighlighter { - public static computeUnicodeHighlights(model: IUnicodeCharacterSearcherTarget, options: UnicodeHighlighterOptions, range?: IRange): Range[] { + public static computeUnicodeHighlights(model: IUnicodeCharacterSearcherTarget, options: UnicodeHighlighterOptions, range?: IRange): IUnicodeHighlightsResult { const startLine = range ? range.startLineNumber : 1; const endLine = range ? range.endLineNumber : model.getLineCount(); @@ -23,8 +25,15 @@ export class UnicodeTextModelHighlighter { } const searcher = new Searcher(null, regex); - const result: Range[] = []; + const ranges: Range[] = []; + let hasMore = false; let m: RegExpExecArray | null; + + let ambiguousCharacterCount = 0; + let invisibleCharacterCount = 0; + let nonBasicAsciiCharacterCount = 0; + + forLoop: for (let lineNumber = startLine, lineCount = endLine; lineNumber <= lineCount; lineNumber++) { const lineContent = model.getLineContent(lineNumber); const lineLength = lineContent.length; @@ -51,19 +60,37 @@ export class UnicodeTextModelHighlighter { } } const str = lineContent.substring(startIndex, endIndex); - if (codePointHighlighter.shouldHighlightNonBasicASCII(str) !== SimpleHighlightReason.None) { - result.push(new Range(lineNumber, startIndex + 1, lineNumber, endIndex + 1)); + const highlightReason = codePointHighlighter.shouldHighlightNonBasicASCII(str); - const maxResultLength = 1000; - if (result.length > maxResultLength) { - // TODO@hediet a message should be shown in this case - break; + if (highlightReason !== SimpleHighlightReason.None) { + if (highlightReason === SimpleHighlightReason.Ambiguous) { + ambiguousCharacterCount++; + } else if (highlightReason === SimpleHighlightReason.Invisible) { + invisibleCharacterCount++; + } else if (highlightReason === SimpleHighlightReason.NonBasicASCII) { + nonBasicAsciiCharacterCount++; + } else { + assertNever(highlightReason); } + + const MAX_RESULT_LENGTH = 1000; + if (ranges.length >= MAX_RESULT_LENGTH) { + hasMore = true; + break forLoop; + } + + ranges.push(new Range(lineNumber, startIndex + 1, lineNumber, endIndex + 1)); } } } while (m); } - return result; + return { + ranges, + hasMore, + ambiguousCharacterCount, + invisibleCharacterCount, + nonBasicAsciiCharacterCount + }; } public static computeUnicodeHighlightReason(char: string, options: UnicodeHighlighterOptions): UnicodeHighlighterReason | null { diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index b87215dfe26..8f91c6b9df4 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -18,7 +18,7 @@ import { ensureValidWordDefinition, getWordAtText } from 'vs/editor/common/model import { IInplaceReplaceSupportResult, ILink, TextEdit } from 'vs/editor/common/modes'; import { ILinkComputerTarget, computeLinks } from 'vs/editor/common/modes/linkComputer'; import { BasicInplaceReplace } from 'vs/editor/common/modes/supports/inplaceReplaceSupport'; -import { IDiffComputationResult } from 'vs/editor/common/services/editorWorkerService'; +import { IDiffComputationResult, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorkerService'; import { createMonacoBaseAPI } from 'vs/editor/common/standalone/standaloneBase'; import * as types from 'vs/base/common/types'; import { EditorWorkerHost } from 'vs/editor/common/services/editorWorkerServiceImpl'; @@ -372,10 +372,10 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable { delete this._models[strURL]; } - public async computeUnicodeHighlights(url: string, options: UnicodeHighlighterOptions, range?: IRange): Promise { + public async computeUnicodeHighlights(url: string, options: UnicodeHighlighterOptions, range?: IRange): Promise { const model = this._getModel(url); if (!model) { - return []; + return { ranges: [], hasMore: false, ambiguousCharacterCount: 0, invisibleCharacterCount: 0, nonBasicAsciiCharacterCount: 0 }; } return UnicodeTextModelHighlighter.computeUnicodeHighlights(model, options, range); } diff --git a/src/vs/editor/common/services/editorWorkerService.ts b/src/vs/editor/common/services/editorWorkerService.ts index 753bfabc8db..199dcab3a7a 100644 --- a/src/vs/editor/common/services/editorWorkerService.ts +++ b/src/vs/editor/common/services/editorWorkerService.ts @@ -23,7 +23,7 @@ export interface IEditorWorkerService { readonly _serviceBrand: undefined; canComputeUnicodeHighlights(uri: URI): boolean; - computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise; + computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise; computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise; @@ -38,3 +38,11 @@ export interface IEditorWorkerService { canNavigateValueSet(resource: URI): boolean; navigateValueSet(resource: URI, range: IRange, up: boolean): Promise; } + +export interface IUnicodeHighlightsResult { + ranges: IRange[]; + hasMore: boolean; + nonBasicAsciiCharacterCount: number; + invisibleCharacterCount: number; + ambiguousCharacterCount: number; +} diff --git a/src/vs/editor/common/services/editorWorkerServiceImpl.ts b/src/vs/editor/common/services/editorWorkerServiceImpl.ts index fd310432f73..f5210b5440c 100644 --- a/src/vs/editor/common/services/editorWorkerServiceImpl.ts +++ b/src/vs/editor/common/services/editorWorkerServiceImpl.ts @@ -15,7 +15,7 @@ import { ITextModel } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { EditorSimpleWorker } from 'vs/editor/common/services/editorSimpleWorker'; -import { IDiffComputationResult, IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { IDiffComputationResult, IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorkerService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { regExpFlags } from 'vs/base/common/strings'; @@ -86,7 +86,7 @@ export class EditorWorkerServiceImpl extends Disposable implements IEditorWorker return canSyncModel(this._modelService, uri); } - public computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise { + public computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise { return this._workerManager.withWorker().then(client => client.computedUnicodeHighlights(uri, options, range)); } @@ -475,7 +475,7 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien }); } - public computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise { + public computedUnicodeHighlights(uri: URI, options: UnicodeHighlighterOptions, range?: IRange): Promise { return this._withSyncedResources([uri]).then(proxy => { return proxy.computeUnicodeHighlights(uri.toString(), options, range); }); diff --git a/src/vs/editor/contrib/unicodeHighlighter/bannerController.css b/src/vs/editor/contrib/unicodeHighlighter/bannerController.css new file mode 100644 index 00000000000..c43944d464a --- /dev/null +++ b/src/vs/editor/contrib/unicodeHighlighter/bannerController.css @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.editor-banner { + box-sizing: border-box; + cursor: default; + width: 100%; + font-size: 12px; + display: flex; + overflow: visible; + + height: 26px; + + background: var(--vscode-banner-background); +} + + +.editor-banner .icon-container { + display: flex; + flex-shrink: 0; + align-items: center; + padding: 0 6px 0 10px; +} + +.editor-banner .icon-container.custom-icon { + background-repeat: no-repeat; + background-position: center center; + background-size: 16px; + width: 16px; + padding: 0; + margin: 0 6px 0 10px; +} + +.editor-banner .message-container { + display: flex; + align-items: center; + line-height: 26px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.editor-banner .message-container p { + margin-block-start: 0; + margin-block-end: 0; +} + +.editor-banner .message-actions-container { + flex-grow: 1; + flex-shrink: 0; + line-height: 26px; + margin: 0 4px; +} + +.editor-banner .message-actions-container a.monaco-button { + width: inherit; + margin: 2px 8px; + padding: 0px 12px; +} + +.editor-banner .message-actions-container a { + padding: 3px; + margin-left: 12px; + text-decoration: underline; +} + +.editor-banner .action-container { + padding: 0 10px 0 6px; +} + +.editor-banner { + background-color: var(--vscode-banner-background); +} + +.editor-banner, +.editor-banner .action-container .codicon, +.editor-banner .message-actions-container .monaco-link { + color: var(--vscode-banner-foreground); +} + +.editor-banner .icon-container .codicon { + color: var(--vscode-banner-iconForeground); +} diff --git a/src/vs/editor/contrib/unicodeHighlighter/bannerController.ts b/src/vs/editor/contrib/unicodeHighlighter/bannerController.ts new file mode 100644 index 00000000000..4276d56c1ff --- /dev/null +++ b/src/vs/editor/contrib/unicodeHighlighter/bannerController.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./bannerController'; +import { $, append, clearNode } from 'vs/base/browser/dom'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action } from 'vs/base/common/actions'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILinkDescriptor, Link } from 'vs/platform/opener/browser/link'; +import { widgetClose } from 'vs/platform/theme/common/iconRegistry'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; + +const BANNER_ELEMENT_HEIGHT = 26; + +export class BannerController extends Disposable { + private readonly banner: Banner; + + constructor( + private readonly _editor: ICodeEditor, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + this.banner = this._register(this.instantiationService.createInstance(Banner)); + } + + public hide() { + this._editor.setBanner(null, 0); + this.banner.clear(); + } + + public show(item: IBannerItem) { + this.banner.show({ + ...item, + onClose: () => { + this.hide(); + if (item.onClose) { + item.onClose(); + } + } + }); + this._editor.setBanner(this.banner.element, BANNER_ELEMENT_HEIGHT); + } +} + +// TODO@hediet: Investigate if this can be reused by the workspace banner (bannerPart.ts). +class Banner extends Disposable { + public element: HTMLElement; + + private readonly markdownRenderer: MarkdownRenderer; + + private messageActionsContainer: HTMLElement | undefined; + + private actionBar: ActionBar | undefined; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {}); + + this.element = $('div.editor-banner'); + this.element.tabIndex = 0; + } + + private getAriaLabel(item: IBannerItem): string | undefined { + if (item.ariaLabel) { + return item.ariaLabel; + } + if (typeof item.message === 'string') { + return item.message; + } + + return undefined; + } + + private getBannerMessage(message: MarkdownString | string): HTMLElement { + if (typeof message === 'string') { + const element = $('span'); + element.innerText = message; + return element; + } + + return this.markdownRenderer.render(message).element; + } + + public clear() { + clearNode(this.element); + } + + public show(item: IBannerItem) { + // Clear previous item + clearNode(this.element); + + // Banner aria label + const ariaLabel = this.getAriaLabel(item); + if (ariaLabel) { + this.element.setAttribute('aria-label', ariaLabel); + } + + // Icon + const iconContainer = append(this.element, $('div.icon-container')); + iconContainer.setAttribute('aria-hidden', 'true'); + + if (item.icon) { + iconContainer.appendChild($(`div${ThemeIcon.asCSSSelector(item.icon)}`)); + } + + // Message + const messageContainer = append(this.element, $('div.message-container')); + messageContainer.setAttribute('aria-hidden', 'true'); + messageContainer.appendChild(this.getBannerMessage(item.message)); + + // Message Actions + this.messageActionsContainer = append(this.element, $('div.message-actions-container')); + if (item.actions) { + for (const action of item.actions) { + this._register(this.instantiationService.createInstance(Link, this.messageActionsContainer, { ...action, tabIndex: -1 }, {})); + } + } + + // Action + const actionBarContainer = append(this.element, $('div.action-container')); + this.actionBar = this._register(new ActionBar(actionBarContainer)); + this.actionBar.push(this._register( + new Action( + 'banner.close', + 'Close Banner', + ThemeIcon.asClassName(widgetClose), + true, + () => { + if (typeof item.onClose === 'function') { + item.onClose(); + } + } + ) + ), { icon: true, label: false }); + this.actionBar.setFocusable(false); + } +} + +export interface IBannerItem { + readonly id: string; + readonly icon: ThemeIcon | undefined; + readonly message: string | MarkdownString; + readonly actions?: ILinkDescriptor[]; + readonly ariaLabel?: string; + readonly onClose?: () => void; +} diff --git a/src/vs/editor/contrib/unicodeHighlighter/unicodeHighlighter.ts b/src/vs/editor/contrib/unicodeHighlighter/unicodeHighlighter.ts index 8550c07fef2..5650ff6e2fb 100644 --- a/src/vs/editor/contrib/unicodeHighlighter/unicodeHighlighter.ts +++ b/src/vs/editor/contrib/unicodeHighlighter/unicodeHighlighter.ts @@ -5,6 +5,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { CharCode } from 'vs/base/common/charCode'; +import { Codicon } from 'vs/base/common/codicons'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { InvisibleCharacters } from 'vs/base/common/strings'; @@ -17,32 +18,44 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDecoration, IModelDeltaDecoration, ITextModel, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { UnicodeHighlighterOptions, UnicodeHighlighterReason, UnicodeHighlighterReasonKind, UnicodeTextModelHighlighter } from 'vs/editor/common/modes/unicodeTextModelHighlighter'; -import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorkerService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { HoverAnchor, HoverAnchorType, IEditorHover, IEditorHoverParticipant, IEditorHoverStatusBar, IHoverPart } from 'vs/editor/contrib/hover/hoverTypes'; import { MarkdownHover, renderMarkdownHovers } from 'vs/editor/contrib/hover/markdownHoverParticipant'; +import { BannerController } from 'vs/editor/contrib/unicodeHighlighter/bannerController'; import * as nls from 'vs/nls'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { minimapFindMatch, minimapUnicodeHighlight, overviewRulerFindMatchForeground, overviewRulerUnicodeHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +export const warningIcon = registerIcon('extensions-warning-message', Codicon.warning, nls.localize('warningIcon', 'Icon shown with a warning message in the extensions editor.')); + export class UnicodeHighlighter extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.unicodeHighlighter'; private _highlighter: DocumentUnicodeHighlighter | ViewportUnicodeHighlighter | null = null; private _options: InternalUnicodeHighlightOptions; + private readonly _bannerController: BannerController; + private _bannerClosed: boolean = false; + constructor( private readonly _editor: ICodeEditor, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @IWorkspaceTrustManagementService private readonly _workspaceTrustService: IWorkspaceTrustManagementService, + @IInstantiationService instantiationService: IInstantiationService, ) { super(); + this._bannerController = this._register(instantiationService.createInstance(BannerController, _editor)); + this._register(this._editor.onDidChangeModel(() => { + this._bannerClosed = false; this._updateHighlighter(); })); @@ -70,7 +83,57 @@ export class UnicodeHighlighter extends Disposable implements IEditorContributio super.dispose(); } + private readonly _updateState = (state: IUnicodeHighlightsResult | null): void => { + if (state && state.hasMore) { + if (this._bannerClosed) { + return; + } + + // This document contains many non-basic ASCII characters. + const max = Math.max(state.ambiguousCharacterCount, state.nonBasicAsciiCharacterCount, state.invisibleCharacterCount); + + let data; + if (state.nonBasicAsciiCharacterCount >= max) { + data = { + message: nls.localize('unicodeHighlighting.thisDocumentHasManyNonBasicAsciiUnicodeCharacters', 'This document contains many non-basic ASCII unicode characters'), + command: new DisableHighlightingOfNonBasicAsciiCharactersAction(), + }; + } else if (state.ambiguousCharacterCount >= max) { + data = { + message: nls.localize('unicodeHighlighting.thisDocumentHasManyAmbiguousUnicodeCharacters', 'This document contains many ambiguous unicode characters'), + command: new DisableHighlightingOfAmbiguousCharactersAction(), + }; + } else if (state.invisibleCharacterCount >= max) { + data = { + message: nls.localize('unicodeHighlighting.thisDocumentHasManyInvisibleUnicodeCharacters', 'This document contains many invisible unicode characters'), + command: new DisableHighlightingOfInvisibleCharactersAction(), + }; + } else { + throw new Error('Unreachable'); + } + + this._bannerController.show({ + id: 'unicodeHighlightBanner', + message: data.message, + icon: warningIcon, + actions: [ + { + label: data.command.shortLabel, + href: `command:${data.command.id}` + } + ], + onClose: () => { + this._bannerClosed = true; + }, + }); + } else { + this._bannerController.hide(); + } + }; + private _updateHighlighter(): void { + this._updateState(null); + if (this._highlighter) { this._highlighter.dispose(); this._highlighter = null; @@ -100,9 +163,9 @@ export class UnicodeHighlighter extends Disposable implements IEditorContributio }; if (this._editorWorkerService.canComputeUnicodeHighlights(this._editor.getModel().uri)) { - this._highlighter = new DocumentUnicodeHighlighter(this._editor, highlightOptions, this._editorWorkerService); + this._highlighter = new DocumentUnicodeHighlighter(this._editor, highlightOptions, this._updateState, this._editorWorkerService); } else { - this._highlighter = new ViewportUnicodeHighlighter(this._editor, highlightOptions); + this._highlighter = new ViewportUnicodeHighlighter(this._editor, highlightOptions, this._updateState); } } @@ -156,6 +219,7 @@ class DocumentUnicodeHighlighter extends Disposable { constructor( private readonly _editor: IActiveCodeEditor, private readonly _options: UnicodeHighlighterOptions, + private readonly _updateState: (state: IUnicodeHighlightsResult | null) => void, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, ) { super(); @@ -182,14 +246,20 @@ class DocumentUnicodeHighlighter extends Disposable { const modelVersionId = this._model.getVersionId(); this._editorWorkerService .computedUnicodeHighlights(this._model.uri, this._options) - .then((ranges) => { + .then((info) => { if (this._model.getVersionId() !== modelVersionId) { // model changed in the meantime return; } + this._updateState(info); + const decorations: IModelDeltaDecoration[] = []; - for (const range of ranges) { - decorations.push({ range: range, options: this._options.includeComments ? DECORATION : DECORATION_HIDE_IN_COMMENTS }); + if (!info.hasMore) { + // Don't show decoration if there are too many. + // In this case, a banner is shown. + for (const range of info.ranges) { + decorations.push({ range: range, options: this._options.includeComments ? DECORATION : DECORATION_HIDE_IN_COMMENTS }); + } } this._decorationIds = new Set(this._editor.deltaDecorations( Array.from(this._decorationIds), @@ -218,7 +288,8 @@ class ViewportUnicodeHighlighter extends Disposable { constructor( private readonly _editor: IActiveCodeEditor, - private readonly _options: UnicodeHighlighterOptions + private readonly _options: UnicodeHighlighterOptions, + private readonly _updateState: (state: IUnicodeHighlightsResult | null) => void, ) { super(); @@ -253,12 +324,33 @@ class ViewportUnicodeHighlighter extends Disposable { const ranges = this._editor.getVisibleRanges(); const decorations: IModelDeltaDecoration[] = []; + const totalResult: IUnicodeHighlightsResult = { + ranges: [], + ambiguousCharacterCount: 0, + invisibleCharacterCount: 0, + nonBasicAsciiCharacterCount: 0, + hasMore: false, + }; for (const range of ranges) { - const ranges = UnicodeTextModelHighlighter.computeUnicodeHighlights(this._model, this._options, range); - for (const range of ranges) { + const result = UnicodeTextModelHighlighter.computeUnicodeHighlights(this._model, this._options, range); + for (const r of result.ranges) { + totalResult.ranges.push(r); + } + totalResult.ambiguousCharacterCount += totalResult.ambiguousCharacterCount; + totalResult.invisibleCharacterCount += totalResult.invisibleCharacterCount; + totalResult.nonBasicAsciiCharacterCount += totalResult.nonBasicAsciiCharacterCount; + totalResult.hasMore = totalResult.hasMore || result.hasMore; + } + + if (!totalResult.hasMore) { + // Don't show decorations if there are too many. + // A banner will be shown instead. + for (const range of totalResult.ranges) { decorations.push({ range, options: this._options.includeComments ? DECORATION : DECORATION_HIDE_IN_COMMENTS }); } } + this._updateState(totalResult); + this._decorationIds = new Set(this._editor.deltaDecorations(Array.from(this._decorationIds), decorations)); } @@ -424,6 +516,78 @@ const DECORATION = ModelDecorationOptions.register({ } }); +export class DisableHighlightingOfAmbiguousCharactersAction extends EditorAction { + public static ID = 'editor.action.unicodeHighlight.disableHighlightingOfAmbiguousCharacters'; + public readonly shortLabel = nls.localize('unicodeHighlight.disableHighlightingOfAmbiguousCharacters.shortLabel', ''); + constructor() { + super({ + id: DisableHighlightingOfAmbiguousCharactersAction.ID, + label: nls.localize('action.unicodeHighlight.disableHighlightingOfAmbiguousCharacters', 'Disable Ambiguous Highlight'), + alias: 'Disable highlighting of ambiguous characters', + precondition: undefined + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor, args: any): Promise { + let configurationService = accessor?.get(IConfigurationService); + if (configurationService) { + this.runAction(configurationService); + } + } + + public async runAction(configurationService: IConfigurationService): Promise { + await configurationService.updateValue(unicodeHighlightConfigKeys.ambiguousCharacters, false, ConfigurationTarget.USER); + } +} + +export class DisableHighlightingOfInvisibleCharactersAction extends EditorAction { + public static ID = 'editor.action.unicodeHighlight.disableHighlightingOfInvisibleCharacters'; + public readonly shortLabel = nls.localize('unicodeHighlight.disableHighlightingOfInvisibleCharacters.shortLabel', 'Disable Invisible Highlight'); + constructor() { + super({ + id: DisableHighlightingOfInvisibleCharactersAction.ID, + label: nls.localize('action.unicodeHighlight.disableHighlightingOfInvisibleCharacters', 'Disable highlighting of invisible characters'), + alias: 'Disable highlighting of invisible characters', + precondition: undefined + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor, args: any): Promise { + let configurationService = accessor?.get(IConfigurationService); + if (configurationService) { + this.runAction(configurationService); + } + } + + public async runAction(configurationService: IConfigurationService): Promise { + await configurationService.updateValue(unicodeHighlightConfigKeys.invisibleCharacters, false, ConfigurationTarget.USER); + } +} + +export class DisableHighlightingOfNonBasicAsciiCharactersAction extends EditorAction { + public static ID = 'editor.action.unicodeHighlight.disableHighlightingOfNonBasicAsciiCharacters'; + public readonly shortLabel = nls.localize('unicodeHighlight.disableHighlightingOfNonBasicAsciiCharacters.shortLabel', 'Disable Non ASCII Highlight'); + constructor() { + super({ + id: DisableHighlightingOfNonBasicAsciiCharactersAction.ID, + label: nls.localize('action.unicodeHighlight.dhowDisableHighlightingOfNonBasicAsciiCharacters', 'Disable highlighting of non basic ASCII characters'), + alias: 'Disable highlighting of non basic ASCII characters', + precondition: undefined + }); + } + + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor, args: any): Promise { + let configurationService = accessor?.get(IConfigurationService); + if (configurationService) { + this.runAction(configurationService); + } + } + + public async runAction(configurationService: IConfigurationService): Promise { + await configurationService.updateValue(unicodeHighlightConfigKeys.nonBasicASCII, false, ConfigurationTarget.USER); + } +} + interface ShowExcludeOptionsArgs { codePoint: number; reason: UnicodeHighlighterReason['kind']; @@ -439,6 +603,7 @@ export class ShowExcludeOptions extends EditorAction { precondition: undefined }); } + public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor, args: any): Promise { const { codePoint, reason } = args as ShowExcludeOptionsArgs; @@ -470,28 +635,16 @@ export class ShowExcludeOptions extends EditorAction { ]; if (reason === UnicodeHighlighterReasonKind.Ambiguous) { - options.push({ - label: nls.localize('unicodeHighlight.disableHighlightingOfAmbiguousCharacters', 'Disable highlighting of ambiguous characters'), - run: async () => { - await configurationService.updateValue(unicodeHighlightConfigKeys.ambiguousCharacters, false, ConfigurationTarget.USER); - } - }); + const action = new DisableHighlightingOfAmbiguousCharactersAction(); + options.push({ label: action.label, run: async () => action.runAction(configurationService) }); } else if (reason === UnicodeHighlighterReasonKind.Invisible) { - options.push({ - label: nls.localize('unicodeHighlight.disableHighlightingOfInvisibleCharacters', 'Disable highlighting of invisible characters'), - run: async () => { - await configurationService.updateValue(unicodeHighlightConfigKeys.invisibleCharacters, false, ConfigurationTarget.USER); - } - }); + const action = new DisableHighlightingOfInvisibleCharactersAction(); + options.push({ label: action.label, run: async () => action.runAction(configurationService) }); } else if (reason === UnicodeHighlighterReasonKind.NonBasicAscii) { - options.push({ - label: nls.localize('unicodeHighlight.disableHighlightingOfNonBasicAsciiCharacters', 'Disable highlighting of non basic ASCII characters'), - run: async () => { - await configurationService.updateValue(unicodeHighlightConfigKeys.nonBasicASCII, false, ConfigurationTarget.USER); - } - }); + const action = new DisableHighlightingOfNonBasicAsciiCharactersAction(); + options.push({ label: action.label, run: async () => action.runAction(configurationService) }); } else { expectNever(reason); } @@ -511,5 +664,8 @@ function expectNever(value: never) { throw new Error(`Unexpected value: ${value}`); } +registerEditorAction(DisableHighlightingOfAmbiguousCharactersAction); +registerEditorAction(DisableHighlightingOfInvisibleCharactersAction); +registerEditorAction(DisableHighlightingOfNonBasicAsciiCharactersAction); registerEditorAction(ShowExcludeOptions); registerEditorContribution(UnicodeHighlighter.ID, UnicodeHighlighter); diff --git a/src/vs/editor/test/common/mocks/testConfiguration.ts b/src/vs/editor/test/common/mocks/testConfiguration.ts index ec94f2201ce..044fcae5ac0 100644 --- a/src/vs/editor/test/common/mocks/testConfiguration.ts +++ b/src/vs/editor/test/common/mocks/testConfiguration.ts @@ -47,4 +47,8 @@ export class TestConfiguration extends CommonEditorConfiguration { maxDigitWidth: 10, }, true); } + + public reserveHeight(height: number): void { + throw new Error('Not supported'); + } } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 6792f8509c2..a84e7abca3e 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5117,6 +5117,7 @@ declare namespace monaco.editor { * Apply the same font settings as the editor to `target`. */ applyFontInfo(target: HTMLElement): void; + setBanner(bannerDomNode: HTMLElement | null, height: number): void; } /** diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index d41752d564c..eff636e571d 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -132,7 +132,7 @@ import { IEditorResolverService } from 'vs/workbench/services/editor/common/edit import { IWorkingCopyEditorService, WorkingCopyEditorService } from 'vs/workbench/services/workingCopy/common/workingCopyEditorService'; import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService'; import { BrowserElevatedFileService } from 'vs/workbench/services/files/browser/elevatedFileService'; -import { IDiffComputationResult, IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { IDiffComputationResult, IEditorWorkerService, IUnicodeHighlightsResult } from 'vs/editor/common/services/editorWorkerService'; import { TextEdit, IInplaceReplaceSupportResult } from 'vs/editor/common/modes'; import { ResourceMap } from 'vs/base/common/map'; import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput'; @@ -1866,7 +1866,7 @@ export class TestEditorWorkerService implements IEditorWorkerService { declare readonly _serviceBrand: undefined; canComputeUnicodeHighlights(uri: URI): boolean { return false; } - async computedUnicodeHighlights(uri: URI): Promise { return []; } + async computedUnicodeHighlights(uri: URI): Promise { return { ranges: [], hasMore: false, ambiguousCharacterCount: 0, invisibleCharacterCount: 0, nonBasicAsciiCharacterCount: 0 }; } async computeDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean, maxComputationTime: number): Promise { return null; } canComputeDirtyDiff(original: URI, modified: URI): boolean { return false; } async computeDirtyDiff(original: URI, modified: URI, ignoreTrimWhitespace: boolean): Promise { return null; }