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

* 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
This commit is contained in:
Alex Ross 2022-06-27 09:56:36 +02:00 committed by GitHub
parent e71b6105eb
commit ffe53e8d71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 190 additions and 17 deletions

View file

@ -25,11 +25,18 @@
"supported": true
}
},
"enabledApiProposals": [
"contribShareMenu"
],
"contributes": {
"commands": [
{
"command": "github.publish",
"title": "Publish to GitHub"
},
{
"command": "github.copyVscodeDevLink",
"title": "Copy vscode.dev Link"
}
],
"menus": {
@ -37,6 +44,22 @@
{
"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"
}
]
},

View file

@ -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();
@ -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;
}

View file

@ -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';
@ -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();
@ -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 {

View file

@ -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)}`;
}

View file

@ -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');

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Repository } from './typings/git';
export class DisposableStore {
@ -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);
}

View file

@ -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',

View file

@ -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');
@ -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');

View file

@ -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);
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;
}
}
activeActions.push(action);
if (action) {
activeActions.push(action);
}
}
}
if (activeActions.length > 0) {

View file

@ -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',

View file

@ -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,
@ -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,

View file

@ -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',

View file

@ -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