From 037f9f38e57d4342e5340cc5738d8500f17f8164 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 9 Jan 2025 02:13:06 -0800 Subject: [PATCH] one click switch --- .../parts/editor/editorGroupWatermark.ts | 82 +++++----- .../contrib/void/browser/media/void.css | 4 + .../void/browser/react/src/util/inputs.tsx | 7 + .../void/browser/react/src/util/services.tsx | 8 +- .../react/src/void-settings-tsx/Settings.tsx | 153 +++++++++++++----- 5 files changed, 174 insertions(+), 80 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 158e6072..787632ae 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -167,17 +167,17 @@ export class EditorGroupWatermark extends Disposable { // .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id)); this.clear(); - const box = append(this.shortcuts, $('.watermark-box')); - const boxBelow = append(this.shortcuts, $('')) - boxBelow.style.display = 'flex' - boxBelow.style.flex = 'row' - boxBelow.style.justifyContent = 'center' + const voidIconBox = append(this.shortcuts, $('.watermark-box')); + const recentsBox = append(this.shortcuts, $('div')); + recentsBox.style.display = 'flex' + recentsBox.style.flex = 'row' + recentsBox.style.justifyContent = 'center' const update = async () => { - clearNode(box); - clearNode(boxBelow); + clearNode(voidIconBox); + clearNode(recentsBox); this.currentDisposables.forEach(label => label.dispose()); this.currentDisposables.clear(); @@ -187,13 +187,14 @@ export class EditorGroupWatermark extends Disposable { if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { // Open a folder - const button = h('button') - button.root.classList.add('void-watermark-button') - button.root.style.display = 'block' - button.root.style.marginLeft = 'auto' - button.root.style.marginRight = 'auto' - button.root.textContent = 'Open a folder' - button.root.onclick = () => { + const openFolderButton = h('button') + openFolderButton.root.classList.add('void-watermark-button') + openFolderButton.root.style.display = 'block' + openFolderButton.root.style.marginLeft = 'auto' + openFolderButton.root.style.marginRight = 'auto' + openFolderButton.root.style.marginBottom = '16px' + openFolderButton.root.textContent = 'Open a folder' + openFolderButton.root.onclick = () => { this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID) // if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) { // this.commandService.executeCommand(OpenFolderViaWorkspaceAction.ID); @@ -201,7 +202,7 @@ export class EditorGroupWatermark extends Disposable { // this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder'); // } } - box.appendChild(button.root); + voidIconBox.appendChild(openFolderButton.root); // Recents @@ -211,13 +212,8 @@ export class EditorGroupWatermark extends Disposable { if (recentlyOpened.length !== 0) { - const span = $('div') - span.textContent = 'Recent' - span.style.fontWeight = '500' - box.append(span) - - box.append( - ...recentlyOpened.map(w => { + voidIconBox.append( + ...recentlyOpened.map((w, i) => { let fullPath: string; let windowOpenable: IWindowOpenable; @@ -234,14 +230,13 @@ export class EditorGroupWatermark extends Disposable { const { name, parentPath } = splitRecentLabel(fullPath); - const li = $('li'); - const link = $('span'); - link.classList.add('void-link') + const linkSpan = $('span'); + linkSpan.classList.add('void-link') + linkSpan.style.display = 'flex' + linkSpan.style.gap = '4px' + linkSpan.style.padding = '8px' - link.innerText = name; - link.title = fullPath; - link.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath)); - link.addEventListener('click', e => { + linkSpan.addEventListener('click', e => { this.hostService.openWindow([windowOpenable], { forceNewWindow: e.ctrlKey || e.metaKey, remoteAuthority: w.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable @@ -249,29 +244,30 @@ export class EditorGroupWatermark extends Disposable { e.preventDefault(); e.stopPropagation(); }); - li.appendChild(link); - const span = $('span'); - span.style.paddingLeft = '4px'; - span.classList.add('path'); - span.classList.add('detail'); - span.innerText = parentPath; - span.title = fullPath; - li.appendChild(span); + const nameSpan = $('span'); + nameSpan.innerText = name; + nameSpan.title = fullPath; + linkSpan.appendChild(nameSpan); - return li + const dirSpan = $('span'); + dirSpan.style.paddingLeft = '4px'; + dirSpan.innerText = parentPath; + dirSpan.title = fullPath; + + linkSpan.appendChild(dirSpan); + + return linkSpan }).filter(v => !!v) ) } - - } else { // show them Void keybindings const keys = this.keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID); - const dl = append(box, $('dl')); + const dl = append(voidIconBox, $('dl')); const dt = append(dl, $('dt')); dt.textContent = 'Chat' const dd = append(dl, $('dd')); @@ -282,7 +278,7 @@ export class EditorGroupWatermark extends Disposable { const keys2 = this.keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID); - const dl2 = append(box, $('dl')); + const dl2 = append(voidIconBox, $('dl')); const dt2 = append(dl2, $('dt')); dt2.textContent = 'Quick Edit' const dd2 = append(dl2, $('dd')); @@ -292,7 +288,7 @@ export class EditorGroupWatermark extends Disposable { this.currentDisposables.add(label2); const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings'); - const button3 = append(boxBelow, $('button')); + const button3 = append(recentsBox, $('button')); button3.textContent = 'Void Settings' button3.style.display = 'block' button3.style.marginLeft = 'auto' diff --git a/src/vs/workbench/contrib/void/browser/media/void.css b/src/vs/workbench/contrib/void/browser/media/void.css index 41177002..f3a1d4b8 100644 --- a/src/vs/workbench/contrib/void/browser/media/void.css +++ b/src/vs/workbench/contrib/void/browser/media/void.css @@ -70,6 +70,10 @@ .void-link { color: #3b82f6; cursor: pointer; + transition: all 0.2s ease; +} +.void-link:hover { + opacity: 80%; } diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 15d3c48c..5fecb79b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -567,6 +567,13 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars } +export const VoidButton = ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => { + return +} + // export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => { // const instanceRef = useRef(null); // const [childrenPortal, setChildrenPortal] = useState(null) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index c083216a..edf0d820 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -41,7 +41,9 @@ import { ILanguageConfigurationService } from '../../../../../../../editor/commo import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js' import { ILanguageDetectionService } from '../../../../../../services/languageDetection/common/languageDetectionWorkerService.js' import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js' - +import { IEnvironmentService } from '../../../../../../../platform/environment/common/environment.js' +import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js' +import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js' // normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes @@ -176,6 +178,10 @@ const getReactAccessor = (accessor: ServicesAccessor) => { ILanguageFeaturesService: accessor.get(ILanguageFeaturesService), IKeybindingService: accessor.get(IKeybindingService), + IEnvironmentService: accessor.get(IEnvironmentService), + IConfigurationService: accessor.get(IConfigurationService), + IPathService: accessor.get(IPathService), + } as const return reactAccessor } diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 54c99f2c..21986a0f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -7,11 +7,14 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js' import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js' import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' -import { VoidCheckBox, VoidInputBox, _VoidSelectBox, VoidSwitch, VoidCustomSelectBox } from '../util/inputs.js' +import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidSwitch } from '../util/inputs.js' import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js' import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react' import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js' import { useScrollbarStyles } from '../util/useScrollbarStyles.js' +import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js' +import { URI } from '../../../../../../../base/common/uri.js' +import { env } from '../../../../../../../base/common/process.js' const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => { @@ -115,7 +118,7 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => { /> */} {/* model */} -
+
{ modelNameRef.current = modelName }, [])} @@ -125,36 +128,33 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => { {/* button */}
- + }} + >Add model
{!errorString ? null :
{errorString}
} - -
@@ -167,10 +167,7 @@ const AddModelMenuFull = () => { return
{open ? { setOpen(false) }} /> - : + : setOpen(true)}>Add Model }
} @@ -432,11 +429,94 @@ export const FeaturesTab = () => { +// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium +// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed +type TransferFilesInfo = { from: URI, to: URI }[] +const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): TransferFilesInfo => { + if (os === null) + throw new Error(`One-click switch is not possible in this environment.`) + if (os === 'mac') { + const homeDir = env['HOME'] + if (!homeDir) throw new Error(`$HOME not found`) + return [{ + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + }] + } + if (os === 'linux') { + const homeDir = env['HOME'] + if (!homeDir) throw new Error(`variable for $HOME location not found`) + return [{ + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'), + }] + } + if (os === 'windows') { + const appdata = env['APPDATA'] + if (!appdata) throw new Error(`variable for %APPDATA% location not found`) + const userprofile = env['USERPROFILE'] + if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`) + + return [{ + from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'), + to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'), + }, { + from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'), + to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'), + }] + } + + throw new Error(`os '${os}' not recognized`) +} + +const os = null//isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null +let transferTheseFiles: TransferFilesInfo = [] +let transferError: string | null = null + +try { transferTheseFiles = transferTheseFilesOfOS(os) } +catch (e) { transferError = e + '' } const OneClickSwitch = () => { + const accessor = useAccessor() + const fileService = accessor.get('IFileService') + if (transferTheseFiles.length === 0) + return <> +
One-click transfer not available.
+
{transferError}
+ + + const onClick = async () => { + for (let { from, to } of transferTheseFiles) { + console.log('transferring', from, to) + // not sure if this can fail, just wrapping it with try/catch for now + try { await fileService.copy(from, to, true) } + catch (e) { console.error('Void Transfer Error:', e) } + } + } + + return <> + + Transfer Settings + + } @@ -446,21 +526,22 @@ const GeneralTab = () => { {/* keyboard shortcuts */} -

General Settings

+

General Settings

{`VS Code's built-in settings.`}

-

Keyboard Settings

+

Keyboard Settings

{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}

-

One-click Switch

- - Transfer your VS Code settings to Void. - -

Theme

+

One-click Switch

+

{`Transfer your VS Code settings into Void.`}

+ -

Rules for AI

+

Theme

+ + +

Rules for AI

{/* placeholder: "Do not add ;'s. Do not change or delete spacing, formatting, or comments. Respond to queries in French when applicable. " */}