inline suggestions finished

This commit is contained in:
Mathew Pareles 2025-03-29 03:16:30 -07:00
parent 6a73930f4b
commit 7fdde7af3e
12 changed files with 405 additions and 5 deletions

View file

@ -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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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);

View file

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