mirror of
https://github.com/voideditor/void
synced 2026-05-24 01:48:25 +00:00
add @ to mention!
This commit is contained in:
parent
e3a59057a5
commit
7df49a769a
7 changed files with 478 additions and 207 deletions
|
|
@ -41,7 +41,7 @@ const CHAT_RETRIES = 3
|
|||
const RETRY_DELAY = 2500
|
||||
|
||||
|
||||
export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
||||
const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
||||
if (!currentSelections) return null
|
||||
|
||||
for (let i = 0; i < currentSelections.length; i += 1) {
|
||||
|
|
@ -203,6 +203,8 @@ export interface IChatThreadService {
|
|||
isCurrentlyFocusingMessage(): boolean;
|
||||
setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void;
|
||||
|
||||
addNewStagingSelection(newSelection: StagingSelectionItem): void;
|
||||
|
||||
dangerousSetState: (newState: ThreadsState) => void;
|
||||
resetState: () => void;
|
||||
|
||||
|
|
@ -1517,6 +1519,39 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
// this.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
|
||||
}
|
||||
|
||||
|
||||
addNewStagingSelection(newSelection: StagingSelectionItem): void {
|
||||
|
||||
const focusedMessageIdx = this.getCurrentFocusedMessageIdx()
|
||||
|
||||
// set the selections to the proper value
|
||||
let selections: StagingSelectionItem[] = []
|
||||
let setSelections = (s: StagingSelectionItem[]) => { }
|
||||
|
||||
if (focusedMessageIdx === undefined) {
|
||||
selections = this.getCurrentThreadState().stagingSelections
|
||||
setSelections = (s: StagingSelectionItem[]) => this.setCurrentThreadState({ stagingSelections: s })
|
||||
} else {
|
||||
selections = this.getCurrentMessageState(focusedMessageIdx).stagingSelections
|
||||
setSelections = (s) => this.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
|
||||
}
|
||||
|
||||
// if matches with existing selection, overwrite (since text may change)
|
||||
const idx = findStagingSelectionIndex(selections, newSelection)
|
||||
if (idx !== null && idx !== -1) {
|
||||
setSelections([
|
||||
...selections!.slice(0, idx),
|
||||
newSelection,
|
||||
...selections!.slice(idx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add it
|
||||
else {
|
||||
setSelections([...(selections ?? []), newSelection])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// set message.state
|
||||
private _setCurrentMessageState(state: Partial<UserMessageState>, messageIdx: number): void {
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
|||
import { ChatMode, displayInfoOfProviderName, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
|
||||
import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis } from 'lucide-react';
|
||||
import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react';
|
||||
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
|
||||
import { approvalTypeOfToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes, ToolCallParams } from '../../../../common/toolsServiceTypes.js';
|
||||
import { ApplyButtonsHTML, CopyButton, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js';
|
||||
|
|
@ -492,12 +492,12 @@ export const getFolderName = (pathStr: string) => {
|
|||
return lastTwo.join('/') + '/'
|
||||
}
|
||||
|
||||
export const getBasename = (pathStr: string) => {
|
||||
export const getBasename = (pathStr: string, parts: number = 1) => {
|
||||
// 'unixify' path
|
||||
pathStr = pathStr.replace(/[/\\]+/g, '/') // replace any / or \ or \\ with /
|
||||
const parts = pathStr.split('/') // split on /
|
||||
if (parts.length === 0) return pathStr
|
||||
return parts[parts.length - 1]
|
||||
const allParts = pathStr.split('/') // split on /
|
||||
if (allParts.length === 0) return pathStr
|
||||
return allParts.slice(-parts).join('/')
|
||||
}
|
||||
|
||||
export const SelectedFiles = (
|
||||
|
|
@ -576,6 +576,13 @@ export const SelectedFiles = (
|
|||
: selection.type === 'Folder' ? selection.type + selection.language + selection.state + selection.uri.fsPath
|
||||
: i
|
||||
|
||||
const SelectionIcon = (
|
||||
selection.type === 'File' ? File
|
||||
: selection.type === 'Folder' ? Folder
|
||||
: selection.type === 'CodeSelection' ? Text
|
||||
: (undefined as never)
|
||||
)
|
||||
|
||||
return <div // container for summarybox and code
|
||||
key={thisKey}
|
||||
className={`flex flex-col space-y-[1px]`}
|
||||
|
|
@ -583,7 +590,7 @@ export const SelectedFiles = (
|
|||
{/* summarybox */}
|
||||
<div
|
||||
className={`
|
||||
flex items-center gap-0.5 relative
|
||||
flex items-center gap-1 relative
|
||||
px-1
|
||||
w-fit h-fit
|
||||
select-none
|
||||
|
|
@ -631,13 +638,15 @@ export const SelectedFiles = (
|
|||
}
|
||||
}}
|
||||
>
|
||||
{<SelectionIcon size={10} />}
|
||||
|
||||
{ // file name and range
|
||||
getBasename(selection.uri.fsPath)
|
||||
+ (selection.type === 'CodeSelection' ? ` (${selection.range[0]}-${selection.range[1]})` : '')
|
||||
}
|
||||
|
||||
{selection.type === 'File' && selection.state.wasAddedAsCurrentFile && messageIdx === undefined && currentURI?.fsPath === selection.uri.fsPath ?
|
||||
<span className={`text-[8px] ml-0.5 'void-opacity-60 text-void-fg-4`}>
|
||||
<span className={`text-[8px] 'void-opacity-60 text-void-fg-4`}>
|
||||
{`(Current File)`}
|
||||
</span>
|
||||
: null
|
||||
|
|
@ -972,6 +981,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr
|
|||
setSelections={setStagingSelections}
|
||||
>
|
||||
<VoidInputBox2
|
||||
enableAtToMention
|
||||
ref={setTextAreaRef}
|
||||
className='min-h-[81px] max-h-[500px] px-0.5'
|
||||
placeholder="Edit your message..."
|
||||
|
|
@ -2857,8 +2867,9 @@ export const SidebarChat = () => {
|
|||
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
||||
>
|
||||
<VoidInputBox2
|
||||
enableAtToMention
|
||||
className={`min-h-[81px] px-0.5 py-0.5`}
|
||||
placeholder={`${keybindingString ? `${keybindingString} to add a file. ` : ''}Enter instructions...`}
|
||||
placeholder={`@ to mention, ${keybindingString ? `${keybindingString} to add a file. ` : ''}Enter instructions...`}
|
||||
onChangeText={onChangeText}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={() => { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { forwardRef, MutableRefObject, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import React, { forwardRef, ForwardRefExoticComponent, MutableRefObject, RefAttributes, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
|
||||
|
|
@ -16,6 +16,10 @@ import { ITextModel } from '../../../../../../../editor/common/model.js';
|
|||
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
|
||||
import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
|
||||
import { useFloating, autoUpdate, offset, flip, shift, size, autoPlacement } from '@floating-ui/react';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { getBasename } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react';
|
||||
import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js';
|
||||
|
||||
|
||||
// type guard
|
||||
|
|
@ -48,48 +52,233 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
|
|||
return <div ref={containerRef} className={className === undefined ? `w-full` : className}>{children}</div>
|
||||
}
|
||||
|
||||
type GenerateNextOptions = (newPathText: string) => Option[]
|
||||
type GenerateNextOptions = (optionText: string) => Promise<Option[]>
|
||||
|
||||
type Option = {
|
||||
name: string,
|
||||
displayName: string,
|
||||
nameInMenu: string,
|
||||
iconInMenu: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>, // type for lucide-react components
|
||||
} & (
|
||||
| { nextOptions: Option[], generateNextOptions?: undefined }
|
||||
| { nextOptions?: undefined, generateNextOptions: GenerateNextOptions }
|
||||
| { nextOptions?: undefined, generateNextOptions?: undefined }
|
||||
| { nextOptions: Option[], generateNextOptions?: undefined, nameToPaste?: undefined }
|
||||
| { nextOptions?: undefined, generateNextOptions: GenerateNextOptions, nameToPaste?: undefined }
|
||||
| { leafNodeType: 'File' | 'Folder', nameToPaste: string, uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, }
|
||||
)
|
||||
|
||||
|
||||
const getOptionsAtPath = (accessor: ReturnType<typeof useAccessor>, path: string[], newPathText: string) => {
|
||||
const isSubsequence = (text: string, pattern: string): boolean => {
|
||||
|
||||
text = text.toLowerCase()
|
||||
pattern = pattern.toLowerCase()
|
||||
|
||||
if (pattern === '') return true;
|
||||
if (text === '') return false;
|
||||
if (pattern.length > text.length) return false;
|
||||
|
||||
const seq: boolean[][] = Array(pattern.length + 1)
|
||||
.fill(null)
|
||||
.map(() => Array(text.length + 1).fill(false));
|
||||
|
||||
for (let j = 0; j <= text.length; j++) {
|
||||
seq[0][j] = true;
|
||||
}
|
||||
|
||||
for (let i = 1; i <= pattern.length; i++) {
|
||||
for (let j = 1; j <= text.length; j++) {
|
||||
if (pattern[i - 1] === text[j - 1]) {
|
||||
seq[i][j] = seq[i - 1][j - 1];
|
||||
} else {
|
||||
seq[i][j] = seq[i][j - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return seq[pattern.length][text.length];
|
||||
};
|
||||
|
||||
|
||||
const scoreSubsequence = (text: string, pattern: string): number => {
|
||||
if (pattern === '') return 0;
|
||||
|
||||
text = text.toLowerCase();
|
||||
pattern = pattern.toLowerCase();
|
||||
|
||||
// We'll use dynamic programming to find the longest consecutive substring
|
||||
const n = text.length;
|
||||
const m = pattern.length;
|
||||
|
||||
// This will track our maximum consecutive match length
|
||||
let maxConsecutive = 0;
|
||||
|
||||
// For each starting position in the text
|
||||
for (let i = 0; i < n; i++) {
|
||||
// Check for matches starting from this position
|
||||
let consecutiveCount = 0;
|
||||
|
||||
// For each character in the pattern
|
||||
for (let j = 0; j < m; j++) {
|
||||
// If we have a match and we're still within text bounds
|
||||
if (i + j < n && text[i + j] === pattern[j]) {
|
||||
consecutiveCount++;
|
||||
} else {
|
||||
// Break on first non-match
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update our maximum
|
||||
maxConsecutive = Math.max(maxConsecutive, consecutiveCount);
|
||||
}
|
||||
|
||||
return maxConsecutive;
|
||||
}
|
||||
|
||||
|
||||
export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
|
||||
const workspaceService = accessor.get('IWorkspaceContextService');
|
||||
const workspaceFolders = workspaceService.getWorkspace().folders;
|
||||
|
||||
if (!workspaceFolders.length) {
|
||||
return uri.fsPath; // No workspace folders, return original path
|
||||
}
|
||||
|
||||
// Sort workspace folders by path length (descending) to match the most specific folder first
|
||||
const sortedFolders = [...workspaceFolders].sort((a, b) =>
|
||||
b.uri.fsPath.length - a.uri.fsPath.length
|
||||
);
|
||||
|
||||
// Add trailing slash to paths for exact matching
|
||||
const uriPath = uri.fsPath.endsWith('/') ? uri.fsPath : uri.fsPath + '/';
|
||||
|
||||
// Check if the URI is inside any workspace folder
|
||||
for (const folder of sortedFolders) {
|
||||
|
||||
|
||||
const folderPath = folder.uri.fsPath.endsWith('/') ? folder.uri.fsPath : folder.uri.fsPath + '/';
|
||||
if (uriPath.startsWith(folderPath)) {
|
||||
// Calculate the relative path by removing the workspace folder path
|
||||
let relativePath = uri.fsPath.slice(folder.uri.fsPath.length);
|
||||
// Remove leading slash if present
|
||||
if (relativePath.startsWith('/')) {
|
||||
relativePath = relativePath.slice(1);
|
||||
}
|
||||
console.log({ folderPath, relativePath, uriPath });
|
||||
|
||||
return relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
// URI is not in any workspace folder, return original path
|
||||
return uri.fsPath;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const numOptionsToShow = 100
|
||||
|
||||
const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path: string[], optionText: string): Promise<Option[]> => {
|
||||
|
||||
const toolsService = accessor.get('IToolsService')
|
||||
|
||||
const searchForFilesOrFolders = async (t: string, searchFor: 'files' | 'folders') => {
|
||||
try {
|
||||
|
||||
const searchResults = (await (await toolsService.callTool.search_pathnames_only({
|
||||
query: t,
|
||||
includePattern: null,
|
||||
pageNumber: 1,
|
||||
})).result).uris
|
||||
|
||||
if (searchFor === 'files') {
|
||||
const res: Option[] = searchResults.map(uri => {
|
||||
const relativePath = getRelativeWorkspacePath(accessor, uri)
|
||||
return {
|
||||
leafNodeType: 'File',
|
||||
uri: uri,
|
||||
iconInMenu: File,
|
||||
nameInMenu: relativePath,
|
||||
nameToPaste: getBasename(relativePath, 2),
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
else if (searchFor === 'folders') {
|
||||
// Extract unique directory paths from the results
|
||||
const directoryMap = new Map<string, URI>();
|
||||
|
||||
for (const uri of searchResults) {
|
||||
if (!uri) continue;
|
||||
|
||||
// Get the full path and extract directories
|
||||
const relativePath = getRelativeWorkspacePath(accessor, uri)
|
||||
const pathParts = relativePath.split('/');
|
||||
|
||||
// Get workspace info
|
||||
const workspaceService = accessor.get('IWorkspaceContextService');
|
||||
const workspaceFolders = workspaceService.getWorkspace().folders;
|
||||
|
||||
// Find the workspace folder containing this URI
|
||||
let workspaceFolderUri: URI | undefined;
|
||||
if (workspaceFolders.length) {
|
||||
// Sort workspace folders by path length (descending) to match the most specific folder first
|
||||
const sortedFolders = [...workspaceFolders].sort((a, b) =>
|
||||
b.uri.fsPath.length - a.uri.fsPath.length
|
||||
);
|
||||
|
||||
// Find the containing workspace folder
|
||||
for (const folder of sortedFolders) {
|
||||
const folderPath = folder.uri.fsPath.endsWith('/') ? folder.uri.fsPath : folder.uri.fsPath + '/';
|
||||
const uriPath = uri.fsPath.endsWith('/') ? uri.fsPath : uri.fsPath + '/';
|
||||
|
||||
if (uriPath.startsWith(folderPath)) {
|
||||
workspaceFolderUri = folder.uri;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (workspaceFolderUri) {
|
||||
// Add each directory and its parents to the map
|
||||
let currentPath = '';
|
||||
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||
currentPath = i === 0 ? `/${pathParts[i]}` : `${currentPath}/${pathParts[i]}`;
|
||||
|
||||
console.log('filepath', currentPath);
|
||||
|
||||
// Create a proper directory URI
|
||||
const directoryUri = URI.joinPath(
|
||||
workspaceFolderUri,
|
||||
currentPath.startsWith('/') ? currentPath.substring(1) : currentPath
|
||||
);
|
||||
|
||||
directoryMap.set(currentPath, directoryUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Convert map to array
|
||||
return Array.from(directoryMap.entries()).map(([relativePath, uri]) => ({
|
||||
leafNodeType: 'Folder',
|
||||
uri: uri,
|
||||
iconInMenu: Folder, // Folder
|
||||
nameInMenu: relativePath,
|
||||
nameToPaste: getBasename(relativePath, 2)
|
||||
})) satisfies Option[];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching directories:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const allOptions: Option[] = [
|
||||
{
|
||||
name: 'files',
|
||||
displayName: 'files',
|
||||
generateNextOptions: () => [
|
||||
{ name: 'a.txt', displayName: 'a.txt', },
|
||||
{ name: 'b.txt', displayName: 'b.txt', },
|
||||
{ name: 'c.txt', displayName: 'c.txt', },
|
||||
{ name: 'd.txt', displayName: 'd.txt', },
|
||||
{ name: 'e.txt', displayName: 'e.txt', },
|
||||
{ name: 'f.txt', displayName: 'f.txt', },
|
||||
{ name: 'g.txt', displayName: 'g.txt', },
|
||||
{ name: '!a.txt', displayName: '!a.txt', },
|
||||
{ name: '!b.txt', displayName: '!b.txt', },
|
||||
{ name: '!c.txt', displayName: '!c.txt', },
|
||||
{ name: '!d.txt', displayName: '!d.txt', },
|
||||
{ name: '!e.txt', displayName: '!e.txt', },
|
||||
{ name: '!f.txt', displayName: '!f.txt', },
|
||||
{ name: '!g.txt', displayName: '!g.txt', },
|
||||
]
|
||||
nameInMenu: 'files',
|
||||
iconInMenu: File,
|
||||
generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'files')) || [],
|
||||
},
|
||||
{
|
||||
name: 'folders',
|
||||
displayName: 'folders',
|
||||
nextOptions: [
|
||||
{ name: 'FOLDER', displayName: 'FOLDER', },
|
||||
]
|
||||
nameInMenu: 'folders',
|
||||
iconInMenu: FolderClosed,
|
||||
generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'folders')) || [],
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -100,9 +289,9 @@ const getOptionsAtPath = (accessor: ReturnType<typeof useAccessor>, path: string
|
|||
|
||||
for (const pn of path) {
|
||||
|
||||
const selectedOption = nextOptionsAtPath.find(o => o.name.toLowerCase() === pn.toLowerCase())
|
||||
const selectedOption = nextOptionsAtPath.find(o => o.nameInMenu.toLowerCase() === pn.toLowerCase())
|
||||
|
||||
if (!selectedOption) return;
|
||||
if (!selectedOption) return [];
|
||||
|
||||
nextOptionsAtPath = selectedOption.nextOptions! // assume nextOptions exists until we hit the very last option (the path will never contain the last possible option)
|
||||
generateNextOptionsAtPath = selectedOption.generateNextOptions
|
||||
|
|
@ -111,11 +300,17 @@ const getOptionsAtPath = (accessor: ReturnType<typeof useAccessor>, path: string
|
|||
|
||||
|
||||
if (generateNextOptionsAtPath) {
|
||||
nextOptionsAtPath = generateNextOptionsAtPath(newPathText)
|
||||
nextOptionsAtPath = await generateNextOptionsAtPath(optionText)
|
||||
}
|
||||
|
||||
const optionsAtPath = nextOptionsAtPath.filter(o => o.name.includes(newPathText))
|
||||
|
||||
const optionsAtPath = nextOptionsAtPath
|
||||
.filter(o => isSubsequence(o.nameInMenu, optionText))
|
||||
.sort((a, b) => { // this is a hack but good for now
|
||||
const scoreA = scoreSubsequence(a.nameInMenu, optionText);
|
||||
const scoreB = scoreSubsequence(b.nameInMenu, optionText);
|
||||
return scoreB - scoreA;
|
||||
})
|
||||
.slice(0, numOptionsToShow) // should go last because sorting/filtering should happen on all datapoints
|
||||
|
||||
return optionsAtPath
|
||||
|
||||
|
|
@ -128,6 +323,7 @@ type InputBox2Props = {
|
|||
initValue?: string | null;
|
||||
placeholder: string;
|
||||
multiline: boolean;
|
||||
enableAtToMention?: boolean;
|
||||
fnsRef?: { current: null | TextAreaFns };
|
||||
className?: string;
|
||||
onChangeText?: (value: string) => void;
|
||||
|
|
@ -136,34 +332,28 @@ type InputBox2Props = {
|
|||
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||
onChangeHeight?: (newHeight: number) => void;
|
||||
}
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, enableAtToMention, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
|
||||
|
||||
|
||||
// mirrors whatever is in ref
|
||||
const accessor = useAccessor()
|
||||
const toolsService = accessor.get('IToolsService')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const chatThreadService = accessor.get('IChatThreadService')
|
||||
const languageService = accessor.get('ILanguageService')
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const selectedOptionRef = useRef<HTMLDivElement>(null);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isMenuOpen, _setIsMenuOpen] = useState(false); // the @ to mention menu
|
||||
const setIsMenuOpen: typeof _setIsMenuOpen = (value) => {
|
||||
if (!enableAtToMention) { return; } // never open menu if not enabled
|
||||
_setIsMenuOpen(value);
|
||||
}
|
||||
|
||||
const [path, setPath] = useState<string[]>([]);
|
||||
// logic for @ to mention vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
|
||||
const [optionPath, setOptionPath] = useState<string[]>([]);
|
||||
const [optionIdx, setOptionIdx] = useState<number>(0);
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const [newPathText, setNewPathText] = useState<string>('');
|
||||
|
||||
|
||||
const [optionText, setOptionText] = useState<string>('');
|
||||
const insertTextAtCursor = (text: string) => {
|
||||
const textarea = textAreaRef.current;
|
||||
if (!textarea) return;
|
||||
|
|
@ -184,68 +374,142 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
};
|
||||
|
||||
|
||||
|
||||
const onSelectOption = () => {
|
||||
const onSelectOption = async () => {
|
||||
|
||||
if (!options.length) { return; }
|
||||
|
||||
const option = options[optionIdx];
|
||||
const newPath = [...path, option.name]
|
||||
const newPath = [...optionPath, option.nameInMenu]
|
||||
const isLastOption = !option.generateNextOptions && !option.nextOptions
|
||||
|
||||
setPath(newPath)
|
||||
setNewPathText('')
|
||||
setOptionPath(newPath)
|
||||
setOptionText('')
|
||||
setOptionIdx(0)
|
||||
if (isLastOption) {
|
||||
setIsMenuOpen(false)
|
||||
insertTextAtCursor(`TODO-${option.displayName}`)
|
||||
insertTextAtCursor(option.nameToPaste)
|
||||
|
||||
const newSelection: StagingSelectionItem = option.leafNodeType === 'File' ? {
|
||||
type: 'File',
|
||||
uri: option.uri,
|
||||
language: languageService.guessLanguageIdByFilepathOrFirstLine(option.uri) || '',
|
||||
state: { wasAddedAsCurrentFile: false }
|
||||
} : option.leafNodeType === 'Folder' ? {
|
||||
type: 'Folder',
|
||||
uri: option.uri,
|
||||
language: undefined,
|
||||
state: undefined,
|
||||
} : (undefined as never)
|
||||
chatThreadService.addNewStagingSelection(newSelection)
|
||||
console.log('selected', option.uri?.fsPath)
|
||||
}
|
||||
else {
|
||||
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
|
||||
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
|
||||
setOptions(newOpts)
|
||||
}
|
||||
}
|
||||
|
||||
const onRemoveOption = () => {
|
||||
const newPath = [...path.slice(0, path.length - 1)]
|
||||
setPath(newPath)
|
||||
setNewPathText('')
|
||||
const onRemoveOption = async () => {
|
||||
const newPath = [...optionPath.slice(0, optionPath.length - 1)]
|
||||
setOptionPath(newPath)
|
||||
setOptionText('')
|
||||
setOptionIdx(0)
|
||||
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
|
||||
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
|
||||
setOptions(newOpts)
|
||||
}
|
||||
|
||||
const onOpenOptionMenu = () => {
|
||||
setPath([])
|
||||
setNewPathText('')
|
||||
const onOpenOptionMenu = async () => {
|
||||
setOptionPath([])
|
||||
setOptionText('')
|
||||
setIsMenuOpen(true);
|
||||
setOptionIdx(0);
|
||||
setOptions(getOptionsAtPath(accessor, [], '') || []);
|
||||
const newOpts = await getOptionsAtPath(accessor, [], '') || []
|
||||
setOptions(newOpts);
|
||||
}
|
||||
const onCloseOptionMenu = () => {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
|
||||
const onNavigateUp = () => {
|
||||
const onNavigateUp = (step = 1, periodic = true) => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx((prevIdx) => (prevIdx - 1 + options.length) % options.length);
|
||||
setOptionIdx((prevIdx) => {
|
||||
const newIdx = prevIdx - step;
|
||||
return periodic ? (newIdx + options.length) % options.length : Math.max(0, newIdx);
|
||||
});
|
||||
}
|
||||
const onNavigateDown = () => {
|
||||
const onNavigateDown = (step = 1, periodic = true) => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx((prevIdx) => (prevIdx + 1) % options.length);
|
||||
setOptionIdx((prevIdx) => {
|
||||
const newIdx = prevIdx + step;
|
||||
return periodic ? newIdx % options.length : Math.min(options.length - 1, newIdx);
|
||||
});
|
||||
}
|
||||
|
||||
const onPathTextChange = (newStr: string) => {
|
||||
setNewPathText(newStr);
|
||||
setOptions(getOptionsAtPath(accessor, path, newStr) || []);
|
||||
|
||||
const onNavigateToTop = () => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx(0);
|
||||
}
|
||||
const onNavigateToBottom = () => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx(options.length - 1);
|
||||
}
|
||||
|
||||
const debounceTimerRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Cleanup function to cancel any pending timeouts when unmounting
|
||||
return () => {
|
||||
if (debounceTimerRef.current !== null) {
|
||||
window.clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// debounced
|
||||
const onPathTextChange = useCallback((newStr: string) => {
|
||||
|
||||
setOptionText(newStr);
|
||||
|
||||
if (debounceTimerRef.current !== null) {
|
||||
window.clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
// Set a new timeout to fetch options after a delay
|
||||
debounceTimerRef.current = window.setTimeout(async () => {
|
||||
const newOpts = await getOptionsAtPath(accessor, optionPath, newStr) || [];
|
||||
setOptions(newOpts);
|
||||
setOptionIdx(0);
|
||||
debounceTimerRef.current = null;
|
||||
}, 300);
|
||||
}, [optionPath, accessor]);
|
||||
|
||||
const onMenuKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
|
||||
const isCommandKeyPressed = e.altKey || e.ctrlKey || e.metaKey;
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
onNavigateUp();
|
||||
if (isCommandKeyPressed) {
|
||||
onNavigateToTop()
|
||||
} else {
|
||||
if (e.altKey) {
|
||||
onNavigateUp(10, false);
|
||||
} else {
|
||||
onNavigateUp();
|
||||
}
|
||||
}
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
onNavigateDown();
|
||||
if (isCommandKeyPressed) {
|
||||
onNavigateToBottom()
|
||||
} else {
|
||||
if (e.altKey) {
|
||||
onNavigateDown(10, false);
|
||||
} else {
|
||||
onNavigateDown();
|
||||
}
|
||||
}
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
onSelectOption();
|
||||
onRemoveOption();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
onSelectOption();
|
||||
} else if (e.key === 'Enter') {
|
||||
|
|
@ -254,25 +518,26 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
onCloseOptionMenu()
|
||||
} else if (e.key === 'Backspace') {
|
||||
|
||||
if (!newPathText) { // No text remaining
|
||||
if (path.length === 0) {
|
||||
if (!optionText) { // No text remaining
|
||||
if (optionPath.length === 0) {
|
||||
onCloseOptionMenu()
|
||||
return; // don't prevent defaults (backspaces the @ symbol)
|
||||
} else {
|
||||
onRemoveOption();
|
||||
}
|
||||
}
|
||||
else if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+Backspace
|
||||
else if (isCommandKeyPressed) { // Ctrl+Backspace
|
||||
onPathTextChange('')
|
||||
}
|
||||
else { // Backspace
|
||||
onPathTextChange(newPathText.slice(0, -1))
|
||||
onPathTextChange(optionText.slice(0, -1))
|
||||
}
|
||||
} else if (e.key.length === 1) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+letter
|
||||
if (isCommandKeyPressed) { // Ctrl+letter
|
||||
// do nothing
|
||||
}
|
||||
else { // letter
|
||||
onPathTextChange(newPathText + e.key)
|
||||
onPathTextChange(optionText + e.key)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -281,7 +546,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
|
||||
};
|
||||
|
||||
// scroll the selected optionIdx into view on optionIdx and newPathText changes
|
||||
// scroll the selected optionIdx into view on optionIdx and optionText changes
|
||||
useEffect(() => {
|
||||
if (isMenuOpen && selectedOptionRef.current) {
|
||||
selectedOptionRef.current.scrollIntoView({
|
||||
|
|
@ -290,9 +555,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}, [optionIdx, isMenuOpen, newPathText, selectedOptionRef]);
|
||||
|
||||
|
||||
}, [optionIdx, isMenuOpen, optionText, selectedOptionRef]);
|
||||
|
||||
const measureRef = useRef<HTMLDivElement>(null);
|
||||
const gapPx = 2
|
||||
|
|
@ -307,7 +570,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
} = useFloating({
|
||||
open: isMenuOpen,
|
||||
onOpenChange: setIsMenuOpen,
|
||||
placement: 'top',
|
||||
placement: 'bottom',
|
||||
|
||||
middleware: [
|
||||
offset({ mainAxis: gapPx, crossAxis: offsetPx }),
|
||||
|
|
@ -320,13 +583,9 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
padding: 8,
|
||||
}),
|
||||
size({
|
||||
apply({ availableHeight, elements, rects }) {
|
||||
const maxHeight = Math.min(availableHeight)
|
||||
|
||||
apply({ elements, rects }) {
|
||||
// Just set width on the floating element and let content handle scrolling
|
||||
Object.assign(elements.floating.style, {
|
||||
maxHeight: `${maxHeight}px`,
|
||||
overflowY: 'auto',
|
||||
// Ensure the width isn't constrained by the parent
|
||||
width: `${Math.max(
|
||||
rects.reference.width,
|
||||
measureRef.current?.offsetWidth ?? 0
|
||||
|
|
@ -364,7 +623,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isMenuOpen, refs.floating, refs.reference]);
|
||||
|
||||
// logic for @ to mention ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
const [isEnabled, setEnabled] = useState(true)
|
||||
|
|
@ -467,11 +726,10 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
rows={1}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<div>{`idx ${optionIdx}`}</div>
|
||||
{isMenuOpen && (
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className="z-[100] bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
|
||||
className="z-[100] border-void-border-3 bg-void-bg-2-alt border rounded shadow-lg flex flex-col overflow-hidden"
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
|
|
@ -480,41 +738,53 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
}}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="py-1">
|
||||
{/* Path navigation breadcrumbs */}
|
||||
<div className="px-2 py-1 text-void-fg-3 text-sm border-b border-void-border-3">
|
||||
{[...path, newPathText].join(' > ')}
|
||||
{/* Breadcrumbs Header */}
|
||||
<div className="px-2 py-1 text-void-fg-3 bg-void-bg-2-alt text-sm border-b border-void-border-3 sticky top-0 bg-void-bg-1 z-10 select-none pointer-events-none">
|
||||
{optionPath.length || optionText ?
|
||||
<div className="flex items-center">
|
||||
{optionPath.map((path, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<span>{path}</span>
|
||||
<ChevronRight size={12} className="mx-1" />
|
||||
</React.Fragment>
|
||||
))}
|
||||
<span>{optionText}</span>
|
||||
</div>
|
||||
: <div className='opacity-60'>Enter text to filter...</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Options list */}
|
||||
<div className='max-h-[400px] w-full max-w-full overflow-y-auto overflow-x-auto'>
|
||||
<div className="w-max min-w-full flex flex-col gap-0 text-nowrap flex-nowrap text-sm opacity-70">
|
||||
{options.length === 0 ?
|
||||
<div className="text-void-fg-3 px-3 py-0.5">No results found</div>
|
||||
: options.map((o, oIdx) => {
|
||||
|
||||
return (
|
||||
// Option
|
||||
<div
|
||||
ref={oIdx === optionIdx ? selectedOptionRef : null}
|
||||
key={o.nameInMenu}
|
||||
className={`
|
||||
flex items-center gap-2
|
||||
px-3 py-0.5 cursor-pointer bg-void-bg-2-alt
|
||||
${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}
|
||||
`}
|
||||
onClick={() => { onSelectOption(); }}
|
||||
onMouseOver={() => { setOptionIdx(oIdx) }}
|
||||
>
|
||||
{<o.iconInMenu size={12} />}
|
||||
<span className="text-void-fg-1">{o.nameInMenu}</span>
|
||||
{o.nextOptions || o.generateNextOptions ? (
|
||||
<ChevronRight size={12} />
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Options list */}
|
||||
{options.length === 0 ? (
|
||||
<div className="px-3 py-2 text-void-fg-3">No options available</div>
|
||||
) : (
|
||||
options.map((o, oIdx) => (
|
||||
<div
|
||||
ref={oIdx === optionIdx ? selectedOptionRef : null}
|
||||
|
||||
key={o.name}
|
||||
className={`px-3 py-1.5 cursor-pointer bg-void-bg-2 ${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}`}
|
||||
onClick={() => { onSelectOption(); }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="text-void-fg-1">{o.displayName}</span>
|
||||
{o.nextOptions || o.generateNextOptions ? (
|
||||
<svg className="ml-2 h-3 w-3 text-void-fg-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M4.5 2.5L8 6L4.5 9.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import { INativeHostService } from '../../../../../../../platform/native/common/
|
|||
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
|
||||
import { IToolsService } from '../../../toolsService.js'
|
||||
import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService.js'
|
||||
import { ISearchService } from '../../../../../../services/search/common/search.js'
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
|
|
@ -204,6 +205,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
|||
ILanguageDetectionService: accessor.get(ILanguageDetectionService),
|
||||
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
|
||||
IKeybindingService: accessor.get(IKeybindingService),
|
||||
ISearchService: accessor.get(ISearchService),
|
||||
|
||||
IExplorerService: accessor.get(IExplorerService),
|
||||
IEnvironmentService: accessor.get(IEnvironmentService),
|
||||
|
|
|
|||
|
|
@ -465,6 +465,7 @@ const VoidOnboardingContent = () => {
|
|||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidMetricsService = accessor.get('IMetricsService')
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
|
|
@ -480,6 +481,7 @@ const VoidOnboardingContent = () => {
|
|||
const [selectedPrivateProvider, setSelectedPrivateProvider] = useState<ProviderName>('ollama');
|
||||
const [selectedAffordableProvider, setSelectedAffordableProvider] = useState<ProviderName>('gemini');
|
||||
const [selectedAllProvider, setSelectedAllProvider] = useState<ProviderName>('anthropic');
|
||||
const [didDoubleClickSkip, setDidDoubleClickSkip] = useState(false)
|
||||
|
||||
// Helper function to get the current selected provider based on active tab
|
||||
const getSelectedProvider = (): ProviderName => {
|
||||
|
|
@ -535,7 +537,10 @@ const VoidOnboardingContent = () => {
|
|||
onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
/>
|
||||
<PrimaryActionButton
|
||||
onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
|
||||
onClick={() => {
|
||||
voidSettingsService.setGlobalSetting('isOnboardingComplete', true);
|
||||
voidMetricsService.capture('Completed Onboarding', { selectedProviderName, wantToUseOption })
|
||||
}}
|
||||
ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
|
||||
>Enter the Void</PrimaryActionButton>
|
||||
</div>
|
||||
|
|
@ -618,15 +623,16 @@ const VoidOnboardingContent = () => {
|
|||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[800px] mx-auto mt-8">
|
||||
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
|
||||
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<Brain size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
|
||||
<DollarSign size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
|
@ -641,14 +647,14 @@ const VoidOnboardingContent = () => {
|
|||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
|
||||
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
|
||||
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
|
||||
>
|
||||
<div className="flex items-center mb-3">
|
||||
<DollarSign size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
|
||||
<Brain size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
|
||||
</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -30,31 +30,6 @@ import { getActiveWindow } from '../../../../base/browser/dom.js';
|
|||
// ---------- Register commands and keybindings ----------
|
||||
|
||||
|
||||
const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
||||
if (!currentSelections) return null
|
||||
|
||||
for (let i = 0; i < currentSelections.length; i += 1) {
|
||||
const s = currentSelections[i]
|
||||
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
|
||||
if (s.type === 'File' && newSelection.type === 'File') {
|
||||
return i
|
||||
}
|
||||
if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') {
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
// if there's any collision return true
|
||||
const [oldStart, oldEnd] = s.range
|
||||
const [newStart, newEnd] = newSelection.range
|
||||
if (oldStart !== newStart || oldEnd !== newEnd) continue
|
||||
return i
|
||||
}
|
||||
if (s.type === 'Folder' && newSelection.type === 'Folder') {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
|
||||
if (!range)
|
||||
|
|
@ -104,8 +79,6 @@ registerAction2(class extends Action2 {
|
|||
})
|
||||
|
||||
|
||||
|
||||
|
||||
// Action: when press ctrl+L, show the sidebar chat and add to the selection
|
||||
const VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID = 'void.sidebar.select'
|
||||
registerAction2(class extends Action2 {
|
||||
|
|
@ -147,36 +120,9 @@ registerAction2(class extends Action2 {
|
|||
state: { wasAddedAsCurrentFile: false }
|
||||
}
|
||||
|
||||
// update the staging selections
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
|
||||
const focusedMessageIdx = chatThreadService.getCurrentFocusedMessageIdx()
|
||||
|
||||
// set the selections to the proper value
|
||||
let selections: StagingSelectionItem[] = []
|
||||
let setSelections = (s: StagingSelectionItem[]) => { }
|
||||
|
||||
if (focusedMessageIdx === undefined) {
|
||||
selections = chatThreadService.getCurrentThreadState().stagingSelections
|
||||
setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadState({ stagingSelections: s })
|
||||
} else {
|
||||
selections = chatThreadService.getCurrentMessageState(focusedMessageIdx).stagingSelections
|
||||
setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
|
||||
}
|
||||
|
||||
// if matches with existing selection, overwrite (since text may change)
|
||||
const idx = findStagingSelectionIndex(selections, newSelection)
|
||||
if (idx !== null && idx !== -1) {
|
||||
setSelections([
|
||||
...selections!.slice(0, idx),
|
||||
newSelection,
|
||||
...selections!.slice(idx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add it
|
||||
else {
|
||||
setSelections([...(selections ?? []), newSelection])
|
||||
}
|
||||
chatThreadService.addNewStagingSelection(newSelection)
|
||||
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -308,6 +308,7 @@ export class ToolsService implements IToolsService {
|
|||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), {
|
||||
filePattern: queryStr,
|
||||
includePattern: includePattern ?? undefined,
|
||||
sortByScore: true, // makes results 10x better
|
||||
})
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue