|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + * Licensed under the MIT License. See License.txt in the project root for license information. |
| 4 | + *--------------------------------------------------------------------------------------------*/ |
| 5 | + |
| 6 | +import { localize, localize2 } from '../../../../nls.js'; |
| 7 | +import { GettingStartedInputSerializer, GettingStartedPage, inWelcomeContext } from './gettingStarted.js'; |
| 8 | +import { Registry } from '../../../../platform/registry/common/platform.js'; |
| 9 | +import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js'; |
| 10 | +import { MenuId, registerAction2, Action2 } from '../../../../platform/actions/common/actions.js'; |
| 11 | +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; |
| 12 | +import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; |
| 13 | +import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; |
| 14 | +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; |
| 15 | +import { KeyCode } from '../../../../base/common/keyCodes.js'; |
| 16 | +import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; |
| 17 | +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; |
| 18 | +import { IWalkthroughsService } from './gettingStartedService.js'; |
| 19 | +import { GettingStartedEditorOptions, GettingStartedInput } from './gettingStartedInput.js'; |
| 20 | +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; |
| 21 | +import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; |
| 22 | +import { workbenchConfigurationNodeBase } from '../../../common/configuration.js'; |
| 23 | +import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; |
| 24 | +import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; |
| 25 | +import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; |
| 26 | +import { isLinux, isMacintosh, isWindows, OperatingSystem as OS } from '../../../../base/common/platform.js'; |
| 27 | +import { IExtensionManagementServerService } from '../../../services/extensionManagement/common/extensionManagement.js'; |
| 28 | +import { IExtensionService } from '../../../services/extensions/common/extensions.js'; |
| 29 | +import { StartupPageEditorResolverContribution, StartupPageRunnerContribution } from './startupPage.js'; |
| 30 | +import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; |
| 31 | +import { DisposableStore } from '../../../../base/common/lifecycle.js'; |
| 32 | +import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; |
| 33 | +import { GettingStartedAccessibleView } from './gettingStartedAccessibleView.js'; |
| 34 | + |
| 35 | +export * as icons from './gettingStartedIcons.js'; |
| 36 | + |
| 37 | +registerAction2(class extends Action2 { |
| 38 | + constructor() { |
| 39 | + super({ |
| 40 | + id: 'workbench.action.openWalkthrough', |
| 41 | + title: localize2('miWelcome', 'Welcome'), |
| 42 | + category: Categories.Help, |
| 43 | + f1: true, |
| 44 | + menu: { |
| 45 | + id: MenuId.MenubarHelpMenu, |
| 46 | + group: '1_welcome', |
| 47 | + order: 1, |
| 48 | + }, |
| 49 | + metadata: { |
| 50 | + description: localize2('minWelcomeDescription', 'Opens a Walkthrough to help you get started in VS Code.') |
| 51 | + } |
| 52 | + }); |
| 53 | + } |
| 54 | + |
| 55 | + public run( |
| 56 | + accessor: ServicesAccessor, |
| 57 | + walkthroughID: string | { category: string; step: string } | undefined, |
| 58 | + optionsOrToSide: { toSide?: boolean; inactive?: boolean } | boolean | undefined |
| 59 | + ) { |
| 60 | + const editorService = accessor.get(IEditorService); |
| 61 | + const commandService = accessor.get(ICommandService); |
| 62 | + |
| 63 | + const toSide = typeof optionsOrToSide === 'object' ? optionsOrToSide.toSide : optionsOrToSide; |
| 64 | + const activeEditor = editorService.activeEditor; |
| 65 | + |
| 66 | + if (walkthroughID) { |
| 67 | + const selectedCategory = typeof walkthroughID === 'string' ? walkthroughID : walkthroughID.category; |
| 68 | + let selectedStep: string | undefined; |
| 69 | + if (typeof walkthroughID === 'object' && 'category' in walkthroughID && 'step' in walkthroughID) { |
| 70 | + selectedStep = `${walkthroughID.category}#${walkthroughID.step}`; |
| 71 | + } else { |
| 72 | + selectedStep = undefined; |
| 73 | + } |
| 74 | + |
| 75 | + // If the walkthrough is already open just reveal the step |
| 76 | + if (selectedStep && activeEditor instanceof GettingStartedInput && activeEditor.selectedCategory === selectedCategory) { |
| 77 | + activeEditor.showWelcome = false; |
| 78 | + commandService.executeCommand('walkthroughs.selectStep', selectedStep); |
| 79 | + return; |
| 80 | + } |
| 81 | + |
| 82 | + let options: GettingStartedEditorOptions; |
| 83 | + if (selectedCategory) { |
| 84 | + // Otherwise open the walkthrough editor with the selected category and step |
| 85 | + options = { selectedCategory, selectedStep, showWelcome: false, preserveFocus: toSide ?? false }; |
| 86 | + } else { |
| 87 | + // Open Welcome page |
| 88 | + options = { selectedCategory, selectedStep, showWelcome: true, preserveFocus: toSide ?? false }; |
| 89 | + } |
| 90 | + editorService.openEditor({ |
| 91 | + resource: GettingStartedInput.RESOURCE, |
| 92 | + options |
| 93 | + }, toSide ? SIDE_GROUP : undefined); |
| 94 | + |
| 95 | + } else { |
| 96 | + editorService.openEditor({ |
| 97 | + resource: GettingStartedInput.RESOURCE, |
| 98 | + options: { preserveFocus: toSide ?? false } |
| 99 | + }, toSide ? SIDE_GROUP : undefined); |
| 100 | + } |
| 101 | + } |
| 102 | +}); |
| 103 | + |
| 104 | +Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).registerEditorSerializer(GettingStartedInput.ID, GettingStartedInputSerializer); |
| 105 | +Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane( |
| 106 | + EditorPaneDescriptor.create( |
| 107 | + GettingStartedPage, |
| 108 | + GettingStartedPage.ID, |
| 109 | + localize('welcome', "Welcome") |
| 110 | + ), |
| 111 | + [ |
| 112 | + new SyncDescriptor(GettingStartedInput) |
| 113 | + ] |
| 114 | +); |
| 115 | + |
| 116 | +const category = localize2('welcome', "Welcome"); |
| 117 | + |
| 118 | +registerAction2(class extends Action2 { |
| 119 | + constructor() { |
| 120 | + super({ |
| 121 | + id: 'welcome.goBack', |
| 122 | + title: localize2('welcome.goBack', 'Go Back'), |
| 123 | + category, |
| 124 | + keybinding: { |
| 125 | + weight: KeybindingWeight.EditorContrib, |
| 126 | + primary: KeyCode.Escape, |
| 127 | + when: inWelcomeContext |
| 128 | + }, |
| 129 | + precondition: ContextKeyExpr.equals('activeEditor', 'gettingStartedPage'), |
| 130 | + f1: true |
| 131 | + }); |
| 132 | + } |
| 133 | + |
| 134 | + run(accessor: ServicesAccessor) { |
| 135 | + const editorService = accessor.get(IEditorService); |
| 136 | + const editorPane = editorService.activeEditorPane; |
| 137 | + if (editorPane instanceof GettingStartedPage) { |
| 138 | + editorPane.escape(); |
| 139 | + } |
| 140 | + } |
| 141 | +}); |
| 142 | + |
| 143 | +CommandsRegistry.registerCommand({ |
| 144 | + id: 'walkthroughs.selectStep', |
| 145 | + handler: (accessor, stepID: string) => { |
| 146 | + const editorService = accessor.get(IEditorService); |
| 147 | + const editorPane = editorService.activeEditorPane; |
| 148 | + if (editorPane instanceof GettingStartedPage) { |
| 149 | + editorPane.selectStepLoose(stepID); |
| 150 | + } else { |
| 151 | + console.error('Cannot run walkthroughs.selectStep outside of walkthrough context'); |
| 152 | + } |
| 153 | + } |
| 154 | +}); |
| 155 | + |
| 156 | +registerAction2(class extends Action2 { |
| 157 | + constructor() { |
| 158 | + super({ |
| 159 | + id: 'welcome.markStepComplete', |
| 160 | + title: localize('welcome.markStepComplete', "Mark Step Complete"), |
| 161 | + category, |
| 162 | + }); |
| 163 | + } |
| 164 | + |
| 165 | + run(accessor: ServicesAccessor, arg: string) { |
| 166 | + if (!arg) { return; } |
| 167 | + const gettingStartedService = accessor.get(IWalkthroughsService); |
| 168 | + gettingStartedService.progressStep(arg); |
| 169 | + } |
| 170 | +}); |
| 171 | + |
| 172 | +registerAction2(class extends Action2 { |
| 173 | + constructor() { |
| 174 | + super({ |
| 175 | + id: 'welcome.markStepIncomplete', |
| 176 | + title: localize('welcome.markStepInomplete', "Mark Step Incomplete"), |
| 177 | + category, |
| 178 | + }); |
| 179 | + } |
| 180 | + |
| 181 | + run(accessor: ServicesAccessor, arg: string) { |
| 182 | + if (!arg) { return; } |
| 183 | + const gettingStartedService = accessor.get(IWalkthroughsService); |
| 184 | + gettingStartedService.deprogressStep(arg); |
| 185 | + } |
| 186 | +}); |
| 187 | + |
| 188 | +registerAction2(class extends Action2 { |
| 189 | + constructor() { |
| 190 | + super({ |
| 191 | + id: 'welcome.showAllWalkthroughs', |
| 192 | + title: localize2('welcome.showAllWalkthroughs', 'Open Walkthrough...'), |
| 193 | + category, |
| 194 | + f1: true, |
| 195 | + menu: { |
| 196 | + id: MenuId.MenubarHelpMenu, |
| 197 | + group: '1_welcome', |
| 198 | + order: 3, |
| 199 | + }, |
| 200 | + }); |
| 201 | + } |
| 202 | + |
| 203 | + private async getQuickPickItems( |
| 204 | + contextService: IContextKeyService, |
| 205 | + gettingStartedService: IWalkthroughsService |
| 206 | + ): Promise<IQuickPickItem[]> { |
| 207 | + const categories = await gettingStartedService.getWalkthroughs(); |
| 208 | + return categories |
| 209 | + .filter(c => contextService.contextMatchesRules(c.when)) |
| 210 | + .map(x => ({ |
| 211 | + id: x.id, |
| 212 | + label: x.title, |
| 213 | + detail: x.description, |
| 214 | + description: x.source, |
| 215 | + })); |
| 216 | + } |
| 217 | + |
| 218 | + async run(accessor: ServicesAccessor) { |
| 219 | + const commandService = accessor.get(ICommandService); |
| 220 | + const contextService = accessor.get(IContextKeyService); |
| 221 | + const quickInputService = accessor.get(IQuickInputService); |
| 222 | + const gettingStartedService = accessor.get(IWalkthroughsService); |
| 223 | + const extensionService = accessor.get(IExtensionService); |
| 224 | + |
| 225 | + const disposables = new DisposableStore(); |
| 226 | + const quickPick = disposables.add(quickInputService.createQuickPick()); |
| 227 | + quickPick.canSelectMany = false; |
| 228 | + quickPick.matchOnDescription = true; |
| 229 | + quickPick.matchOnDetail = true; |
| 230 | + quickPick.placeholder = localize('pickWalkthroughs', 'Select a walkthrough to open'); |
| 231 | + quickPick.items = await this.getQuickPickItems(contextService, gettingStartedService); |
| 232 | + quickPick.busy = true; |
| 233 | + disposables.add(quickPick.onDidAccept(() => { |
| 234 | + const selection = quickPick.selectedItems[0]; |
| 235 | + if (selection) { |
| 236 | + commandService.executeCommand('workbench.action.openWalkthrough', selection.id); |
| 237 | + } |
| 238 | + quickPick.hide(); |
| 239 | + })); |
| 240 | + disposables.add(quickPick.onDidHide(() => disposables.dispose())); |
| 241 | + await extensionService.whenInstalledExtensionsRegistered(); |
| 242 | + disposables.add(gettingStartedService.onDidAddWalkthrough(async () => { |
| 243 | + quickPick.items = await this.getQuickPickItems(contextService, gettingStartedService); |
| 244 | + })); |
| 245 | + quickPick.show(); |
| 246 | + quickPick.busy = false; |
| 247 | + } |
| 248 | +}); |
| 249 | + |
| 250 | +CommandsRegistry.registerCommand({ |
| 251 | + id: 'welcome.newWorkspaceChat', |
| 252 | + handler: (accessor, stepID: string) => { |
| 253 | + const commandService = accessor.get(ICommandService); |
| 254 | + commandService.executeCommand('workbench.action.chat.open', { mode: 'agent', query: '#new ', isPartialQuery: true }); |
| 255 | + } |
| 256 | +}); |
| 257 | + |
| 258 | +export const WorkspacePlatform = new RawContextKey<'mac' | 'linux' | 'windows' | 'webworker' | undefined>('workspacePlatform', undefined, localize('workspacePlatform', "The platform of the current workspace, which in remote or serverless contexts may be different from the platform of the UI")); |
| 259 | +class WorkspacePlatformContribution { |
| 260 | + |
| 261 | + static readonly ID = 'workbench.contrib.workspacePlatform'; |
| 262 | + |
| 263 | + constructor( |
| 264 | + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, |
| 265 | + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, |
| 266 | + @IContextKeyService private readonly contextService: IContextKeyService, |
| 267 | + ) { |
| 268 | + this.remoteAgentService.getEnvironment().then(env => { |
| 269 | + const remoteOS = env?.os; |
| 270 | + |
| 271 | + const remotePlatform = remoteOS === OS.Macintosh ? 'mac' |
| 272 | + : remoteOS === OS.Windows ? 'windows' |
| 273 | + : remoteOS === OS.Linux ? 'linux' |
| 274 | + : undefined; |
| 275 | + |
| 276 | + if (remotePlatform) { |
| 277 | + WorkspacePlatform.bindTo(this.contextService).set(remotePlatform); |
| 278 | + } else if (this.extensionManagementServerService.localExtensionManagementServer) { |
| 279 | + if (isMacintosh) { |
| 280 | + WorkspacePlatform.bindTo(this.contextService).set('mac'); |
| 281 | + } else if (isLinux) { |
| 282 | + WorkspacePlatform.bindTo(this.contextService).set('linux'); |
| 283 | + } else if (isWindows) { |
| 284 | + WorkspacePlatform.bindTo(this.contextService).set('windows'); |
| 285 | + } |
| 286 | + } else if (this.extensionManagementServerService.webExtensionManagementServer) { |
| 287 | + WorkspacePlatform.bindTo(this.contextService).set('webworker'); |
| 288 | + } else { |
| 289 | + console.error('Error: Unable to detect workspace platform'); |
| 290 | + } |
| 291 | + }); |
| 292 | + } |
| 293 | +} |
| 294 | + |
| 295 | +const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration); |
| 296 | +configurationRegistry.registerConfiguration({ |
| 297 | + ...workbenchConfigurationNodeBase, |
| 298 | + properties: { |
| 299 | + 'workbench.welcomePage.walkthroughs.openOnInstall': { |
| 300 | + scope: ConfigurationScope.MACHINE, |
| 301 | + type: 'boolean', |
| 302 | + default: true, |
| 303 | + description: localize('workbench.welcomePage.walkthroughs.openOnInstall', "When enabled, an extension's walkthrough will open upon install of the extension.") |
| 304 | + }, |
| 305 | + 'workbench.welcomePage.extraAnnouncements': { |
| 306 | + scope: ConfigurationScope.MACHINE, |
| 307 | + type: 'boolean', |
| 308 | + default: true, |
| 309 | + description: localize('workbench.welcomePage.extraAnnouncements', "When enabled, the get started page loads additional announcements from !!APP_NAME!!'s repository.") |
| 310 | + }, |
| 311 | + 'workbench.startupEditor': { |
| 312 | + 'scope': ConfigurationScope.RESOURCE, |
| 313 | + 'type': 'string', |
| 314 | + 'enum': ['none', 'welcomePage', 'readme', 'newUntitledFile', 'welcomePageInEmptyWorkbench', 'terminal'], |
| 315 | + 'enumDescriptions': [ |
| 316 | + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.none' }, "Start without an editor."), |
| 317 | + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePage' }, "Open the Welcome page, with content to aid in getting started with VS Code and extensions."), |
| 318 | + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.readme' }, "Open the README when opening a folder that contains one, fallback to 'welcomePage' otherwise. Note: This is only observed as a global configuration, it will be ignored if set in a workspace or folder configuration."), |
| 319 | + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.newUntitledFile' }, "Open a new untitled text file (only applies when opening an empty window)."), |
| 320 | + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.welcomePageInEmptyWorkbench' }, "Open the Welcome page when opening an empty workbench."), |
| 321 | + localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'workbench.startupEditor.terminal' }, "Open a new terminal in the editor area."), |
| 322 | + ], |
| 323 | + 'default': 'welcomePage', |
| 324 | + 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") |
| 325 | + }, |
| 326 | + 'workbench.welcomePage.preferReducedMotion': { |
| 327 | + scope: ConfigurationScope.APPLICATION, |
| 328 | + type: 'boolean', |
| 329 | + default: false, |
| 330 | + deprecationMessage: localize('deprecationMessage', "Deprecated, use the global `workbench.reduceMotion`."), |
| 331 | + description: localize('workbench.welcomePage.preferReducedMotion', "When enabled, reduce motion in welcome page.") |
| 332 | + } |
| 333 | + } |
| 334 | +}); |
| 335 | + |
| 336 | +registerWorkbenchContribution2(WorkspacePlatformContribution.ID, WorkspacePlatformContribution, WorkbenchPhase.AfterRestored); |
| 337 | +registerWorkbenchContribution2(StartupPageEditorResolverContribution.ID, StartupPageEditorResolverContribution, WorkbenchPhase.BlockRestore); |
| 338 | +registerWorkbenchContribution2(StartupPageRunnerContribution.ID, StartupPageRunnerContribution, WorkbenchPhase.AfterRestored); |
| 339 | + |
| 340 | +AccessibleViewRegistry.register(new GettingStartedAccessibleView()); |
| 341 | + |
0 commit comments