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

Add a Share menu and a share vscode.dev command #152765

Merged
merged 5 commits into from Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
23 changes: 23 additions & 0 deletions extensions/github/package.json
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
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
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
@@ -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
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
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
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
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
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
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
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
Expand Up @@ -13,6 +13,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
@@ -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