Adds MarkdownString support to SCM validations

This commit is contained in:
Eric Amodio 2021-08-04 18:40:35 -04:00
parent 8c4f6d75d9
commit a4e75b4ec1
8 changed files with 92 additions and 21 deletions

View file

@ -784,7 +784,7 @@ declare module 'vscode' {
/**
* The validation message to display.
*/
readonly message: string;
readonly message: string | MarkdownString;
/**
* The validation type.
@ -800,7 +800,7 @@ declare module 'vscode' {
/**
* Shows a transient contextual message on the input.
*/
showValidationMessage(message: string, type: SourceControlInputBoxValidationType): void;
showValidationMessage(message: string | MarkdownString, type: SourceControlInputBoxValidationType): void;
/**
* A validation function for the input box. It's possible to change

View file

@ -14,6 +14,7 @@ import { ISplice, Sequence } from 'vs/base/common/sequence';
import { CancellationToken } from 'vs/base/common/cancellation';
import { MarshalledId } from 'vs/base/common/marshalling';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IMarkdownString } from 'vs/base/common/htmlContent';
class MainThreadSCMResourceGroup implements ISCMResourceGroup {
@ -438,7 +439,7 @@ export class MainThreadSCM implements MainThreadSCMShape {
repository.input.setFocus();
}
$showValidationMessage(sourceControlHandle: number, message: string, type: InputValidationType) {
$showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType) {
const repository = this._repositories.get(sourceControlHandle);
if (!repository) {
return;

View file

@ -1082,7 +1082,7 @@ export interface MainThreadSCMShape extends IDisposable {
$setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void;
$setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void;
$setInputBoxFocus(sourceControlHandle: number): void;
$showValidationMessage(sourceControlHandle: number, message: string, type: InputValidationType): void;
$showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): void;
$setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void;
}
@ -1757,7 +1757,7 @@ export interface ExtHostSCMShape {
$provideOriginalResource(sourceControlHandle: number, uri: UriComponents, token: CancellationToken): Promise<UriComponents | null>;
$onInputBoxValueChange(sourceControlHandle: number, value: string): void;
$executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number, preserveFocus: boolean): Promise<void>;
$validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string, number] | undefined>;
$validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>;
$setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise<void>;
}

View file

@ -20,6 +20,8 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import { MarshalledId } from 'vs/base/common/marshalling';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { MarkdownString } from 'vs/workbench/api/common/extHostTypeConverters';
type ProviderHandle = number;
type GroupHandle = number;
@ -275,7 +277,7 @@ export class ExtHostSCMInputBox implements vscode.SourceControlInputBox {
this._proxy.$setInputBoxFocus(this._sourceControlHandle);
}
showValidationMessage(message: string, type: vscode.SourceControlInputBoxValidationType) {
showValidationMessage(message: string | vscode.MarkdownString, type: vscode.SourceControlInputBoxValidationType) {
checkProposedApiEnabled(this._extension);
this._proxy.$showValidationMessage(this._sourceControlHandle, message, type as any);
@ -770,7 +772,7 @@ export class ExtHostSCM implements ExtHostSCMShape {
return group.$executeResourceCommand(handle, preserveFocus);
}
$validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string, number] | undefined> {
$validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined> {
this.logService.trace('ExtHostSCM#$validateInput', sourceControlHandle);
const sourceControl = this._sourceControls.get(sourceControlHandle);
@ -788,7 +790,12 @@ export class ExtHostSCM implements ExtHostSCMShape {
return Promise.resolve(undefined);
}
return Promise.resolve<[string, number]>([result.message, result.type]);
const message = MarkdownString.fromStrict(result.message);
if (!message) {
return Promise.resolve(undefined);
}
return Promise.resolve<[string | IMarkdownString, number]>([message, result.type]);
});
}

View file

@ -215,6 +215,16 @@
border-top: none;
}
.scm-editor-validation p {
margin: 0;
padding: 0;
}
.scm-editor-validation a {
-webkit-user-select: none;
user-select: none;
}
.scm-view .scm-editor-placeholder {
position: absolute;
pointer-events: none;

View file

@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { basename, dirname } from 'vs/base/common/resources';
import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
import { append, $, Dimension, asCSSUrl } from 'vs/base/browser/dom';
import { append, $, Dimension, asCSSUrl, trackFocus } from 'vs/base/browser/dom';
import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm';
import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels';
@ -22,7 +22,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, MenuRegistry, Action2 } from 'vs/platform/actions/common/actions';
import { IAction, ActionRunner } from 'vs/base/common/actions';
import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
import { IThemeService, registerThemingParticipant, IFileIconTheme, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IThemeService, registerThemingParticipant, IFileIconTheme, ThemeIcon, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider } from './util';
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
import { WorkbenchCompressibleObjectTree, IOpenEvent } from 'vs/platform/list/browser/listService';
@ -56,7 +56,7 @@ import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEdito
import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu';
import * as platform from 'vs/base/common/platform';
import { compare, format } from 'vs/base/common/strings';
import { inputPlaceholderForeground, inputValidationInfoBorder, inputValidationWarningBorder, inputValidationErrorBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBackground, inputValidationErrorForeground, inputBackground, inputForeground, inputBorder, focusBorder, registerColor, contrastBorder, editorSelectionBackground, selectionBackground } from 'vs/platform/theme/common/colorRegistry';
import { inputPlaceholderForeground, inputValidationInfoBorder, inputValidationWarningBorder, inputValidationErrorBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBackground, inputValidationErrorForeground, inputBackground, inputForeground, inputBorder, focusBorder, registerColor, contrastBorder, editorSelectionBackground, selectionBackground, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { Schemas } from 'vs/base/common/network';
@ -81,6 +81,7 @@ import { Selection } from 'vs/editor/common/core/selection';
import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands';
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer';
type TreeElement = ISCMRepository | ISCMInput | ISCMResourceGroup | IResourceNode<ISCMResource, ISCMResourceGroup> | ISCMResource;
@ -1470,6 +1471,7 @@ class SCMInputWidget extends Disposable {
private validation: IInputValidation | undefined;
private validationDisposable: IDisposable = Disposable.None;
private validationHasFocus: boolean = false;
private _validationTimer: any;
// This is due to "Setup height change listener on next tick" above
@ -1488,7 +1490,7 @@ class SCMInputWidget extends Disposable {
return;
}
this.validationDisposable.dispose();
this.clearValidation();
this.editorContainer.classList.remove('synthetic-focus');
this.repositoryDisposables.dispose();
@ -1556,6 +1558,7 @@ class SCMInputWidget extends Disposable {
}));
this.repositoryDisposables.add(input.onDidChangeFocus(() => this.focus()));
this.repositoryDisposables.add(input.onDidChangeValidationMessage((e) => this.setValidation(e, { focus: true, timeout: true })));
this.repositoryDisposables.add(input.onDidChangeValidateInput((e) => triggerValidation()));
// Keep API in sync with model, update placeholder visibility and validate
const updatePlaceholderVisibility = () => this.placeholderTextContainer.classList.toggle('hidden', textModel.getValueLength() > 0);
@ -1640,9 +1643,10 @@ class SCMInputWidget extends Disposable {
@IModeService private modeService: IModeService,
@IKeybindingService private keybindingService: IKeybindingService,
@IConfigurationService private configurationService: IConfigurationService,
@IInstantiationService instantiationService: IInstantiationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ISCMViewService private readonly scmViewService: ISCMViewService,
@IContextViewService private readonly contextViewService: IContextViewService
@IContextViewService private readonly contextViewService: IContextViewService,
@IOpenerService private readonly openerService: IOpenerService,
) {
super();
@ -1705,7 +1709,12 @@ class SCMInputWidget extends Disposable {
}));
this._register(this.inputEditor.onDidBlurEditorText(() => {
this.editorContainer.classList.remove('synthetic-focus');
this.validationDisposable.dispose();
setTimeout(() => {
if (!this.validation || !this.validationHasFocus) {
this.clearValidation();
}
}, 0);
}));
const firstLineKey = contextKeyService2.createKey('scmInputIsInFirstPosition', false);
@ -1778,7 +1787,7 @@ class SCMInputWidget extends Disposable {
}
private renderValidation(): void {
this.validationDisposable.dispose();
this.clearValidation();
this.editorContainer.classList.toggle('validation-info', this.validation?.type === InputValidationType.Information);
this.editorContainer.classList.toggle('validation-warning', this.validation?.type === InputValidationType.Warning);
@ -1788,6 +1797,8 @@ class SCMInputWidget extends Disposable {
return;
}
const disposables = new DisposableStore();
this.validationDisposable = this.contextViewService.showContextView({
getAnchor: () => this.editorContainer,
render: container => {
@ -1796,9 +1807,36 @@ class SCMInputWidget extends Disposable {
element.classList.toggle('validation-warning', this.validation!.type === InputValidationType.Warning);
element.classList.toggle('validation-error', this.validation!.type === InputValidationType.Error);
element.style.width = `${this.editorContainer.clientWidth}px`;
element.textContent = this.validation!.message;
const message = this.validation!.message;
if (typeof message === 'string') {
element.textContent = message;
} else {
const tracker = trackFocus(element);
disposables.add(tracker);
disposables.add(tracker.onDidFocus(() => (this.validationHasFocus = true)));
disposables.add(tracker.onDidBlur(() => {
this.validationHasFocus = false;
this.contextViewService.hideContextView();
}));
const { element: mdElement } = this.instantiationService.createInstance(MarkdownRenderer, {}).render(message, {
actionHandler: {
callback: (content) => {
this.openerService.open(content, { allowCommands: typeof message !== 'string' && message.isTrusted });
this.contextViewService.hideContextView();
},
disposeables: disposables
},
});
element.appendChild(mdElement);
}
return Disposable.None;
},
onHide: () => {
this.validationHasFocus = false;
disposables.dispose();
},
anchorAlignment: AnchorAlignment.LEFT
});
}
@ -1833,16 +1871,29 @@ class SCMInputWidget extends Disposable {
clearValidation(): void {
this.validationDisposable.dispose();
this.validationHasFocus = false;
}
override dispose(): void {
this.input = undefined;
this.repositoryDisposables.dispose();
this.validationDisposable.dispose();
this.clearValidation();
super.dispose();
}
}
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
const link = theme.getColor(textLinkForeground);
if (link) {
collector.addRule(`.scm-editor-validation a { color: ${link}; }`);
}
const activeLink = theme.getColor(textLinkActiveForeground);
if (activeLink) {
collector.addRule(`.scm-editor-validation a:active, .scm-editor-validation a:hover { color: ${activeLink}; }`);
}
});
export class SCMViewPane extends ViewPane {
private _onDidLayout: Emitter<void>;

View file

@ -12,6 +12,7 @@ import { ISequence } from 'vs/base/common/sequence';
import { IAction } from 'vs/base/common/actions';
import { IMenu } from 'vs/platform/actions/common/actions';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IMarkdownString } from 'vs/base/common/htmlContent';
export const VIEWLET_ID = 'workbench.view.scm';
export const VIEW_PANE_ID = 'workbench.scm';
@ -77,7 +78,7 @@ export const enum InputValidationType {
}
export interface IInputValidation {
message: string;
message: string | IMarkdownString;
type: InputValidationType;
}
@ -114,7 +115,7 @@ export interface ISCMInput {
setFocus(): void;
readonly onDidChangeFocus: Event<void>;
showValidationMessage(message: string, type: InputValidationType): void;
showValidationMessage(message: string | IMarkdownString, type: InputValidationType): void;
readonly onDidChangeValidationMessage: Event<IInputValidation>;
showNextHistoryValue(): void;

View file

@ -10,6 +10,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { HistoryNavigator2 } from 'vs/base/common/history';
import { IMarkdownString } from 'vs/base/common/htmlContent';
class SCMInput implements ISCMInput {
@ -57,7 +58,7 @@ class SCMInput implements ISCMInput {
private readonly _onDidChangeFocus = new Emitter<void>();
readonly onDidChangeFocus: Event<void> = this._onDidChangeFocus.event;
showValidationMessage(message: string, type: InputValidationType): void {
showValidationMessage(message: string | IMarkdownString, type: InputValidationType): void {
this._onDidChangeValidationMessage.fire({ message: message, type: type });
}