Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial share provider API and UI #182999

Merged
merged 6 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions build/lib/stylelint/vscode-known-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@
"--vscode-problemsErrorIcon-foreground",
"--vscode-problemsInfoIcon-foreground",
"--vscode-problemsWarningIcon-foreground",
"--vscode-problemsSuccessIcon-foreground",
"--vscode-profileBadge-background",
"--vscode-profileBadge-foreground",
"--vscode-progressBar-background",
Expand Down
3 changes: 2 additions & 1 deletion extensions/github/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"enabledApiProposals": [
"contribShareMenu",
"contribEditSessions",
"canonicalUriProvider"
"canonicalUriProvider",
"shareProvider"
],
"contributes": {
"commands": [
Expand Down
2 changes: 2 additions & 0 deletions extensions/github/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { GitBaseExtension } from './typings/git-base';
import { GithubRemoteSourcePublisher } from './remoteSourcePublisher';
import { GithubBranchProtectionProviderManager } from './branchProtection';
import { GitHubCanonicalUriProvider } from './canonicalUriProvider';
import { VscodeDevShareProvider } from './shareProviders';

export function activate(context: ExtensionContext): void {
const disposables: Disposable[] = [];
Expand Down Expand Up @@ -95,6 +96,7 @@ function initializeGitExtension(context: ExtensionContext, logger: LogOutputChan
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler()));
disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI)));
disposables.add(new GitHubCanonicalUriProvider(gitAPI));
disposables.add(new VscodeDevShareProvider(gitAPI));
setGitHubContext(gitAPI, disposables);

commands.executeCommand('setContext', 'git-base.gitEnabled', true);
Expand Down
4 changes: 2 additions & 2 deletions extensions/github/src/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function getRangeOrSelection(lineNumber: number | undefined) {
: vscode.window.activeTextEditor?.selection;
}

function rangeString(range: vscode.Range | undefined) {
export function rangeString(range: vscode.Range | undefined) {
if (!range) {
return '';
}
Expand All @@ -119,7 +119,7 @@ export function notebookCellRangeString(index: number | undefined, range: vscode
return hash;
}

function encodeURIComponentExceptSlashes(path: string) {
export function encodeURIComponentExceptSlashes(path: string) {
// There may be special characters like # and whitespace in the path.
// These characters are not escaped by encodeURI(), so it is not sufficient to
// feed the full URI to encodeURI().
Expand Down
111 changes: 111 additions & 0 deletions extensions/github/src/shareProviders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { API } from './typings/git';
import { getRepositoryFromUrl, repositoryHasGitHubRemote } from './util';
import { encodeURIComponentExceptSlashes, getRepositoryForFile, notebookCellRangeString, rangeString } from './links';

export class VscodeDevShareProvider implements vscode.ShareProvider, vscode.Disposable {
readonly id: string = 'copyVscodeDevLink';
readonly label: string = vscode.l10n.t('Copy vscode.dev Link');
readonly priority: number = 10;


private _hasGitHubRepositories: boolean = false;
private set hasGitHubRepositories(value: boolean) {
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', value);
this._hasGitHubRepositories = value;
this.ensureShareProviderRegistration();
}

private shareProviderRegistration: vscode.Disposable | undefined;
private disposables: vscode.Disposable[] = [];

constructor(private readonly gitAPI: API) {
this.initializeGitHubRepoContext();
}

dispose() {
this.disposables.forEach(d => d.dispose());
}

private initializeGitHubRepoContext() {
if (this.gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
this.hasGitHubRepositories = true;
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
} else {
this.disposables.push(this.gitAPI.onDidOpenRepository(async e => {
await e.status();
if (repositoryHasGitHubRemote(e)) {
vscode.commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
this.hasGitHubRepositories = true;
}
}));
}
this.disposables.push(this.gitAPI.onDidCloseRepository(() => {
if (!this.gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
this.hasGitHubRepositories = false;
}
}));
}

private ensureShareProviderRegistration() {
if (vscode.env.appHost !== 'codespaces' && !this.shareProviderRegistration && this._hasGitHubRepositories) {
const shareProviderRegistration = vscode.window.registerShareProvider({ scheme: 'file' }, this);
this.shareProviderRegistration = shareProviderRegistration;
this.disposables.push(shareProviderRegistration);
} else if (this.shareProviderRegistration && !this._hasGitHubRepositories) {
this.shareProviderRegistration.dispose();
this.shareProviderRegistration = undefined;
}
}

provideShare(item: vscode.ShareableItem, _token: vscode.CancellationToken): vscode.ProviderResult<vscode.Uri> {
const repository = getRepositoryForFile(this.gitAPI, item.resourceUri);
if (!repository) {
return;
}

let repo: { owner: string; repo: string } | undefined;
repository.state.remotes.find(remote => {
if (remote.fetchUrl) {
const foundRepo = getRepositoryFromUrl(remote.fetchUrl);
if (foundRepo && (remote.name === repository.state.HEAD?.upstream?.remote)) {
repo = foundRepo;
return;
} else if (foundRepo && !repo) {
repo = foundRepo;
}
}
return;
});

if (!repo) {
return;
}

const blobSegment = repository?.state.HEAD?.name ? encodeURIComponentExceptSlashes(repository.state.HEAD?.name) : repository?.state.HEAD?.commit;
const filepathSegment = encodeURIComponentExceptSlashes(item.resourceUri.path.substring(repository?.rootUri.path.length));
const rangeSegment = getRangeSegment(item);
return vscode.Uri.parse(`${this.getVscodeDevHost()}/${repo.owner}/${repo.repo}/blob/${blobSegment}${filepathSegment}${rangeSegment}${rangeSegment}`);

}

private getVscodeDevHost(): string {
return `https://${vscode.env.appName.toLowerCase().includes('insiders') ? 'insiders.' : ''}vscode.dev/github`;
}
}

function getRangeSegment(item: vscode.ShareableItem) {
if (item.resourceUri.scheme === 'vscode-notebook-cell') {
const notebookEditor = vscode.window.visibleNotebookEditors.find(editor => editor.notebook.uri.fsPath === item.resourceUri.fsPath);
const cell = notebookEditor?.notebook.getCells().find(cell => cell.document.uri.fragment === item.resourceUri?.fragment);
const cellIndex = cell?.index ?? notebookEditor?.selection.start;
return notebookCellRangeString(cellIndex, item.selection);
}

return rangeString(item.selection);
}
29 changes: 29 additions & 0 deletions extensions/github/src/typings/vscode.proposed.shareProvider.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// https://github.com/microsoft/vscode/issues/176316

declare module 'vscode' {
export interface TreeItem {
shareableItem?: ShareableItem;
}

export interface ShareableItem {
resourceUri: Uri;
selection?: Range;
}

export interface ShareProvider {
readonly id: string;
readonly label: string;
readonly priority: number;

provideShare(item: ShareableItem, token: CancellationToken): ProviderResult<Uri>;
}

export namespace window {
export function registerShareProvider(selector: DocumentSelector, provider: ShareProvider): Disposable;
}
}
1 change: 1 addition & 0 deletions src/vs/base/browser/ui/dialog/dialog.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
/** Dialog: Message Container */
.monaco-dialog-box .dialog-message-row .dialog-message-container {
display: flex;
flex: 1;
joyceerhl marked this conversation as resolved.
Show resolved Hide resolved
flex-direction: column;
overflow: hidden;
text-overflow: ellipsis;
Expand Down
19 changes: 14 additions & 5 deletions src/vs/base/browser/ui/dialog/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface IDialogOptions {
readonly detail?: string;
readonly checkboxLabel?: string;
readonly checkboxChecked?: boolean;
readonly type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending';
readonly type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending' | 'success';
readonly inputs?: IDialogInputOptions[];
readonly keyEventProcessor?: (event: StandardKeyboardEvent) => void;
readonly renderBody?: (container: HTMLElement) => void;
Expand Down Expand Up @@ -58,6 +58,7 @@ export interface IDialogStyles {
readonly errorIconForeground: string | undefined;
readonly warningIconForeground: string | undefined;
readonly infoIconForeground: string | undefined;
readonly successIconForeground: string | undefined;
readonly textLinkForeground: string | undefined;
}

Expand Down Expand Up @@ -172,17 +173,19 @@ export class Dialog extends Disposable {
}

private getIconAriaLabel(): string {
const typeLabel = nls.localize('dialogInfoMessage', 'Info');
let typeLabel = nls.localize('dialogInfoMessage', 'Info');
switch (this.options.type) {
case 'error':
nls.localize('dialogErrorMessage', 'Error');
typeLabel = nls.localize('dialogErrorMessage', 'Error');
break;
case 'warning':
nls.localize('dialogWarningMessage', 'Warning');
typeLabel = nls.localize('dialogWarningMessage', 'Warning');
break;
case 'pending':
nls.localize('dialogPendingMessage', 'In Progress');
typeLabel = nls.localize('dialogPendingMessage', 'In Progress');
break;
case 'success':
typeLabel = nls.localize('dialogSuccessMessage', 'Success');
case 'none':
case 'info':
case 'question':
Expand Down Expand Up @@ -371,6 +374,9 @@ export class Dialog extends Disposable {
case 'warning':
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.dialogWarning));
break;
case 'success':
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.check));
break;
case 'pending':
this.iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), spinModifierClassName);
break;
Expand Down Expand Up @@ -455,6 +461,9 @@ export class Dialog extends Disposable {
case 'warning':
color = style.warningIconForeground;
break;
case 'success':
color = style.successIconForeground;
break;
default:
color = style.infoIconForeground;
break;
Expand Down
10 changes: 9 additions & 1 deletion src/vs/base/common/severity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ enum Severity {
Ignore = 0,
Info = 1,
Warning = 2,
Error = 3
Error = 3,
Success = 4
}

namespace Severity {
Expand All @@ -19,6 +20,7 @@ namespace Severity {
const _warn = 'warn';
const _info = 'info';
const _ignore = 'ignore';
const _success = 'success';

/**
* Parses 'error', 'warning', 'warn', 'info' in call casings
Expand All @@ -40,6 +42,11 @@ namespace Severity {
if (strings.equalsIgnoreCase(_info, value)) {
return Severity.Info;
}

if (strings.equalsIgnoreCase(_success, value)) {
return Severity.Success;
}

return Severity.Ignore;
}

Expand All @@ -48,6 +55,7 @@ namespace Severity {
case Severity.Error: return _error;
case Severity.Warning: return _warning;
case Severity.Info: return _info;
case Severity.Success: return _success;
default: return _ignore;
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/vs/editor/common/standalone/standaloneEnums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,8 @@ export enum MarkerSeverity {
Hint = 1,
Info = 2,
Warning = 4,
Error = 8
Error = 8,
Success = 16
}

export enum MarkerTag {
Expand Down
3 changes: 2 additions & 1 deletion src/vs/monaco.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ declare namespace monaco {
Hint = 1,
Info = 2,
Warning = 4,
Error = 8
Error = 8,
Success = 16
}

export class CancellationTokenSource {
Expand Down
2 changes: 2 additions & 0 deletions src/vs/platform/actions/common/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ export class MenuId {
static readonly MenubarViewMenu = new MenuId('MenubarViewMenu');
static readonly MenubarHomeMenu = new MenuId('MenubarHomeMenu');
static readonly OpenEditorsContext = new MenuId('OpenEditorsContext');
static readonly OpenEditorsContextShare = new MenuId('OpenEditorsContextShare');
static readonly ProblemsPanelContext = new MenuId('ProblemsPanelContext');
static readonly SCMChangeContext = new MenuId('SCMChangeContext');
static readonly SCMResourceContext = new MenuId('SCMResourceContext');
static readonly SCMResourceContextShare = new MenuId('SCMResourceContextShare');
static readonly SCMResourceFolderContext = new MenuId('SCMResourceFolderContext');
static readonly SCMResourceGroupContext = new MenuId('SCMResourceGroupContext');
static readonly SCMSourceControl = new MenuId('SCMSourceControl');
Expand Down
9 changes: 7 additions & 2 deletions src/vs/platform/dialogs/common/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,12 @@ export interface IPromptResultWithCancel<T> extends IPromptResult<T> {

export type IDialogResult = IConfirmationResult | IInputResult | IPromptResult<unknown>;

export type DialogType = 'none' | 'info' | 'error' | 'question' | 'warning';
export type DialogType = 'none' | 'info' | 'error' | 'question' | 'warning' | 'success';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bpasero this PR introduces a success/confirmation dialog to support a Share experience, given prior art in Powerpoint/Word, e.g.:
image

This also addresses a pain point where copy link actions today do not display success/confirmation and can fail silently. I would appreciate your review of the dialog-related changes here please--thank you!


export interface IInputBox<T> {
readonly value: string;
readonly action?: IPromptButton<T>;
}

export interface ICheckbox {
readonly label: string;
Expand Down Expand Up @@ -403,7 +408,7 @@ export abstract class AbstractDialogHandler implements IDialogHandler {
}

if (typeof type === 'number') {
return (type === Severity.Info) ? 'info' : (type === Severity.Error) ? 'error' : (type === Severity.Warning) ? 'warning' : 'none';
return (type === Severity.Info) ? 'info' : (type === Severity.Error) ? 'error' : (type === Severity.Warning) ? 'warning' : (type === Severity.Success) ? 'success' : 'none';
}

return undefined;
Expand Down
3 changes: 3 additions & 0 deletions src/vs/platform/markers/common/markers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export enum MarkerSeverity {
Info = 2,
Warning = 4,
Error = 8,
Success = 16
}

export namespace MarkerSeverity {
Expand All @@ -69,6 +70,7 @@ export namespace MarkerSeverity {
case Severity.Error: return MarkerSeverity.Error;
case Severity.Warning: return MarkerSeverity.Warning;
case Severity.Info: return MarkerSeverity.Info;
case Severity.Success: return MarkerSeverity.Success;
case Severity.Ignore: return MarkerSeverity.Hint;
}
}
Expand All @@ -79,6 +81,7 @@ export namespace MarkerSeverity {
case MarkerSeverity.Warning: return Severity.Warning;
case MarkerSeverity.Info: return Severity.Info;
case MarkerSeverity.Hint: return Severity.Ignore;
case MarkerSeverity.Success: return Severity.Success;
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/vs/platform/severityIcon/browser/media/severityIcon.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,12 @@
.preferences-editor .codicon.codicon-info {
color: var(--vscode-problemsInfoIcon-foreground);
}

.monaco-editor .zone-widget .codicon.codicon-check,
.markers-panel .marker-icon.info, .markers-panel .marker-icon .codicon.codicon-check,
.text-search-provider-messages .providerMessage .codicon.codicon-check,
.extensions-viewlet > .extensions .codicon.codicon-check,
.extension-editor .codicon.codicon-check,
.preferences-editor .codicon.codicon-check {
color: var(--vscode-problemsSuccessIcon-foreground);
}