mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
inline suggestions finished
This commit is contained in:
parent
6a73930f4b
commit
7fdde7af3e
12 changed files with 405 additions and 5 deletions
|
|
@ -74,7 +74,7 @@ function saveStylesFile() {
|
|||
} catch (err) {
|
||||
console.error('[scope-tailwind] Error saving styles.css:', err);
|
||||
}
|
||||
}, 4000);
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
|
|||
null
|
||||
)
|
||||
|
||||
const statusIndicatorHTML = <StatusIndicator color={color} />
|
||||
const statusIndicatorHTML = <StatusIndicator className='mx-2' color={color} />
|
||||
|
||||
return {
|
||||
statusIndicatorHTML,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
|
||||
|
||||
import '../styles.css'
|
||||
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { Circle, MoreVertical } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { VoidSelectionHelperProps } from '../../../../../../contrib/void/browser/voidSelectionHelperWidget.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
|
||||
|
||||
export const VoidSelectionHelperMain = (props: VoidSelectionHelperProps) => {
|
||||
|
||||
const isDark = useIsDark()
|
||||
|
||||
return <div
|
||||
className={`@@void-scope ${isDark ? 'dark' : ''} pointer-events-none`}
|
||||
>
|
||||
<VoidSelectionHelper {...props} />
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
const VoidSelectionHelper = ({ rerenderKey }: VoidSelectionHelperProps) => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const keybindingService = accessor.get('IKeybindingService')
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
const ctrlLKeybind = keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID)
|
||||
const ctrlKKeybind = keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID)
|
||||
|
||||
const dividerHTML = <div className='w-[0.5px] bg-void-border-3'></div>
|
||||
|
||||
const [reactRerenderCount, setReactRerenderKey] = useState(rerenderKey)
|
||||
const [clickState, setClickState] = useState<'init' | 'clickedOption' | 'clickedMore'>('init')
|
||||
|
||||
// rerender when the key changes
|
||||
if (reactRerenderCount !== rerenderKey) {
|
||||
setReactRerenderKey(rerenderKey)
|
||||
setClickState('init')
|
||||
}
|
||||
// useEffect(() => {
|
||||
// }, [rerenderKey, reactRerenderCount, setReactRerenderKey, setClickState])
|
||||
|
||||
// if the user selected an option, close
|
||||
if (clickState === 'clickedOption') {
|
||||
return null
|
||||
}
|
||||
|
||||
const defaultHTML = <>
|
||||
{ctrlLKeybind &&
|
||||
<div
|
||||
className='
|
||||
flex items-center px-2 py-1.5
|
||||
hover:bg-void-bg-1
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
|
||||
setClickState('clickedOption');
|
||||
}}
|
||||
>
|
||||
<span>Add to Chat</span>
|
||||
<span className='ml-1 px-1 rounded bg-[var(--vscode-keybindingLabel-background)] text-[var(--vscode-keybindingLabel-foreground)] border border-[var(--vscode-keybindingLabel-border)]'>
|
||||
{ctrlLKeybind.getLabel()}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
{ctrlLKeybind && ctrlKKeybind &&
|
||||
dividerHTML
|
||||
}
|
||||
{ctrlKKeybind &&
|
||||
<div
|
||||
className='
|
||||
flex items-center px-2 py-1.5
|
||||
hover:bg-void-bg-1
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
commandService.executeCommand(VOID_CTRL_K_ACTION_ID)
|
||||
setClickState('clickedOption');
|
||||
}}
|
||||
>
|
||||
<span className='ml-1'>Edit Inline</span>
|
||||
<span className='ml-1 px-1 rounded bg-[var(--vscode-keybindingLabel-background)] text-[var(--vscode-keybindingLabel-foreground)] border border-[var(--vscode-keybindingLabel-border)]'>
|
||||
{ctrlKKeybind.getLabel()}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
{dividerHTML}
|
||||
|
||||
<div
|
||||
className='
|
||||
flex items-center px-0.5
|
||||
hover:bg-void-bg-1
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
setClickState('clickedMore');
|
||||
}}
|
||||
>
|
||||
<MoreVertical className="w-4" />
|
||||
</div>
|
||||
</>
|
||||
|
||||
|
||||
const moreOptionsHTML = <>
|
||||
<div
|
||||
className='
|
||||
flex items-center px-2 py-1.5
|
||||
hover:bg-void-bg-1
|
||||
cursor-pointer
|
||||
'
|
||||
onClick={() => {
|
||||
commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
|
||||
setClickState('clickedOption');
|
||||
}}
|
||||
>
|
||||
Disable Suggestions?
|
||||
</div>
|
||||
</>
|
||||
|
||||
return <div className='
|
||||
pointer-events-auto select-none
|
||||
ml-4 z-[1000]
|
||||
rounded-sm shadow-md flex flex-nowrap text-nowrap
|
||||
border border-void-border-3 bg-void-bg-2
|
||||
transition-all duration-200
|
||||
'>
|
||||
{clickState === 'init' ? defaultHTML
|
||||
: clickState === 'clickedMore' ? moreOptionsHTML
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -5,5 +5,9 @@
|
|||
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { VoidCommandBarMain } from './VoidCommandBar.js'
|
||||
import { VoidSelectionHelperMain } from './VoidSelectionHelper.js'
|
||||
|
||||
export const mountVoidCommandBar = mountFnGenerator(VoidCommandBarMain)
|
||||
|
||||
export const mountVoidSelectionHelper = mountFnGenerator(VoidSelectionHelperMain)
|
||||
|
||||
|
|
@ -534,9 +534,26 @@ export const FeaturesTab = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>Editor</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Settings that control the visibility of suggestions and widgets in the code editor.`}</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Auto Accept Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={voidSettingsState.globalSettings.showInlineSuggestions}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { defineConfig } from 'tsup'
|
|||
|
||||
export default defineConfig({
|
||||
entry: [
|
||||
'./src2/void-command-bar-tsx/index.tsx',
|
||||
'./src2/void-editor-widgets-tsx/index.tsx',
|
||||
'./src2/sidebar-tsx/index.tsx',
|
||||
'./src2/void-settings-tsx/index.tsx',
|
||||
'./src2/quick-edit-tsx/index.tsx',
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ registerAction2(class extends Action2 {
|
|||
super({
|
||||
id: VOID_CTRL_L_ACTION_ID,
|
||||
f1: true,
|
||||
title: localize2('voidCtrlL', 'Void: Add Select to Chat'),
|
||||
title: localize2('voidCtrlL', 'Void: Add Selection to Chat'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ import './metricsPollService.js'
|
|||
// helper services
|
||||
import './helperServices/consistentItemService.js'
|
||||
|
||||
// register selection helper
|
||||
import './voidSelectionHelperWidget.js'
|
||||
|
||||
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
|
||||
|
||||
// llmMessage
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { Widget } from '../../../../base/browser/ui/widget.js';
|
|||
import { IOverlayWidget, ICodeEditor, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { mountVoidCommandBar } from './react/out/void-command-bar-tsx/index.js'
|
||||
import { mountVoidCommandBar } from './react/out/void-editor-widgets-tsx/index.js'
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IEditCodeService } from './editCodeServiceInterface.js';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { ICursorSelectionChangedEvent } from '../../../../editor/common/cursorEvents.js';
|
||||
import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
|
||||
import { Selection } from '../../../../editor/common/core/selection.js';
|
||||
import { RunOnceScheduler } from '../../../../base/common/async.js';
|
||||
import * as dom from '../../../../base/browser/dom.js';
|
||||
import { mountVoidSelectionHelper } from './react/out/void-editor-widgets-tsx/index.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
|
||||
|
||||
const minDistanceFromRightPx = 400;
|
||||
const minDistanceFromLeftPx = 60;
|
||||
|
||||
|
||||
export type VoidSelectionHelperProps = {
|
||||
rerenderKey: number // alternates between 0 and 1
|
||||
}
|
||||
|
||||
|
||||
export class SelectionHelperContribution extends Disposable implements IEditorContribution, IOverlayWidget {
|
||||
public static readonly ID = 'editor.contrib.voidSelectionHelper';
|
||||
// react
|
||||
private _rootHTML: HTMLElement;
|
||||
private _rerender: (props?: any) => void = () => { };
|
||||
private _rerenderKey: number = 0;
|
||||
private _reactComponentDisposable: IDisposable | null = null;
|
||||
|
||||
// internal
|
||||
private _isVisible = false;
|
||||
private _showScheduler: RunOnceScheduler;
|
||||
private _lastSelection: Selection | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IVoidSettingsService private readonly _voidSettingsService: IVoidSettingsService
|
||||
) {
|
||||
super();
|
||||
|
||||
// Create the container element for React component
|
||||
const { root, content } = dom.h('div@root', [
|
||||
dom.h('div@content', [])
|
||||
]);
|
||||
|
||||
// Set styles for container
|
||||
root.style.position = 'absolute';
|
||||
root.style.display = 'none'; // Start hidden
|
||||
|
||||
// Initialize React component
|
||||
this._instantiationService.invokeFunction(accessor => {
|
||||
if (this._reactComponentDisposable) {
|
||||
this._reactComponentDisposable.dispose();
|
||||
}
|
||||
const res = mountVoidSelectionHelper(content, accessor);
|
||||
if (!res) return;
|
||||
|
||||
this._reactComponentDisposable = res;
|
||||
this._rerender = res.rerender;
|
||||
|
||||
this._register(this._reactComponentDisposable);
|
||||
|
||||
|
||||
});
|
||||
|
||||
this._rootHTML = root;
|
||||
|
||||
// Register as overlay widget
|
||||
this._editor.addOverlayWidget(this);
|
||||
|
||||
// Use scheduler to debounce showing widget
|
||||
this._showScheduler = new RunOnceScheduler(() => {
|
||||
if (this._lastSelection) {
|
||||
this._showHelperForSelection(this._lastSelection);
|
||||
}
|
||||
}, 50);
|
||||
|
||||
// Register event listeners
|
||||
this._register(this._editor.onDidChangeCursorSelection(e => this._onSelectionChange(e)));
|
||||
|
||||
// Add a flag to track if mouse is over the widget
|
||||
let isMouseOverWidget = false;
|
||||
this._rootHTML.addEventListener('mouseenter', () => {
|
||||
isMouseOverWidget = true;
|
||||
});
|
||||
this._rootHTML.addEventListener('mouseleave', () => {
|
||||
isMouseOverWidget = false;
|
||||
});
|
||||
|
||||
// Only hide helper when text editor loses focus and mouse is not over the widget
|
||||
this._register(this._editor.onDidBlurEditorText(() => {
|
||||
if (!isMouseOverWidget) {
|
||||
this._hideHelper();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this._editor.onDidScrollChange(() => this._updatePositionIfVisible()));
|
||||
this._register(this._editor.onDidLayoutChange(() => this._updatePositionIfVisible()));
|
||||
}
|
||||
|
||||
// IOverlayWidget implementation
|
||||
public getId(): string {
|
||||
return SelectionHelperContribution.ID;
|
||||
}
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._rootHTML;
|
||||
}
|
||||
|
||||
public getPosition(): IOverlayWidgetPosition | null {
|
||||
return null; // We position manually
|
||||
}
|
||||
|
||||
private _onSelectionChange(e: ICursorSelectionChangedEvent): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selection = this._editor.getSelection();
|
||||
|
||||
if (!selection || selection.isEmpty()) {
|
||||
this._hideHelper();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get selection text to check if it's worth showing the helper
|
||||
const text = this._editor.getModel()!.getValueInRange(selection);
|
||||
if (text.length < 3) {
|
||||
this._hideHelper();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store selection
|
||||
this._lastSelection = new Selection(
|
||||
selection.startLineNumber,
|
||||
selection.startColumn,
|
||||
selection.endLineNumber,
|
||||
selection.endColumn
|
||||
);
|
||||
|
||||
this._showScheduler.schedule();
|
||||
}
|
||||
|
||||
// Update the _showHelperForSelection method to work with the React component
|
||||
private _showHelperForSelection(selection: Selection): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel()!;
|
||||
|
||||
// Calculate the middle line of the selection
|
||||
const startLine = selection.startLineNumber;
|
||||
const endLine = selection.endLineNumber;
|
||||
const middleLineNumber = Math.floor(startLine + (endLine - startLine) / 2);
|
||||
|
||||
// Get the content of the middle line
|
||||
const lineContent = model.getLineContent(middleLineNumber);
|
||||
|
||||
// Find the position at the end of the middle line
|
||||
const endOfLinePos = this._editor.getScrolledVisiblePosition({
|
||||
lineNumber: middleLineNumber,
|
||||
column: lineContent.length + 1 // +1 because columns are 1-based
|
||||
});
|
||||
|
||||
if (!endOfLinePos) {
|
||||
this._hideHelper();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate right edge of visible editor area
|
||||
const editorWidth = this._editor.getLayoutInfo().width;
|
||||
|
||||
// Position the helper element at the end of the middle line but ensure it's visible
|
||||
const xPosition = Math.max(Math.min(endOfLinePos.left, editorWidth - minDistanceFromRightPx), minDistanceFromLeftPx);
|
||||
|
||||
// Update the React component position
|
||||
this._rootHTML.style.left = `${xPosition}px`;
|
||||
this._rootHTML.style.top = `${endOfLinePos.top}px`;
|
||||
this._rootHTML.style.display = 'flex'; // Show the container
|
||||
|
||||
this._isVisible = true;
|
||||
|
||||
// rerender
|
||||
const enabled = this._voidSettingsService.state.globalSettings.showInlineSuggestions
|
||||
&& this._editor.hasTextFocus() // needed since VS Code counts unfocused selections as selections, which causes this to rerender when it shouldnt (bad ux)
|
||||
|
||||
if (enabled) {
|
||||
this._rerender({ rerenderKey: this._rerenderKey } satisfies VoidSelectionHelperProps)
|
||||
this._rerenderKey = (this._rerenderKey + 1) % 2;
|
||||
// this._reactComponentRerender();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private _hideHelper(): void {
|
||||
this._rootHTML.style.display = 'none';
|
||||
this._isVisible = false;
|
||||
this._lastSelection = null;
|
||||
}
|
||||
|
||||
private _updatePositionIfVisible(): void {
|
||||
if (!this._isVisible || !this._lastSelection || !this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showHelperForSelection(this._lastSelection);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._hideHelper();
|
||||
if (this._reactComponentDisposable) {
|
||||
this._reactComponentDisposable.dispose();
|
||||
}
|
||||
this._editor.removeOverlayWidget(this);
|
||||
this._showScheduler.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Register the contribution
|
||||
registerEditorContribution(SelectionHelperContribution.ID, SelectionHelperContribution, EditorContributionInstantiation.Eager);
|
||||
|
|
@ -389,6 +389,7 @@ export type GlobalSettings = {
|
|||
enableFastApply: boolean;
|
||||
chatMode: ChatMode;
|
||||
autoApprove: boolean;
|
||||
showInlineSuggestions: boolean;
|
||||
}
|
||||
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
|
|
@ -399,6 +400,7 @@ export const defaultGlobalSettings: GlobalSettings = {
|
|||
enableFastApply: true,
|
||||
chatMode: 'agent',
|
||||
autoApprove: false,
|
||||
showInlineSuggestions: true,
|
||||
}
|
||||
|
||||
export type GlobalSettingName = keyof GlobalSettings
|
||||
|
|
|
|||
Loading…
Reference in a new issue