one click switch

This commit is contained in:
Andrew Pareles 2025-01-09 02:13:06 -08:00
parent e132c87792
commit 037f9f38e5
5 changed files with 174 additions and 80 deletions

View file

@ -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'

View file

@ -70,6 +70,10 @@
.void-link {
color: #3b82f6;
cursor: pointer;
transition: all 0.2s ease;
}
.void-link:hover {
opacity: 80%;
}

View file

@ -567,6 +567,13 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
}
export const VoidButton = ({ children, onClick }: { children: React.ReactNode; onClick: () => void }) => {
return <button
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
onClick={onClick}
>{children}</button>
}
// export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => {
// const instanceRef = useRef<DomScrollableElement | null>(null);
// const [childrenPortal, setChildrenPortal] = useState<React.ReactNode | null>(null)

View file

@ -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
}

View file

@ -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 */}
<div className='max-w-40 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'>
<div className='max-w-40 w-fit border border-vscode-editorwidget-border'>
<VoidInputBox
placeholder='Model Name'
onChangeText={useCallback((modelName) => { modelNameRef.current = modelName }, [])}
@ -125,36 +128,33 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
{/* button */}
<div className='max-w-40'>
<button
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
onClick={() => {
const modelName = modelNameRef.current
<VoidButton onClick={() => {
const modelName = modelNameRef.current
if (providerName === null) {
setErrorString('Please select a provider.')
return
}
if (!modelName) {
setErrorString('Please enter a model name.')
return
}
// if model already exists here
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
setErrorString(`This model already exists under ${providerName}.`)
return
}
if (providerName === null) {
setErrorString('Please select a provider.')
return
}
if (!modelName) {
setErrorString('Please enter a model name.')
return
}
// if model already exists here
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
setErrorString(`This model already exists under ${providerName}.`)
return
}
settingsStateService.addModel(providerName, modelName)
onSubmit()
settingsStateService.addModel(providerName, modelName)
onSubmit()
}}>Add model</button>
}}
>Add model</VoidButton>
</div>
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap'>
{errorString}
</div>}
</div>
</>
@ -167,10 +167,7 @@ const AddModelMenuFull = () => {
return <div className='hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 my-4 pb-1 px-3 rounded-sm overflow-hidden '>
{open ?
<AddModelMenu onSubmit={() => { setOpen(false) }} />
: <button
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
onClick={() => setOpen(true)}
>Add Model</button>
: <VoidButton onClick={() => setOpen(true)}>Add Model</VoidButton>
}
</div>
}
@ -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 <>
<div>One-click transfer not available.</div>
<div>{transferError}</div>
</>
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 <>
<VoidButton onClick={onClick}>
Transfer Settings
</VoidButton>
</>
}
@ -446,21 +526,22 @@ const GeneralTab = () => {
{/* keyboard shortcuts */}
<h2 className={`text-3xl mb-2`}>General Settings</h2>
<h2 className={`text-2xl mb-2`}>General Settings</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`VS Code's built-in settings.`}</h3>
<h2 className={`text-3xl mb-2`}>Keyboard Settings</h2>
<h2 className={`text-2xl mb-2`}>Keyboard Settings</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
<h2 className={`text-3xl mb-2`}>One-click Switch</h2>
Transfer your VS Code settings to Void.
<h2 className={`text-3xl mb-2`}>Theme</h2>
<h2 className={`text-2xl mb-2`}>One-click Switch</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Transfer your VS Code settings into Void.`}</h3>
<OneClickSwitch />
<h2 className={`text-3xl mb-2`}>Rules for AI</h2>
<h2 className={`text-2xl mb-2`}>Theme</h2>
<h2 className={`text-2xl mb-2`}>Rules for AI</h2>
{/* placeholder: "Do not add ;'s. Do not change or delete spacing, formatting, or comments. Respond to queries in French when applicable. " */}
</>