mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 04:49:35 +00:00
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:
parent
e71b6105eb
commit
ffe53e8d71
|
@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
78
extensions/github/src/links.ts
Normal file
78
extensions/github/src/links.ts
Normal 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)}`;
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
6
src/vscode-dts/vscode.proposed.contribShareMenu.d.ts
vendored
Normal file
6
src/vscode-dts/vscode.proposed.contribShareMenu.d.ts
vendored
Normal 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
|
Loading…
Reference in a new issue