From 56f4fffa8fc3ce31663abfd747ef97ac56abafce Mon Sep 17 00:00:00 2001 From: Himanshu Date: Sat, 2 Nov 2024 02:01:34 +0530 Subject: [PATCH] feat: new window watermark (#87) --- .../parts/editor/editorGroupWatermark.ts | 146 ++++++++++++++-- .../parts/editor/media/editorgroupview.css | 163 ++++++++++++++++-- .../browser/gettingStarted.contribution.ts | 2 +- 3 files changed, 275 insertions(+), 36 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 0617008ccc5ce..bc5b5bc21ce59 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -9,13 +9,18 @@ import { isMacintosh, isWeb, OS } from 'vs/base/common/platform'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { append, clearNode, $, h } from 'vs/base/browser/dom'; +import { append, clearNode, $, h, addDisposableListener, EventType } from 'vs/base/browser/dom'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { defaultKeybindingLabelStyles } from 'vs/platform/theme/browser/defaultStyles'; import { editorForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { isRecentFolder, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { IWindowOpenable } from 'vs/platform/window/common/window'; +import { ILabelService, Verbosity } from 'vs/platform/label/common/label'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { splitRecentLabel } from 'vs/base/common/labels'; registerColor('editorWatermark.foreground', { dark: transparent(editorForeground, 0.6), light: transparent(editorForeground, 0.68), hcDark: editorForeground, hcLight: editorForeground }, localize('editorLineHighlight', 'Foreground color for the labels in the editor watermark.')); @@ -26,7 +31,7 @@ interface WatermarkEntry { readonly when?: ContextKeyExpression; } -const openPearAIChat: WatermarkEntry = { text: localize('watermark.openPearAIChat', "Open Chat"), id: 'pearai.focusContinueInput', when: ContextKeyExpr.has('pearAIExtensionLoaded') }; +const openPearAIChat: WatermarkEntry = { text: localize('watermark.openPearAIChat', "Open Chat"), id: 'pearai.focusContinueInput', when: ContextKeyExpr.has('pearAIExtensionLoaded') }; const bigChat: WatermarkEntry = { text: localize('watermark.pearAIBigChat', "Big Chat"), id: 'pearai.resizeAuxiliaryBarWidth', when: ContextKeyExpr.has('pearAIExtensionLoaded') }; const prevChat: WatermarkEntry = { text: localize('watermark.pearAIPrevChat', "Previous Chat"), id: 'pearai.loadRecentChat', when: ContextKeyExpr.has('pearAIExtensionLoaded') }; const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' }; @@ -68,7 +73,7 @@ const folderEntries = [ ]; export class EditorGroupWatermark extends Disposable { - private readonly shortcuts: HTMLElement; + private readonly watermark: HTMLElement; private readonly transientDisposables = this._register(new DisposableStore()); private enabled: boolean = false; private workbenchState: WorkbenchState; @@ -80,21 +85,21 @@ export class EditorGroupWatermark extends Disposable { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IExtensionService private readonly extensionService: IExtensionService + @IExtensionService private readonly extensionService: IExtensionService, + @IWorkspacesService private readonly workspacesService: IWorkspacesService, + @ILabelService private readonly labelService: ILabelService, + @IHostService private readonly hostService: IHostService, + @ICommandService private readonly commandService: ICommandService ) { super(); + this.workbenchState = contextService.getWorkbenchState(); + const hasWorkspace = this.workbenchState !== WorkbenchState.EMPTY; - const elements = h('.editor-group-watermark', [ - h('.letterpress'), - h('.shortcuts@shortcuts'), - ]); + const elements = h('.editor-group-watermark-' + (hasWorkspace ? 'workspace' : 'no-workspace')); append(container, elements.root); - this.shortcuts = elements.shortcuts; - + this.watermark = elements.root; this.registerListeners(); - - this.workbenchState = contextService.getWorkbenchState(); this.render(); } @@ -127,7 +132,7 @@ export class EditorGroupWatermark extends Disposable { private async render(): Promise { // Wait for the all extensions to be activated await this.extensionService.activateByEvent('onStartupFinished'); - // TODO: @Himanshu-Singh-Chauhan - this should be set from inside the extension, test it later, if it works, remove this + // TODO: @Himanshu-Singh-Chauhan - this should be set from inside the extension, test it later, if it works, remove this this.contextKeyService.createKey('pearAIExtensionLoaded', true); // Set a context key when the PearAI extension is loaded const enabled = this.configurationService.getValue('workbench.tips.enabled'); @@ -143,7 +148,16 @@ export class EditorGroupWatermark extends Disposable { return; } - const box = append(this.shortcuts, $('.watermark-box')); + const hasWorkspace = this.workbenchState !== WorkbenchState.EMPTY; + + if (!hasWorkspace) { + this.renderNoWorkspaceWatermark(); + return; + } + + append(this.watermark, $('.letterpress')); + const shortcuts = append(this.watermark, $('.shortcuts')); + const box = append(shortcuts, $('.watermark-box')); const folder = this.workbenchState !== WorkbenchState.EMPTY; const selected = (folder ? folderEntries : noFolderEntries) .filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when)) @@ -175,8 +189,108 @@ export class EditorGroupWatermark extends Disposable { this.transientDisposables.add(this.keybindingService.onDidUpdateKeybindings(update)); } + private async renderNoWorkspaceWatermark(): Promise { + const container = append(this.watermark, $('.editor-group-watermark-no-workspace')); + + // button container + const buttonContainer = append(container, $('.button-container')); + const openFolderButton = append(buttonContainer, $('button.open-folder-button')); + // folder icon and text in separate spans + append(openFolderButton, $('span.codicon.codicon-folder-opened')); + append(openFolderButton, $('span.text', {}, localize('watermark.openFolder', "Open Folder"))); + // click handler for Open Folder button + this._register(addDisposableListener(openFolderButton, EventType.CLICK, (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + this.commandService.executeCommand('workbench.action.files.openFolder'); + })); + + // New File text button + const newFileItem = append(container, $('.new-file-item')); + newFileItem.textContent = localize('watermark.newFile', "New File"); + newFileItem.style.cursor = 'pointer'; + + const newFileKeybinding = this.keybindingService.lookupKeybinding('workbench.action.files.newUntitledFile')?.getLabel(); + newFileItem.title = newFileKeybinding ? + localize('watermark.newFileWithKeybinding', "New File ({0})", newFileKeybinding) : + localize('watermark.newFile', "New File"); + + this._register(addDisposableListener(newFileItem, EventType.CLICK, (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + this.commandService.executeCommand('workbench.action.files.newUntitledFile'); + })); + + // recent folders and workspaces list + const recentList = append(container, $('.recent-list')); + + const recentlyOpened = await this.workspacesService.getRecentlyOpened(); + const recents = recentlyOpened.workspaces + .filter(recent => !this.contextService.isCurrentWorkspace( + isRecentFolder(recent) ? recent.folderUri : recent.workspace.configPath + )) + .slice(0, 6); + + if (recents.length === 0) { + const noRecentsElement = append(recentList, $('.recent-item')); + noRecentsElement.textContent = localize('watermark.noRecents', "No Recent Folders"); + return; + } + + recents.forEach(recent => { + const itemElement = append(recentList, $('.recent-item')); + + let fullPath: string; + let windowOpenable: IWindowOpenable; + + if (isRecentFolder(recent)) { + windowOpenable = { folderUri: recent.folderUri }; + fullPath = recent.label || this.labelService.getWorkspaceLabel(recent.folderUri, { verbose: Verbosity.LONG }); + } else { + fullPath = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: Verbosity.LONG }); + windowOpenable = { workspaceUri: recent.workspace.configPath }; + } + + const { name, parentPath } = splitRecentLabel(fullPath); + + itemElement.textContent = name; + if (parentPath) { + append(itemElement, $('span.spacer')); + const pathSpan = append(itemElement, $('span.path')); + pathSpan.textContent = parentPath; + } + + itemElement.title = fullPath; + itemElement.style.cursor = 'pointer'; + + this._register(addDisposableListener(itemElement, EventType.CLICK, async (e: MouseEvent) => { + try { + e.preventDefault(); + e.stopPropagation(); + await this.hostService.openWindow([windowOpenable], { + forceNewWindow: e.ctrlKey || e.metaKey, + remoteAuthority: recent.remoteAuthority ?? null + }); + } catch (error) { + console.error('Failed to open recent item:', error); + } + })); + }); + + // "More..." item + const moreItem = append(recentList, $('.more-item')); + moreItem.textContent = localize('watermark.more', "More..."); + moreItem.title = localize('watermark.showMoreRecents', "Show All Recent Folders"); + moreItem.style.cursor = 'pointer'; + this._register(addDisposableListener(moreItem, EventType.CLICK, (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + this.commandService.executeCommand('workbench.action.openRecent'); + })); + } + private clear(): void { - clearNode(this.shortcuts); + clearNode(this.watermark); this.transientDisposables.clear(); } diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index 76c8a561b4e09..42860ba529aba 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -29,7 +29,7 @@ /* Watermark & shortcuts */ -.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-workspace { display: flex; height: 100%; max-width: 290px; @@ -39,17 +39,99 @@ justify-content: center; } -.monaco-workbench .part.editor > .content .editor-group-container:not(.empty) > .editor-group-watermark { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace { + display: flex; + height: 100%; + max-width: 700px; + margin: auto; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.monaco-workbench .part.editor > .content .editor-group-container:not(.empty) > .editor-group-watermark-workspace { display: none; } -.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty > .editor-group-watermark, -.monaco-workbench .part.editor > .content.auxiliary .editor-group-container.empty > .editor-group-watermark { - max-width: 200px; - height: calc(100% - 70px); +.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty > .editor-group-watermark-no-workspace, +.monaco-workbench .part.editor > .content.auxiliary .editor-group-container.empty > .editor-group-watermark-no-workspace { + display: flex; + height: 100%; + min-width: 500px; + max-width: 700px; + margin: auto; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .button-container { + width: 100%; + display: flex; + justify-content: center; + margin-bottom: 30px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .open-folder-button { + padding: 8px 12px; + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + display: flex; + align-items: center; + gap: 5px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .open-folder-button .codicon { + font-size: 16px; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .open-folder-button:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .recent-list { + width: 100%; + min-width: 260px; + max-width: 700px; + color: var(--vscode-editorWatermark-foreground); + opacity: 0.8; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .recent-list .recent-item { + padding: 6px 0; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + opacity: 0.8; + font-size: 13px; + color: var(--vscode-foreground); } -.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .letterpress { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .recent-list .recent-item .path { + opacity: 0.6; + color: var(--vscode-editorWatermark-foreground); +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .recent-list .recent-item:hover { + opacity: 1; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .recent-list .recent-item:hover .path { + color: var(--vscode-foreground); + opacity: 0.8; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-workspace > .letterpress { width: 100%; max-height: 100%; aspect-ratio: 1/1; @@ -59,48 +141,48 @@ background-repeat: no-repeat; } -.monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress { +.monaco-workbench.vs-dark .part.editor > .content .editor-group-container > .editor-group-watermark-workspace > .letterpress { background-image: url('./letterpress-dark.svg'); } -.monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress { +.monaco-workbench.hc-light .part.editor > .content .editor-group-container > .editor-group-watermark-workspace > .letterpress { background-image: url('./letterpress-hcLight.svg'); } -.monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress { +.monaco-workbench.hc-black .part.editor > .content .editor-group-container > .editor-group-watermark-workspace > .letterpress { background-image: url('./letterpress-hcDark.svg'); } -.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container > .editor-group-watermark > .shortcuts, -.monaco-workbench .part.editor > .content.auxiliary .editor-group-container > .editor-group-watermark > .shortcuts, -.monaco-workbench .part.editor > .content .editor-group-container.max-height-478px > .editor-group-watermark > .shortcuts { +.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container > .editor-group-watermark-workspace > .shortcuts, +.monaco-workbench .part.editor > .content.auxiliary .editor-group-container > .editor-group-watermark-workspace > .shortcuts, +.monaco-workbench .part.editor > .content .editor-group-container.max-height-478px > .editor-group-watermark-workspace > .shortcuts { display: none; } -.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .shortcuts > .watermark-box { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-workspace > .shortcuts > .watermark-box { display: inline-table; border-collapse: separate; border-spacing: 11px 17px; } -.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .shortcuts dl { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-workspace > .shortcuts dl { display: table-row; opacity: .8; cursor: default; color: var(--vscode-editorWatermark-foreground); } -.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .shortcuts dt { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-workspace > .shortcuts dt { text-align: right; letter-spacing: 0.04em } -.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .shortcuts dd { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-workspace > .shortcuts dd { text-align: left; } -.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .shortcuts dt, -.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .shortcuts dd { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-workspace > .shortcuts dt, +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-workspace > .shortcuts dd { display: table-cell; vertical-align: middle; } @@ -169,3 +251,46 @@ width: 100%; height: 100%; } + +/* Hide no-workspace watermark when not needed */ +.monaco-workbench .part.editor > .content .editor-group-container:not(.empty) > .editor-group-watermark-no-workspace { + display: none; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .recent-list .more-item { + opacity: 0.7; + margin-top: 8px; + font-style: italic; +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .recent-list .more-item:hover { + color: var(--vscode-foreground); + opacity: 1; +} + +/* When container is small, allow wrapping */ +@media (max-width: 700px) { + .monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .recent-list .recent-item { + white-space: normal; + word-break: break-all; + } +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .recent-list .recent-item .spacer { + display: inline-block; + width: 10px; /* Adjust this value to increase/decrease spacing between name and path(location) of recent items */ +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .new-file-item { + opacity: 0.7; + cursor: pointer; + padding: 1px 0; + font-style: italic; + margin-bottom: 5px; + color: var(--vscode-editorWatermark-foreground); +} + +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark-no-workspace .new-file-item:hover { + color: var(--vscode-foreground); + opacity: 1; +} diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index c581ac0e4d20c..4d8271add16c1 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -329,7 +329,7 @@ configurationRegistry.registerConfiguration({ 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."), 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."), ], - 'default': 'welcomePage', + 'default': 'none', 'description': localize('workbench.startupEditor', "Controls which editor is shown at startup, if none are restored from the previous session.") }, 'workbench.welcomePage.preferReducedMotion': {