diff --git a/extensions/github/package.json b/extensions/github/package.json index 68cbd72cbfa91..8b78f0e27559c 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -38,16 +38,13 @@ }, { "command": "github.copyVscodeDevLink", + "enablement": "github.hasGitHubRepo && resourceScheme != untitled && remoteName != 'codespaces'", "title": "Copy vscode.dev Link" }, { "command": "github.copyVscodeDevLinkFile", "title": "Copy vscode.dev Link" }, - { - "command": "github.copyVscodeDevLinkWithoutRange", - "title": "Copy vscode.dev Link" - }, { "command": "github.openOnVscodeDev", "title": "Open in vscode.dev", @@ -77,56 +74,24 @@ "command": "github.copyVscodeDevLinkFile", "when": "false" }, - { - "command": "github.copyVscodeDevLinkWithoutRange", - "when": "false" - }, { "command": "github.openOnVscodeDev", "when": "false" } ], - "file/share": [ - { - "command": "github.copyVscodeDevLinkFile", - "when": "github.hasGitHubRepo && remoteName != 'codespaces'", - "group": "0_vscode@0" - } - ], - "editor/context/share": [ - { - "command": "github.copyVscodeDevLink", - "when": "github.hasGitHubRepo && resourceScheme != untitled && remoteName != 'codespaces'", - "group": "0_vscode@0" - } - ], - "explorer/context/share": [ - { - "command": "github.copyVscodeDevLinkWithoutRange", - "when": "github.hasGitHubRepo && resourceScheme != untitled && remoteName != 'codespaces'", - "group": "0_vscode@0" - } - ], - "editor/lineNumber/context": [ - { - "command": "github.copyVscodeDevLink", - "when": "github.hasGitHubRepo && resourceScheme != untitled && activeEditor == workbench.editors.files.textFileEditor && config.editor.lineNumbers == on && remoteName != 'codespaces'", - "group": "1_cutcopypaste@2" - }, + "share": [ { "command": "github.copyVscodeDevLink", - "when": "github.hasGitHubRepo && resourceScheme != untitled && activeEditor == workbench.editor.notebook && remoteName != 'codespaces'", - "group": "1_cutcopypaste@2" + "title": "Copy vscode.dev Link" } ], - "editor/title/context/share": [ + "file/share": [ { - "command": "github.copyVscodeDevLinkWithoutRange", - "when": "github.hasGitHubRepo && resourceScheme != untitled && remoteName != 'codespaces'", + "command": "github.copyVscodeDevLinkFile", + "when": "github.hasGitHubRepo && remoteName != 'codespaces'", "group": "0_vscode@0" } ] - }, "configuration": [ { diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts index 7158759c672f5..0f4348e376c32 100644 --- a/extensions/github/src/commands.ts +++ b/extensions/github/src/commands.ts @@ -45,7 +45,10 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { } })); - disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLink', async (context: LinkContext) => { + disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLink', async (context: LinkContext, ranges?: vscode.Range[]) => { + if (Array.isArray(ranges) && ranges.every((range) => 'start' in range && 'end' in range) && context instanceof vscode.Uri) { + context = { uri: context, ranges }; + } return copyVscodeDevLink(gitAPI, true, context); })); @@ -53,10 +56,6 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable { return copyVscodeDevLink(gitAPI, false, context); })); - disposables.add(vscode.commands.registerCommand('github.copyVscodeDevLinkWithoutRange', async (context: LinkContext) => { - return copyVscodeDevLink(gitAPI, true, context, false); - })); - disposables.add(vscode.commands.registerCommand('github.openOnVscodeDev', async () => { return openVscodeDevLink(gitAPI); })); diff --git a/extensions/github/src/links.ts b/extensions/github/src/links.ts index 5cbdf2e78bccf..ce6bfdb995095 100644 --- a/extensions/github/src/links.ts +++ b/extensions/github/src/links.ts @@ -40,27 +40,41 @@ interface INotebookPosition { range: vscode.Range | undefined; } +interface EditorContext { + uri: vscode.Uri; + ranges: vscode.Range[]; +} + interface EditorLineNumberContext { uri: vscode.Uri; lineNumber: number; } -export type LinkContext = vscode.Uri | EditorLineNumberContext | undefined; -function extractContext(context: LinkContext): { fileUri: vscode.Uri | undefined; lineNumber: number | undefined } { +interface ScmResourceContext { + resourceUri: vscode.Uri; +} + +export type LinkContext = vscode.Uri | EditorContext | EditorLineNumberContext | undefined | ScmResourceContext; + +function extractContext(context: LinkContext): { fileUri: vscode.Uri | undefined; ranges: vscode.Range[] | undefined } { if (context instanceof vscode.Uri) { - return { fileUri: context, lineNumber: undefined }; + return { fileUri: context, ranges: undefined }; + } else if (context !== undefined && 'ranges' in context && 'uri' in context) { + return { fileUri: context.uri, ranges: context.ranges }; } else if (context !== undefined && 'lineNumber' in context && 'uri' in context) { - return { fileUri: context.uri, lineNumber: context.lineNumber }; + return { fileUri: context.uri, ranges: [new vscode.Range(context.lineNumber - 1, 0, context.lineNumber - 1, 1)] }; + } else if (context !== undefined && 'resourceUri' in context) { + return { fileUri: context.resourceUri, ranges: undefined }; } else { - return { fileUri: undefined, lineNumber: undefined }; + return { fileUri: undefined, ranges: undefined }; } } function getFileAndPosition(context: LinkContext): IFilePosition | INotebookPosition | undefined { let range: vscode.Range | undefined; - const { fileUri, lineNumber } = extractContext(context); - const uri = fileUri ?? vscode.window.activeTextEditor?.document.uri; + const { fileUri, ranges: selections } = extractContext(context); + const uri = fileUri; if (uri) { if (uri.scheme === 'vscode-notebook-cell' && vscode.window.activeNotebookEditor?.notebook.uri.fsPath === uri.fsPath) { @@ -69,11 +83,11 @@ function getFileAndPosition(context: LinkContext): IFilePosition | INotebookPosi const cell = vscode.window.activeNotebookEditor.notebook.getCells().find(cell => cell.document.uri.fragment === uri?.fragment); const cellIndex = cell?.index ?? vscode.window.activeNotebookEditor.selection.start; - const range = getRangeOrSelection(lineNumber); + const range = selections?.[0]; return { type: LinkType.Notebook, uri, cellIndex, range }; } else { // the active editor is a text editor - range = getRangeOrSelection(lineNumber); + range = selections?.[0]; return { type: LinkType.File, uri, range }; } } @@ -86,12 +100,6 @@ function getFileAndPosition(context: LinkContext): IFilePosition | INotebookPosi return undefined; } -function getRangeOrSelection(lineNumber: number | undefined) { - return lineNumber !== undefined && (!vscode.window.activeTextEditor || vscode.window.activeTextEditor.selection.isEmpty || !vscode.window.activeTextEditor.selection.contains(new vscode.Position(lineNumber - 1, 0))) - ? new vscode.Range(lineNumber - 1, 0, lineNumber - 1, 1) - : vscode.window.activeTextEditor?.selection; -} - function rangeString(range: vscode.Range | undefined) { if (!range) { return ''; @@ -122,10 +130,7 @@ export function notebookCellRangeString(index: number | undefined, range: vscode export function getLink(gitAPI: GitAPI, useSelection: boolean, hostPrefix?: string, linkType: 'permalink' | 'headlink' = 'permalink', context?: LinkContext, useRange?: boolean): string | undefined { hostPrefix = hostPrefix ?? 'https://github.com'; const fileAndPosition = getFileAndPosition(context); - if (!fileAndPosition) { - return; - } - const uri = fileAndPosition.uri; + const uri = fileAndPosition?.uri; // Use the first repo if we cannot determine a repo from the uri. const gitRepo = (uri ? getRepositoryForFile(gitAPI, uri) : gitAPI.repositories[0]) ?? gitAPI.repositories[0]; @@ -150,9 +155,10 @@ export function getLink(gitAPI: GitAPI, useSelection: boolean, hostPrefix?: stri } const blobSegment = (gitRepo.state.HEAD?.ahead === 0) ? `/blob/${linkType === 'headlink' ? gitRepo.state.HEAD.name : gitRepo.state.HEAD?.commit}` : ''; - const fileSegments = fileAndPosition.type === LinkType.File + const fileSegments = fileAndPosition && uri ? (fileAndPosition.type === LinkType.File ? (useSelection ? `${uri.path.substring(gitRepo.rootUri.path.length)}${useRange ? rangeString(fileAndPosition.range) : ''}` : '') - : (useSelection ? `${uri.path.substring(gitRepo.rootUri.path.length)}${useRange ? notebookCellRangeString(fileAndPosition.cellIndex, fileAndPosition.range) : ''}` : ''); + : (useSelection ? `${uri.path.substring(gitRepo.rootUri.path.length)}${useRange ? notebookCellRangeString(fileAndPosition.cellIndex, fileAndPosition.range) : ''}` : '')) + : ''; return `${hostPrefix}/${repo.owner}/${repo.repo}${blobSegment }${fileSegments}`; diff --git a/src/vs/editor/contrib/clipboard/browser/clipboard.ts b/src/vs/editor/contrib/clipboard/browser/clipboard.ts index 420abfa16a49d..e9a71900cf12c 100644 --- a/src/vs/editor/contrib/clipboard/browser/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/browser/clipboard.ts @@ -16,7 +16,6 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import * as nls from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -108,9 +107,6 @@ 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, when: ContextKeyExpr.and(ContextKeyExpr.notEquals('resourceScheme', 'output'), EditorContextKeys.editorTextFocus) }); -MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { submenu: MenuId.EditorTitleContextShare, title: { value: nls.localize('share', "Share"), original: 'Share', }, group: '11_share', order: -1 }); -MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { submenu: MenuId.ExplorerContextShare, title: { value: nls.localize('share', "Share"), original: 'Share', }, group: '11_share', order: -1 }); export const PasteAction = supportsPaste ? registerCommand(new MultiCommand({ id: 'editor.action.clipboardPasteAction', diff --git a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts index 4eef122381b1f..e0a59867d9b18 100644 --- a/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts +++ b/src/vs/editor/contrib/contextmenu/browser/contextmenu.ts @@ -161,7 +161,18 @@ export class ContextMenuController implements IEditorContribution { // get menu groups const menu = this._menuService.createMenu(menuId, this._contextKeyService); - const groups = menu.getActions({ arg: model.uri }); + const groups = menu.getActions({ + arg: [model.uri, this._editor.getSelections()?.map((selection) => ({ + start: { + line: selection.getStartPosition().lineNumber - 1, + character: selection.getStartPosition().column + }, + end: { + line: selection.getEndPosition().lineNumber - 1, + character: selection.getEndPosition().column + } + }))] + }); menu.dispose(); // translate them into other actions diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 34551c1a5d5ee..6faa88dee3dda 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -98,15 +98,18 @@ 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'); static readonly SCMTitle = new MenuId('SCMTitle'); static readonly SearchContext = new MenuId('SearchContext'); static readonly SearchActionMenu = new MenuId('SearchActionContext'); + static readonly Share = new MenuId('Share'); static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu'); static readonly StatusBarRemoteIndicatorMenu = new MenuId('StatusBarRemoteIndicatorMenu'); static readonly StickyScrollContext = new MenuId('StickyScrollContext'); @@ -477,7 +480,9 @@ export class MenuItemAction implements IAction { run(...args: any[]): Promise { let runArgs: any[] = []; - if (this._options?.arg) { + if (this._options?.arg && Array.isArray(this._options.arg)) { + runArgs = [...runArgs, ...this._options.arg]; + } else if (this._options?.arg) { runArgs = [...runArgs, this._options.arg]; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts b/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts index 4baeaca8412e0..e287bba453a13 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/editorLineNumberMenu.ts @@ -88,9 +88,6 @@ export class EditorLineNumberContextMenu extends Disposable implements IEditorCo actions.push(collectedActions); } - const menuActions = menu.getActions({ arg: { lineNumber, uri: model.uri }, shouldForwardArgs: true }); - actions.push(...menuActions.map(a => a[1])); - // if the current editor selections do not contain the target line number, // set the selection to the clicked line number if (e.target.type === MouseTargetType.GUTTER_LINE_NUMBERS) { @@ -107,11 +104,24 @@ export class EditorLineNumberContextMenu extends Disposable implements IEditorCo } } + const ranges = this.editor.getSelections()?.map((selection) => ({ + start: { + line: selection.getStartPosition().lineNumber - 1, + character: selection.getStartPosition().column + }, + end: { + line: selection.getEndPosition().lineNumber - 1, + character: selection.getEndPosition().column + } + })); + const menuActions = menu.getActions({ arg: { lineNumber, uri: model.uri, ranges } }); + actions.push(...menuActions.map(a => a[1])); + this.contextMenuService.showContextMenu({ getAnchor: () => anchor, getActions: () => Separator.join(...actions), menuActionOptions: { shouldForwardArgs: true }, - getActionsContext: () => ({ lineNumber, uri: model.uri }), + getActionsContext: () => ({ lineNumber, uri: model.uri, ranges }), onHide: () => menu.dispose(), }); }); diff --git a/src/vs/workbench/contrib/share/browser/share.contribution.ts b/src/vs/workbench/contrib/share/browser/share.contribution.ts new file mode 100644 index 0000000000000..38a06e736e4d4 --- /dev/null +++ b/src/vs/workbench/contrib/share/browser/share.contribution.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { IMenuService, MenuId, MenuRegistry, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import * as nls from 'vs/nls'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; + +const menuIds: [MenuId, MenuId | undefined, ContextKeyExpression | undefined][] = [ + [MenuId.EditorContext, MenuId.EditorContextShare, ContextKeyExpr.and(ContextKeyExpr.notEquals('resourceScheme', 'output'), EditorContextKeys.editorTextFocus)], + [MenuId.EditorTitleContext, MenuId.EditorTitleContextShare, undefined], + [MenuId.ExplorerContext, MenuId.ExplorerContextShare, undefined], + [MenuId.OpenEditorsContext, MenuId.OpenEditorsContextShare, undefined], + [MenuId.SCMResourceContext, MenuId.SCMResourceContextShare, undefined], + [MenuId.EditorLineNumberContext, undefined, undefined], +]; + +for (const [menuId, submenuId, when] of menuIds) { + if (submenuId !== undefined) { + MenuRegistry.appendMenuItem(menuId, { submenu: submenuId, title: { value: nls.localize('share', "Share"), original: 'Share', }, group: '11_share', order: -1, when }); + } +} + +class ShareContribution { + + private readonly disposableStore = new DisposableStore(); + private readonly menu = this.menuService.createMenu(MenuId.Share, this.contextKeyService); + + constructor( + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + this.ensureActions(); + this.menu.onDidChange(() => this.ensureActions()); + } + + private ensureActions(): void { + this.disposableStore.clear(); + + const allActions = this.menu.getActions(); + + for (const [_, actions] of allActions) { + for (const action of actions) { + if (action instanceof SubmenuItemAction || !action.enabled) { + continue; + } + for (const [menuId, submenuId] of menuIds) { + this.disposableStore.add(MenuRegistry.appendMenuItem( + submenuId ?? menuId, + { command: action.item } + )); + } + } + } + } +} + +Registry.as(Extensions.Workbench).registerWorkbenchContribution(ShareContribution, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index f19d36af35763..e95eba66981ec 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -129,6 +129,11 @@ const apiMenus: IAPIMenu[] = [ id: MenuId.SCMResourceContext, description: localize('menus.resourceStateContext', "The Source Control resource state context menu") }, + { + key: 'scm/resourceState/context/share', + id: MenuId.SCMResourceContextShare, + description: localize('menus.resourceStateContextShare', "The Share submenu in the Source Control resource state context menu") + }, { key: 'scm/resourceFolder/context', id: MenuId.SCMResourceFolderContext, @@ -295,6 +300,13 @@ const apiMenus: IAPIMenu[] = [ description: localize('menus.share', "Share submenu shown in the top level File menu."), proposed: 'contribShareMenu' }, + { + key: 'share', + id: MenuId.Share, + description: localize('share', "Share submenus in all relevant context menus."), + supportsSubmenus: false, + proposed: 'contribShareMenu' + }, { key: 'editor/inlineCompletions/actions', id: MenuId.InlineCompletionsActions, diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index d1945cb9b2334..52d79cfa34b5f 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -358,4 +358,6 @@ import 'vs/workbench/contrib/deprecatedExtensionMigrator/browser/deprecatedExten // Bracket Pair Colorizer 2 Telemetry import 'vs/workbench/contrib/bracketPairColorizer2Telemetry/browser/bracketPairColorizer2Telemetry.contribution'; +import 'vs/workbench/contrib/share/browser/share.contribution'; + //#endregion