Skip to content

Commit

Permalink
Add a Share menu and a share vscode.dev command (#152765)
Browse files Browse the repository at this point in the history
* Add a share menu
Fixes #146309

* Add vscod.dev command in github extension

* Make share menu proposed

* Add share submenu into editor context

* Add proposed to editor share menu
  • Loading branch information
alexr00 committed Jun 27, 2022
1 parent e71b610 commit ffe53e8
Show file tree
Hide file tree
Showing 13 changed files with 191 additions and 18 deletions.
23 changes: 23 additions & 0 deletions extensions/github/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,41 @@
"supported": true
}
},
"enabledApiProposals": [
"contribShareMenu"
],
"contributes": {
"commands": [
{
"command": "github.publish",
"title": "Publish to GitHub"
},
{
"command": "github.copyVscodeDevLink",
"title": "Copy vscode.dev Link"
}
],
"menus": {
"commandPalette": [
{
"command": "github.publish",
"when": "git-base.gitEnabled"
},
{
"command": "github.copyVscodeDevLink",
"when": "false"
}
],
"file/share": [
{
"command": "github.copyVscodeDevLink",
"when": "github.hasGitHubRepo"
}
],
"editor/context/share": [
{
"command": "github.copyVscodeDevLink",
"when": "github.hasGitHubRepo"
}
]
},
Expand Down
12 changes: 12 additions & 0 deletions extensions/github/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as vscode from 'vscode';
import { API as GitAPI } from './typings/git';
import { publishRepository } from './publish';
import { DisposableStore } from './util';
import { getPermalink } from './links';

export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
const disposables = new DisposableStore();
Expand All @@ -19,5 +20,16 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
}
}));

disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLink', async () => {
try {
const permalink = getPermalink(gitAPI, 'https://vscode.dev/github');
if (permalink) {
vscode.env.clipboard.writeText(permalink);
}
} catch (err) {
vscode.window.showErrorMessage(err.message);
}
}));

return disposables;
}
20 changes: 18 additions & 2 deletions extensions/github/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

import { commands, Disposable, ExtensionContext, extensions } from 'vscode';
import { GithubRemoteSourceProvider } from './remoteSourceProvider';
import { GitExtension } from './typings/git';
import { API, GitExtension } from './typings/git';
import { registerCommands } from './commands';
import { GithubCredentialProviderManager } from './credentialProvider';
import { DisposableStore } from './util';
import { DisposableStore, repositoryHasGitHubRemote } from './util';
import { GithubPushErrorHandler } from './pushErrorHandler';
import { GitBaseExtension } from './typings/git-base';
import { GithubRemoteSourcePublisher } from './remoteSourcePublisher';
Expand Down Expand Up @@ -48,6 +48,21 @@ function initializeGitBaseExtension(): Disposable {
return disposables;
}

function setGitHubContext(gitAPI: API, disposables: DisposableStore) {
if (gitAPI.repositories.find(repo => repositoryHasGitHubRemote(repo))) {
commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
} else {
const openRepoDisposable = gitAPI.onDidOpenRepository(async e => {
await e.status();
if (repositoryHasGitHubRemote(e)) {
commands.executeCommand('setContext', 'github.hasGitHubRepo', true);
openRepoDisposable.dispose();
}
});
disposables.add(openRepoDisposable);
}
}

function initializeGitExtension(): Disposable {
const disposables = new DisposableStore();

Expand All @@ -64,6 +79,7 @@ function initializeGitExtension(): Disposable {
disposables.add(new GithubCredentialProviderManager(gitAPI));
disposables.add(gitAPI.registerPushErrorHandler(new GithubPushErrorHandler()));
disposables.add(gitAPI.registerRemoteSourcePublisher(new GithubRemoteSourcePublisher(gitAPI)));
setGitHubContext(gitAPI, disposables);

commands.executeCommand('setContext', 'git-base.gitEnabled', true);
} else {
Expand Down
78 changes: 78 additions & 0 deletions extensions/github/src/links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*---------------------------------------------------------------------------------------------
* 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 as GitAPI, Repository } from './typings/git';
import { getRepositoryFromUrl } from './util';

export function isFileInRepo(repository: Repository, file: vscode.Uri): boolean {
return file.path.toLowerCase() === repository.rootUri.path.toLowerCase() ||
(file.path.toLowerCase().startsWith(repository.rootUri.path.toLowerCase()) &&
file.path.substring(repository.rootUri.path.length).startsWith('/'));
}

export function getRepositoryForFile(gitAPI: GitAPI, file: vscode.Uri): Repository | undefined {
for (const repository of gitAPI.repositories) {
if (isFileInRepo(repository, file)) {
return repository;
}
}
return undefined;
}

function getFileAndPosition(): { uri: vscode.Uri | undefined; range: vscode.Range | undefined } {
let uri: vscode.Uri | undefined;
let range: vscode.Range | undefined;
if (vscode.window.activeTextEditor) {
uri = vscode.window.activeTextEditor.document.uri;
range = vscode.window.activeTextEditor.selection;
}
return { uri, range };
}

function rangeString(range: vscode.Range | undefined) {
if (!range) {
return '';
}
let hash = `#L${range.start.line + 1}`;
if (range.start.line !== range.end.line) {
hash += `-L${range.end.line + 1}`;
}
return hash;
}

export function getPermalink(gitAPI: GitAPI, hostPrefix?: string): string | undefined {
hostPrefix = hostPrefix ?? 'https://github.com';
const { uri, range } = getFileAndPosition();
if (!uri) {
return;
}
const gitRepo = getRepositoryForFile(gitAPI, uri);
if (!gitRepo) {
return;
}
let repo: { owner: string; repo: string } | undefined;
gitRepo.state.remotes.find(remote => {
if (remote.fetchUrl) {
const foundRepo = getRepositoryFromUrl(remote.fetchUrl);
if (foundRepo && (remote.name === gitRepo.state.HEAD?.upstream?.remote)) {
repo = foundRepo;
return;
} else if (foundRepo && !repo) {
repo = foundRepo;
}
}
return;
});
if (!repo) {
return;
}

const commitHash = gitRepo.state.HEAD?.commit;
const pathSegment = uri.path.substring(gitRepo.rootUri.path.length);

return `${hostPrefix}/${repo.owner}/${repo.repo}/blob/${commitHash
}${pathSegment}${rangeString(range)}`;
}
12 changes: 1 addition & 11 deletions extensions/github/src/remoteSourceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,7 @@ import { workspace } from 'vscode';
import { RemoteSourceProvider, RemoteSource } from './typings/git-base';
import { getOctokit } from './auth';
import { Octokit } from '@octokit/rest';

function getRepositoryFromUrl(url: string): { owner: string; repo: string } | undefined {
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\.git/i.exec(url)
|| /^git@github\.com:([^/]+)\/([^/]+)\.git/i.exec(url);
return match ? { owner: match[1], repo: match[2] } : undefined;
}

function getRepositoryFromQuery(query: string): { owner: string; repo: string } | undefined {
const match = /^([^/]+)\/([^/]+)$/i.exec(query);
return match ? { owner: match[1], repo: match[2] } : undefined;
}
import { getRepositoryFromQuery, getRepositoryFromUrl } from './util';

function asRemoteSource(raw: any): RemoteSource {
const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol');
Expand Down
16 changes: 16 additions & 0 deletions extensions/github/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { Repository } from './typings/git';

export class DisposableStore {

Expand All @@ -21,3 +22,18 @@ export class DisposableStore {
this.disposables.clear();
}
}

export function getRepositoryFromUrl(url: string): { owner: string; repo: string } | undefined {
const match = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(\.git)?$/i.exec(url)
|| /^git@github\.com:([^/]+)\/([^/]+?)(\.git)?$/i.exec(url);
return match ? { owner: match[1], repo: match[2] } : undefined;
}

export function getRepositoryFromQuery(query: string): { owner: string; repo: string } | undefined {
const match = /^([^/]+)\/([^/]+)$/i.exec(query);
return match ? { owner: match[1], repo: match[2] } : undefined;
}

export function repositoryHasGitHubRemote(repository: Repository) {
return !!repository.state.remotes.find(remote => remote.fetchUrl ? getRepositoryFromUrl(remote.fetchUrl) : undefined);
}
1 change: 1 addition & 0 deletions src/vs/editor/contrib/clipboard/browser/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const CopyAction = supportsCopy ? registerCommand(new MultiCommand({

MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { submenu: MenuId.MenubarCopy, title: { value: nls.localize('copy as', "Copy As"), original: 'Copy As', }, group: '2_ccp', order: 3 });
MenuRegistry.appendMenuItem(MenuId.EditorContext, { submenu: MenuId.EditorContextCopy, title: { value: nls.localize('copy as', "Copy As"), original: 'Copy As', }, group: CLIPBOARD_CONTEXT_MENU_GROUP, order: 3 });
MenuRegistry.appendMenuItem(MenuId.EditorContext, { submenu: MenuId.EditorContextShare, title: { value: nls.localize('share', "Share"), original: 'Share', }, group: '11_share', order: -1 });

export const PasteAction = supportsPaste ? registerCommand(new MultiCommand({
id: 'editor.action.clipboardPasteAction',
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 @@ -59,6 +59,7 @@ export class MenuId {
static readonly SimpleEditorContext = new MenuId('SimpleEditorContext');
static readonly EditorContextCopy = new MenuId('EditorContextCopy');
static readonly EditorContextPeek = new MenuId('EditorContextPeek');
static readonly EditorContextShare = new MenuId('EditorContextShare');
static readonly EditorTitle = new MenuId('EditorTitle');
static readonly EditorTitleRun = new MenuId('EditorTitleRun');
static readonly EditorTitleContext = new MenuId('EditorTitleContext');
Expand All @@ -85,6 +86,7 @@ export class MenuId {
static readonly MenubarPreferencesMenu = new MenuId('MenubarPreferencesMenu');
static readonly MenubarRecentMenu = new MenuId('MenubarRecentMenu');
static readonly MenubarSelectionMenu = new MenuId('MenubarSelectionMenu');
static readonly MenubarShare = new MenuId('MenubarShare');
static readonly MenubarSwitchEditorMenu = new MenuId('MenubarSwitchEditorMenu');
static readonly MenubarSwitchGroupMenu = new MenuId('MenubarSwitchGroupMenu');
static readonly MenubarTerminalMenu = new MenuId('MenubarTerminalMenu');
Expand Down
19 changes: 14 additions & 5 deletions src/vs/platform/actions/common/menuService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,20 @@ class Menu implements IMenu {
const activeActions: Array<MenuItemAction | SubmenuItemAction> = [];
for (const item of items) {
if (this._contextKeyService.contextMatchesRules(item.when)) {
const action = isIMenuItem(item)
? new MenuItemAction(item.command, item.alt, options, this._contextKeyService, this._commandService)
: new SubmenuItemAction(item, this._menuService, this._contextKeyService, options);

activeActions.push(action);
let action: MenuItemAction | SubmenuItemAction | undefined;
if (isIMenuItem(item)) {
action = new MenuItemAction(item.command, item.alt, options, this._contextKeyService, this._commandService);
} else {
action = new SubmenuItemAction(item, this._menuService, this._contextKeyService, options);
if (action.actions.length === 0) {
action.dispose();
action = undefined;
}
}

if (action) {
activeActions.push(action);
}
}
}
if (activeActions.length > 0) {
Expand Down
7 changes: 7 additions & 0 deletions src/vs/workbench/browser/parts/editor/editor.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,13 @@ MenuRegistry.appendMenuItem(MenuId.MenubarRecentMenu, {
order: 1
});

MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
title: localize('miShare', "Share"),
submenu: MenuId.MenubarShare,
group: '45_share',
order: 1,
});

// Layout menu
MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
group: '2_appearance',
Expand Down
12 changes: 12 additions & 0 deletions src/vs/workbench/services/actions/common/menusExtensionPoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ const apiMenus: IAPIMenu[] = [
id: MenuId.EditorContextCopy,
description: localize('menus.editorContextCopyAs', "'Copy as' submenu in the editor context menu")
},
{
key: 'editor/context/share',
id: MenuId.EditorContextShare,
description: localize('menus.editorContextShare', "'Share' submenu in the editor context menu"),
proposed: 'contribShareMenu'
},
{
key: 'explorer/context',
id: MenuId.ExplorerContext,
Expand Down Expand Up @@ -249,6 +255,12 @@ const apiMenus: IAPIMenu[] = [
description: localize('file.newFile', "The 'New File...' quick pick, shown on welcome page and File menu."),
supportsSubmenus: false,
},
{
key: 'file/share',
id: MenuId.MenubarShare,
description: localize('menus.share', "Share submenu shown in the top level File menu."),
proposed: 'contribShareMenu'
},
{
key: 'editor/inlineCompletions/actions',
id: MenuId.InlineCompletionsActions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const allApiProposals = Object.freeze({
contribMenuBarHome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMenuBarHome.d.ts',
contribMergeEditorToolbar: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribMergeEditorToolbar.d.ts',
contribRemoteHelp: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribRemoteHelp.d.ts',
contribShareMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribShareMenu.d.ts',
contribViewsRemote: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts',
contribViewsWelcome: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribViewsWelcome.d.ts',
customEditorMove: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts',
Expand Down
6 changes: 6 additions & 0 deletions src/vscode-dts/vscode.proposed.contribShareMenu.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// empty placeholder declaration for the `file/share`-submenu contribution point

0 comments on commit ffe53e8

Please sign in to comment.