mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Retrieve workspace filepaths for context menu
This commit is contained in:
@@ -11,7 +11,7 @@ import { serializeError } from "serialize-error"
|
|||||||
import * as vscode from "vscode"
|
import * as vscode from "vscode"
|
||||||
import { ApiHandler, buildApiHandler } from "./api"
|
import { ApiHandler, buildApiHandler } from "./api"
|
||||||
import { TerminalManager } from "./integrations/TerminalManager"
|
import { TerminalManager } from "./integrations/TerminalManager"
|
||||||
import { LIST_FILES_LIMIT, listFiles, parseSourceCodeForDefinitionsTopLevel } from "./parse-source-code"
|
import { listFiles, parseSourceCodeForDefinitionsTopLevel } from "./parse-source-code"
|
||||||
import { ClaudeDevProvider } from "./providers/ClaudeDevProvider"
|
import { ClaudeDevProvider } from "./providers/ClaudeDevProvider"
|
||||||
import { ApiConfiguration } from "./shared/api"
|
import { ApiConfiguration } from "./shared/api"
|
||||||
import { ClaudeRequestResult } from "./shared/ClaudeRequestResult"
|
import { ClaudeRequestResult } from "./shared/ClaudeRequestResult"
|
||||||
@@ -1187,8 +1187,8 @@ export class ClaudeDev {
|
|||||||
try {
|
try {
|
||||||
const recursive = recursiveRaw?.toLowerCase() === "true"
|
const recursive = recursiveRaw?.toLowerCase() === "true"
|
||||||
const absolutePath = path.resolve(cwd, relDirPath)
|
const absolutePath = path.resolve(cwd, relDirPath)
|
||||||
const files = await listFiles(absolutePath, recursive)
|
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
|
||||||
const result = this.formatFilesList(absolutePath, files)
|
const result = this.formatFilesList(absolutePath, files, didHitLimit)
|
||||||
|
|
||||||
const message = JSON.stringify({
|
const message = JSON.stringify({
|
||||||
tool: recursive ? "listFilesRecursive" : "listFilesTopLevel",
|
tool: recursive ? "listFilesRecursive" : "listFilesTopLevel",
|
||||||
@@ -1245,7 +1245,7 @@ export class ClaudeDev {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formatFilesList(absolutePath: string, files: string[]): string {
|
formatFilesList(absolutePath: string, files: string[], didHitLimit: boolean): string {
|
||||||
const sorted = files
|
const sorted = files
|
||||||
.map((file) => {
|
.map((file) => {
|
||||||
// convert absolute path to relative path
|
// convert absolute path to relative path
|
||||||
@@ -1273,11 +1273,12 @@ export class ClaudeDev {
|
|||||||
// the shorter one comes first
|
// the shorter one comes first
|
||||||
return aParts.length - bParts.length
|
return aParts.length - bParts.length
|
||||||
})
|
})
|
||||||
if (sorted.length >= LIST_FILES_LIMIT) {
|
if (didHitLimit) {
|
||||||
const truncatedList = sorted.slice(0, LIST_FILES_LIMIT).join("\n")
|
return `${sorted.join(
|
||||||
return `${truncatedList}\n\n(Truncated at ${LIST_FILES_LIMIT} results. Try listing files in subdirectories if you need to explore further.)`
|
"\n"
|
||||||
|
)}\n\n(Truncated at 200 results. Try listing files in subdirectories if you need to explore further.)`
|
||||||
} else if (sorted.length === 0 || (sorted.length === 1 && sorted[0] === "")) {
|
} else if (sorted.length === 0 || (sorted.length === 1 && sorted[0] === "")) {
|
||||||
return "No files found or you do not have permission to view this directory."
|
return "No files found."
|
||||||
} else {
|
} else {
|
||||||
return sorted.join("\n")
|
return sorted.join("\n")
|
||||||
}
|
}
|
||||||
@@ -1937,8 +1938,8 @@ ${this.customInstructions.trim()}
|
|||||||
|
|
||||||
if (includeFileDetails) {
|
if (includeFileDetails) {
|
||||||
const isDesktop = cwd === path.join(os.homedir(), "Desktop")
|
const isDesktop = cwd === path.join(os.homedir(), "Desktop")
|
||||||
const files = await listFiles(cwd, !isDesktop)
|
const [files, didHitLimit] = await listFiles(cwd, !isDesktop, 200)
|
||||||
const result = this.formatFilesList(cwd, files)
|
const result = this.formatFilesList(cwd, files, didHitLimit)
|
||||||
details += `\n\n# Current Working Directory (${cwd}) Files\n${result}${
|
details += `\n\n# Current Working Directory (${cwd}) Files\n${result}${
|
||||||
isDesktop
|
isDesktop
|
||||||
? "\n(Note: Only top-level contents shown for Desktop by default. Use list_files to explore further if necessary.)"
|
? "\n(Note: Only top-level contents shown for Desktop by default. Use list_files to explore further if necessary.)"
|
||||||
|
|||||||
108
src/integrations/WorkspaceTracker.ts
Normal file
108
src/integrations/WorkspaceTracker.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import * as vscode from "vscode"
|
||||||
|
import * as path from "path"
|
||||||
|
import { listFiles } from "../parse-source-code/index"
|
||||||
|
import { ClaudeDevProvider } from "../providers/ClaudeDevProvider"
|
||||||
|
|
||||||
|
class WorkspaceTracker {
|
||||||
|
private providerRef: WeakRef<ClaudeDevProvider>
|
||||||
|
private disposables: vscode.Disposable[] = []
|
||||||
|
private filePaths: Set<string> = new Set()
|
||||||
|
|
||||||
|
constructor(provider: ClaudeDevProvider) {
|
||||||
|
this.providerRef = new WeakRef(provider)
|
||||||
|
this.registerListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
async initializeFilePaths() {
|
||||||
|
// should not auto get filepaths for desktop since it would immediately show permission popup before claude every creates a file
|
||||||
|
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||||
|
if (!cwd) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const [files, _] = await listFiles(cwd, true, 500)
|
||||||
|
files.forEach((file) => this.filePaths.add(file))
|
||||||
|
this.workspaceDidUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerListeners() {
|
||||||
|
// Listen for file creation
|
||||||
|
this.disposables.push(vscode.workspace.onDidCreateFiles(this.onFilesCreated.bind(this)))
|
||||||
|
|
||||||
|
// Listen for file deletion
|
||||||
|
this.disposables.push(vscode.workspace.onDidDeleteFiles(this.onFilesDeleted.bind(this)))
|
||||||
|
|
||||||
|
// Listen for file renaming
|
||||||
|
this.disposables.push(vscode.workspace.onDidRenameFiles(this.onFilesRenamed.bind(this)))
|
||||||
|
|
||||||
|
// Listen for file changes
|
||||||
|
this.disposables.push(vscode.workspace.onDidChangeTextDocument(this.onFileChanged.bind(this)))
|
||||||
|
|
||||||
|
// Listen for workspace folder changes
|
||||||
|
this.disposables.push(vscode.workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged.bind(this)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFilesCreated(event: vscode.FileCreateEvent) {
|
||||||
|
event.files.forEach(async (file) => {
|
||||||
|
this.filePaths.add(file.fsPath)
|
||||||
|
this.workspaceDidUpdate()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFilesDeleted(event: vscode.FileDeleteEvent) {
|
||||||
|
event.files.forEach((file) => {
|
||||||
|
if (this.filePaths.delete(file.fsPath)) {
|
||||||
|
this.workspaceDidUpdate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private onFilesRenamed(event: vscode.FileRenameEvent) {
|
||||||
|
event.files.forEach(async (file) => {
|
||||||
|
this.filePaths.delete(file.oldUri.fsPath)
|
||||||
|
this.filePaths.add(file.newUri.fsPath)
|
||||||
|
this.workspaceDidUpdate()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onFileChanged(event: vscode.TextDocumentChangeEvent) {
|
||||||
|
const filePath = event.document.uri.fsPath
|
||||||
|
if (!this.filePaths.has(filePath)) {
|
||||||
|
this.filePaths.add(filePath)
|
||||||
|
this.workspaceDidUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onWorkspaceFoldersChanged(event: vscode.WorkspaceFoldersChangeEvent) {
|
||||||
|
for (const folder of event.added) {
|
||||||
|
const [files, _] = await listFiles(folder.uri.fsPath, true, 50)
|
||||||
|
files.forEach((file) => this.filePaths.add(file))
|
||||||
|
}
|
||||||
|
for (const folder of event.removed) {
|
||||||
|
this.filePaths.forEach((filePath) => {
|
||||||
|
if (filePath.startsWith(folder.uri.fsPath)) {
|
||||||
|
this.filePaths.delete(filePath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.workspaceDidUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private workspaceDidUpdate() {
|
||||||
|
console.log("Workspace updated. Current file paths:", Array.from(this.filePaths))
|
||||||
|
// Add your logic here for when the workspace is updated
|
||||||
|
this.providerRef.deref()?.postMessageToWebview({
|
||||||
|
type: "workspaceUpdated",
|
||||||
|
filePaths: Array.from(this.filePaths),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFilePaths(): string[] {
|
||||||
|
return Array.from(this.filePaths)
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
this.disposables.forEach((d) => d.dispose())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WorkspaceTracker
|
||||||
@@ -4,8 +4,6 @@ import os from "os"
|
|||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser"
|
import { LanguageParser, loadRequiredLanguageParsers } from "./languageParser"
|
||||||
|
|
||||||
export const LIST_FILES_LIMIT = 200
|
|
||||||
|
|
||||||
// TODO: implement caching behavior to avoid having to keep analyzing project for new tasks.
|
// TODO: implement caching behavior to avoid having to keep analyzing project for new tasks.
|
||||||
export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Promise<string> {
|
export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Promise<string> {
|
||||||
// check if the path exists
|
// check if the path exists
|
||||||
@@ -18,7 +16,7 @@ export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all files at top level (not gitignored)
|
// Get all files at top level (not gitignored)
|
||||||
const allFiles = await listFiles(dirPath, false)
|
const [allFiles, _] = await listFiles(dirPath, false, 200)
|
||||||
|
|
||||||
let result = ""
|
let result = ""
|
||||||
|
|
||||||
@@ -55,18 +53,18 @@ export async function parseSourceCodeForDefinitionsTopLevel(dirPath: string): Pr
|
|||||||
return result ? result : "No source code definitions found."
|
return result ? result : "No source code definitions found."
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listFiles(dirPath: string, recursive: boolean): Promise<string[]> {
|
export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
|
||||||
const absolutePath = path.resolve(dirPath)
|
const absolutePath = path.resolve(dirPath)
|
||||||
// Do not allow listing files in root or home directory, which Claude tends to want to do when the user's prompt is vague.
|
// Do not allow listing files in root or home directory, which Claude tends to want to do when the user's prompt is vague.
|
||||||
const root = process.platform === "win32" ? path.parse(absolutePath).root : "/"
|
const root = process.platform === "win32" ? path.parse(absolutePath).root : "/"
|
||||||
const isRoot = absolutePath === root
|
const isRoot = absolutePath === root
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
return [root]
|
return [[root], false]
|
||||||
}
|
}
|
||||||
const homeDir = os.homedir()
|
const homeDir = os.homedir()
|
||||||
const isHomeDir = absolutePath === homeDir
|
const isHomeDir = absolutePath === homeDir
|
||||||
if (isHomeDir) {
|
if (isHomeDir) {
|
||||||
return [homeDir]
|
return [[homeDir], false]
|
||||||
}
|
}
|
||||||
|
|
||||||
const dirsToIgnore = [
|
const dirsToIgnore = [
|
||||||
@@ -98,26 +96,24 @@ export async function listFiles(dirPath: string, recursive: boolean): Promise<st
|
|||||||
onlyFiles: false, // true by default, false means it will list directories on their own too
|
onlyFiles: false, // true by default, false means it will list directories on their own too
|
||||||
}
|
}
|
||||||
// * globs all files in one dir, ** globs files in nested directories
|
// * globs all files in one dir, ** globs files in nested directories
|
||||||
const files = recursive
|
const files = recursive ? await globbyLevelByLevel(limit, options) : (await globby("*", options)).slice(0, limit)
|
||||||
? await globbyLevelByLevel(options)
|
return [files, files.length >= limit]
|
||||||
: (await globby("*", options)).slice(0, LIST_FILES_LIMIT)
|
|
||||||
return files
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// globby doesnt natively support top down level by level globbing, so we implement it ourselves
|
// globby doesnt natively support top down level by level globbing, so we implement it ourselves
|
||||||
async function globbyLevelByLevel(options?: Options) {
|
async function globbyLevelByLevel(limit: number, options?: Options) {
|
||||||
let results: string[] = []
|
let results: string[] = []
|
||||||
const globbingProcess = async () => {
|
const globbingProcess = async () => {
|
||||||
let currentLevel = 0
|
let currentLevel = 0
|
||||||
while (results.length < LIST_FILES_LIMIT) {
|
while (results.length < limit) {
|
||||||
const pattern = currentLevel === 0 ? "*" : `${"*/".repeat(currentLevel)}*`
|
const pattern = currentLevel === 0 ? "*" : `${"*/".repeat(currentLevel)}*`
|
||||||
const filesAtLevel = await globby(pattern, options)
|
const filesAtLevel = await globby(pattern, options)
|
||||||
if (filesAtLevel.length === 0) {
|
if (filesAtLevel.length === 0) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
results.push(...filesAtLevel)
|
results.push(...filesAtLevel)
|
||||||
if (results.length >= LIST_FILES_LIMIT) {
|
if (results.length >= limit) {
|
||||||
results = results.slice(0, LIST_FILES_LIMIT)
|
results = results.slice(0, limit)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
currentLevel++
|
currentLevel++
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { HistoryItem } from "../shared/HistoryItem"
|
|||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import { getTheme } from "../utils/getTheme"
|
import { getTheme } from "../utils/getTheme"
|
||||||
import { openFile, openImage } from "../utils/open-file"
|
import { openFile, openImage } from "../utils/open-file"
|
||||||
|
import WorkspaceTracker from "../integrations/WorkspaceTracker"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||||
@@ -50,11 +51,13 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
private disposables: vscode.Disposable[] = []
|
private disposables: vscode.Disposable[] = []
|
||||||
private view?: vscode.WebviewView | vscode.WebviewPanel
|
private view?: vscode.WebviewView | vscode.WebviewPanel
|
||||||
private claudeDev?: ClaudeDev
|
private claudeDev?: ClaudeDev
|
||||||
|
private workspaceTracker?: WorkspaceTracker
|
||||||
private latestAnnouncementId = "sep-14-2024" // update to some unique identifier when we add a new announcement
|
private latestAnnouncementId = "sep-14-2024" // update to some unique identifier when we add a new announcement
|
||||||
|
|
||||||
constructor(readonly context: vscode.ExtensionContext, private readonly outputChannel: vscode.OutputChannel) {
|
constructor(readonly context: vscode.ExtensionContext, private readonly outputChannel: vscode.OutputChannel) {
|
||||||
this.outputChannel.appendLine("ClaudeDevProvider instantiated")
|
this.outputChannel.appendLine("ClaudeDevProvider instantiated")
|
||||||
ClaudeDevProvider.activeInstances.add(this)
|
ClaudeDevProvider.activeInstances.add(this)
|
||||||
|
this.workspaceTracker = new WorkspaceTracker(this)
|
||||||
this.revertKodu()
|
this.revertKodu()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +101,8 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
x.dispose()
|
x.dispose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.workspaceTracker?.dispose()
|
||||||
|
this.workspaceTracker = undefined
|
||||||
this.outputChannel.appendLine("Disposed all disposables")
|
this.outputChannel.appendLine("Disposed all disposables")
|
||||||
ClaudeDevProvider.activeInstances.delete(this)
|
ClaudeDevProvider.activeInstances.delete(this)
|
||||||
}
|
}
|
||||||
@@ -306,6 +311,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
|
|||||||
await this.postStateToWebview()
|
await this.postStateToWebview()
|
||||||
const theme = await getTheme()
|
const theme = await getTheme()
|
||||||
await this.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) })
|
await this.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) })
|
||||||
|
this.workspaceTracker?.initializeFilePaths()
|
||||||
break
|
break
|
||||||
case "newTask":
|
case "newTask":
|
||||||
// Code that should run in response to the hello message command
|
// Code that should run in response to the hello message command
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { HistoryItem } from "./HistoryItem"
|
|||||||
|
|
||||||
// webview will hold state
|
// webview will hold state
|
||||||
export interface ExtensionMessage {
|
export interface ExtensionMessage {
|
||||||
type: "action" | "state" | "selectedImages" | "ollamaModels" | "theme"
|
type: "action" | "state" | "selectedImages" | "ollamaModels" | "theme" | "workspaceUpdated"
|
||||||
text?: string
|
text?: string
|
||||||
action?: "chatButtonTapped" | "settingsButtonTapped" | "historyButtonTapped" | "didBecomeVisible"
|
action?: "chatButtonTapped" | "settingsButtonTapped" | "historyButtonTapped" | "didBecomeVisible"
|
||||||
state?: ExtensionState
|
state?: ExtensionState
|
||||||
images?: string[]
|
images?: string[]
|
||||||
models?: string[]
|
models?: string[]
|
||||||
|
filePaths?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionState {
|
export interface ExtensionState {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { forwardRef, useCallback, useEffect, useRef, useState, useLayoutEffect } from "react"
|
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"
|
||||||
import DynamicTextArea from "react-textarea-autosize"
|
import DynamicTextArea from "react-textarea-autosize"
|
||||||
import { insertMention, shouldShowContextMenu, getContextMenuOptions, removeMention } from "../utils/mention-context"
|
import { useExtensionState } from "../context/ExtensionStateContext"
|
||||||
|
import { getContextMenuOptions, insertMention, removeMention, shouldShowContextMenu } from "../utils/mention-context"
|
||||||
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
|
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
|
||||||
import ContextMenu from "./ContextMenu"
|
import ContextMenu from "./ContextMenu"
|
||||||
import Thumbnails from "./Thumbnails"
|
import Thumbnails from "./Thumbnails"
|
||||||
@@ -49,6 +50,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
const [intendedCursorPosition, setIntendedCursorPosition] = useState<number | null>(null)
|
const [intendedCursorPosition, setIntendedCursorPosition] = useState<number | null>(null)
|
||||||
const contextMenuContainerRef = useRef<HTMLDivElement>(null)
|
const contextMenuContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const { filePaths } = useExtensionState()
|
||||||
|
|
||||||
|
const searchPaths = React.useMemo(() => {
|
||||||
|
return [
|
||||||
|
{ type: "problems", path: "problems" },
|
||||||
|
...filePaths.map((path) => ({
|
||||||
|
type: path.endsWith("/") ? "folder" : "file",
|
||||||
|
path: path,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
}, [filePaths])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (
|
if (
|
||||||
@@ -70,7 +83,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
|
|
||||||
const handleMentionSelect = useCallback(
|
const handleMentionSelect = useCallback(
|
||||||
(type: string, value: string) => {
|
(type: string, value: string) => {
|
||||||
if (value === "File" || value === "Folder") {
|
if (value === "file" || value === "folder") {
|
||||||
setSelectedType(type.toLowerCase())
|
setSelectedType(type.toLowerCase())
|
||||||
setSearchQuery("")
|
setSearchQuery("")
|
||||||
setSelectedMenuIndex(0)
|
setSelectedMenuIndex(0)
|
||||||
@@ -108,17 +121,18 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (showContextMenu) {
|
if (showContextMenu) {
|
||||||
// if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
// // event.preventDefault()
|
// event.preventDefault()
|
||||||
// setShowContextMenu(false)
|
setSelectedType(null)
|
||||||
// return
|
setSelectedMenuIndex(3) // File by default
|
||||||
// }
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
setSelectedMenuIndex((prevIndex) => {
|
setSelectedMenuIndex((prevIndex) => {
|
||||||
const direction = event.key === "ArrowUp" ? -1 : 1
|
const direction = event.key === "ArrowUp" ? -1 : 1
|
||||||
const options = getContextMenuOptions(searchQuery, selectedType)
|
const options = getContextMenuOptions(searchQuery, selectedType, searchPaths)
|
||||||
const optionsLength = options.length
|
const optionsLength = options.length
|
||||||
|
|
||||||
if (optionsLength === 0) return prevIndex
|
if (optionsLength === 0) return prevIndex
|
||||||
@@ -144,7 +158,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
}
|
}
|
||||||
if (event.key === "Enter" && selectedMenuIndex !== -1) {
|
if (event.key === "Enter" && selectedMenuIndex !== -1) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const selectedOption = getContextMenuOptions(searchQuery, selectedType)[selectedMenuIndex]
|
const selectedOption = getContextMenuOptions(searchQuery, selectedType, searchPaths)[
|
||||||
|
selectedMenuIndex
|
||||||
|
]
|
||||||
if (selectedOption && selectedOption.type !== "url") {
|
if (selectedOption && selectedOption.type !== "url") {
|
||||||
handleMentionSelect(selectedOption.type, selectedOption.value)
|
handleMentionSelect(selectedOption.type, selectedOption.value)
|
||||||
}
|
}
|
||||||
@@ -203,6 +219,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
cursorPosition,
|
cursorPosition,
|
||||||
setInputValue,
|
setInputValue,
|
||||||
justDeletedSpaceAfterMention,
|
justDeletedSpaceAfterMention,
|
||||||
|
searchPaths,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -361,6 +378,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||||||
selectedIndex={selectedMenuIndex}
|
selectedIndex={selectedMenuIndex}
|
||||||
setSelectedIndex={setSelectedMenuIndex}
|
setSelectedIndex={setSelectedMenuIndex}
|
||||||
selectedType={selectedType}
|
selectedType={selectedType}
|
||||||
|
searchPaths={searchPaths}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState, useRef } from "react"
|
import React, { useEffect, useRef, useState } from "react"
|
||||||
import { getContextMenuOptions } from "../utils/mention-context"
|
import { getContextMenuOptions } from "../utils/mention-context"
|
||||||
|
|
||||||
interface ContextMenuProps {
|
interface ContextMenuProps {
|
||||||
@@ -8,6 +8,7 @@ interface ContextMenuProps {
|
|||||||
selectedIndex: number
|
selectedIndex: number
|
||||||
setSelectedIndex: (index: number) => void
|
setSelectedIndex: (index: number) => void
|
||||||
selectedType: string | null
|
selectedType: string | null
|
||||||
|
searchPaths: { type: string; path: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContextMenu: React.FC<ContextMenuProps> = ({
|
const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||||
@@ -17,13 +18,16 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||||||
selectedIndex,
|
selectedIndex,
|
||||||
setSelectedIndex,
|
setSelectedIndex,
|
||||||
selectedType,
|
selectedType,
|
||||||
|
searchPaths,
|
||||||
}) => {
|
}) => {
|
||||||
const [filteredOptions, setFilteredOptions] = useState(getContextMenuOptions(searchQuery, selectedType))
|
const [filteredOptions, setFilteredOptions] = useState(
|
||||||
|
getContextMenuOptions(searchQuery, selectedType, searchPaths)
|
||||||
|
)
|
||||||
const menuRef = useRef<HTMLDivElement>(null)
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilteredOptions(getContextMenuOptions(searchQuery, selectedType))
|
setFilteredOptions(getContextMenuOptions(searchQuery, selectedType, searchPaths))
|
||||||
}, [searchQuery, selectedType])
|
}, [searchQuery, selectedType, searchPaths])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (menuRef.current) {
|
if (menuRef.current) {
|
||||||
@@ -84,23 +88,23 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
|
|||||||
onMouseEnter={() => option.type !== "url" && setSelectedIndex(index)}>
|
onMouseEnter={() => option.type !== "url" && setSelectedIndex(index)}>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<i className={`codicon codicon-${option.icon}`} style={{ marginRight: "8px" }} />
|
<i className={`codicon codicon-${option.icon}`} style={{ marginRight: "8px" }} />
|
||||||
{option.value === "File"
|
{option.value === "file"
|
||||||
? "Add File"
|
? "Add File"
|
||||||
: option.value === "Folder"
|
: option.value === "folder"
|
||||||
? "Add Folder"
|
? "Add Folder"
|
||||||
: option.value === "Problems"
|
: option.value === "problems"
|
||||||
? "Workspace Problems"
|
? "Problems"
|
||||||
: option.value === "URL"
|
: option.value === "url"
|
||||||
? "Paste URL to scrape"
|
? "Paste URL to scrape"
|
||||||
: option.value}
|
: option.value}
|
||||||
</div>
|
</div>
|
||||||
{(option.value === "File" || option.value === "Folder") && (
|
{(option.value === "file" || option.value === "folder") && (
|
||||||
<i className="codicon codicon-chevron-right" style={{ fontSize: "14px" }} />
|
<i className="codicon codicon-chevron-right" style={{ fontSize: "14px" }} />
|
||||||
)}
|
)}
|
||||||
{(option.type === "problems" ||
|
{(option.type === "problems" ||
|
||||||
((option.type === "file" || option.type === "folder") &&
|
((option.type === "file" || option.type === "folder") &&
|
||||||
option.value !== "File" &&
|
option.value !== "file" &&
|
||||||
option.value !== "Folder")) && (
|
option.value !== "folder")) && (
|
||||||
<i className="codicon codicon-add" style={{ fontSize: "14px" }} />
|
<i className="codicon codicon-add" style={{ fontSize: "14px" }} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface ExtensionStateContextType extends ExtensionState {
|
|||||||
didHydrateState: boolean
|
didHydrateState: boolean
|
||||||
showWelcome: boolean
|
showWelcome: boolean
|
||||||
theme: any
|
theme: any
|
||||||
|
filePaths: string[]
|
||||||
setApiConfiguration: (config: ApiConfiguration) => void
|
setApiConfiguration: (config: ApiConfiguration) => void
|
||||||
setCustomInstructions: (value?: string) => void
|
setCustomInstructions: (value?: string) => void
|
||||||
setAlwaysAllowReadOnly: (value: boolean) => void
|
setAlwaysAllowReadOnly: (value: boolean) => void
|
||||||
@@ -27,6 +28,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||||
const [showWelcome, setShowWelcome] = useState(false)
|
const [showWelcome, setShowWelcome] = useState(false)
|
||||||
const [theme, setTheme] = useState<any>(undefined)
|
const [theme, setTheme] = useState<any>(undefined)
|
||||||
|
const [filePaths, setFilePaths] = useState<string[]>([])
|
||||||
|
|
||||||
const handleMessage = useCallback((event: MessageEvent) => {
|
const handleMessage = useCallback((event: MessageEvent) => {
|
||||||
const message: ExtensionMessage = event.data
|
const message: ExtensionMessage = event.data
|
||||||
if (message.type === "state" && message.state) {
|
if (message.type === "state" && message.state) {
|
||||||
@@ -50,6 +53,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
if (message.type === "theme" && message.text) {
|
if (message.type === "theme" && message.text) {
|
||||||
setTheme(convertTextMateToHljs(JSON.parse(message.text)))
|
setTheme(convertTextMateToHljs(JSON.parse(message.text)))
|
||||||
}
|
}
|
||||||
|
if (message.type === "workspaceUpdated" && message.filePaths) {
|
||||||
|
setFilePaths(message.filePaths)
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEvent("message", handleMessage)
|
useEvent("message", handleMessage)
|
||||||
@@ -63,6 +69,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||||||
didHydrateState,
|
didHydrateState,
|
||||||
showWelcome,
|
showWelcome,
|
||||||
theme,
|
theme,
|
||||||
|
filePaths,
|
||||||
setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
|
setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
|
||||||
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
|
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
|
||||||
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
|
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
export const mockPaths = [
|
// export const mockPaths = [
|
||||||
{ type: "problems", path: "Problems" },
|
// { type: "problems", path: "Problems" },
|
||||||
{ type: "file", path: "/src/components/Header.tsx" },
|
// { type: "file", path: "/src/components/Header.tsx" },
|
||||||
{ type: "file", path: "/src/components/Footer.tsx" },
|
// { type: "file", path: "/src/components/Footer.tsx" },
|
||||||
{ type: "file", path: "/src/utils/helpers.ts" },
|
// { type: "file", path: "/src/utils/helpers.ts" },
|
||||||
{ type: "folder", path: "/src/components" },
|
// { type: "folder", path: "/src/components" },
|
||||||
{ type: "folder", path: "/src/utils" },
|
// { type: "folder", path: "/src/utils" },
|
||||||
{ type: "folder", path: "/public/images" },
|
// { type: "folder", path: "/public/images" },
|
||||||
{ type: "file", path: "/public/index.html" },
|
// { type: "file", path: "/public/index.html" },
|
||||||
{ type: "file", path: "/package.json" },
|
// { type: "file", path: "/package.json" },
|
||||||
{ type: "folder", path: "/node_modules" },
|
// { type: "folder", path: "/node_modules" },
|
||||||
{ type: "file", path: "/README.md" },
|
// { type: "file", path: "/README.md" },
|
||||||
]
|
// ]
|
||||||
|
|
||||||
export function insertMention(text: string, position: number, value: string): string {
|
export function insertMention(text: string, position: number, value: string): string {
|
||||||
const beforeCursor = text.slice(0, position)
|
const beforeCursor = text.slice(0, position)
|
||||||
@@ -48,39 +48,42 @@ export function removeMention(text: string, position: number): { newText: string
|
|||||||
return { newText: text, newPosition: position }
|
return { newText: text, newPosition: position }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function searchPaths(query: string): { type: string; path: string }[] {
|
// export function queryPaths(
|
||||||
const lowerQuery = query.toLowerCase()
|
// query: string,
|
||||||
return mockPaths.filter(
|
// searchPaths: { type: string; path: string }[]
|
||||||
(item) => item.path.toLowerCase().includes(lowerQuery) || item.type.toLowerCase().includes(lowerQuery)
|
// ): { type: string; path: string }[] {
|
||||||
)
|
// const lowerQuery = query.toLowerCase()
|
||||||
}
|
// return searchPaths.filter(
|
||||||
|
// (item) => item.path.toLowerCase().includes(lowerQuery) || item.type.toLowerCase().includes(lowerQuery)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
export function getContextMenuOptions(
|
export function getContextMenuOptions(
|
||||||
query: string,
|
query: string,
|
||||||
selectedType: string | null = null
|
selectedType: string | null = null,
|
||||||
|
searchPaths: { type: string; path: string }[]
|
||||||
): { type: string; value: string; icon: string }[] {
|
): { type: string; value: string; icon: string }[] {
|
||||||
if (selectedType === "file") {
|
|
||||||
return mockPaths
|
|
||||||
.filter((item) => item.type === "file")
|
|
||||||
.map((item) => ({ type: "file", value: item.path, icon: "file" }))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedType === "folder") {
|
|
||||||
return mockPaths
|
|
||||||
.filter((item) => item.type === "folder")
|
|
||||||
.map((item) => ({ type: "folder", value: item.path, icon: "folder" }))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query === "") {
|
if (query === "") {
|
||||||
|
if (selectedType === "file") {
|
||||||
|
return searchPaths
|
||||||
|
.filter((item) => item.type === "file")
|
||||||
|
.map((item) => ({ type: "file", value: item.path, icon: "file" }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedType === "folder") {
|
||||||
|
return searchPaths
|
||||||
|
.filter((item) => item.type === "folder")
|
||||||
|
.map((item) => ({ type: "folder", value: item.path, icon: "folder" }))
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
{ type: "url", value: "URL", icon: "link" },
|
{ type: "url", value: "url", icon: "link" },
|
||||||
{
|
{
|
||||||
type: "problems",
|
type: "problems",
|
||||||
value: "Problems",
|
value: "problems",
|
||||||
icon: "warning",
|
icon: "warning",
|
||||||
},
|
},
|
||||||
{ type: "folder", value: "Folder", icon: "folder" },
|
{ type: "folder", value: "folder", icon: "folder" },
|
||||||
{ type: "file", value: "File", icon: "file" },
|
{ type: "file", value: "file", icon: "file" },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +94,7 @@ export function getContextMenuOptions(
|
|||||||
return [{ type: "url", value: query, icon: "link" }]
|
return [{ type: "url", value: query, icon: "link" }]
|
||||||
} else {
|
} else {
|
||||||
// Search for files and folders
|
// Search for files and folders
|
||||||
const matchingPaths = mockPaths.filter(
|
const matchingPaths = searchPaths.filter((item) => item.path.toLowerCase().includes(lowerQuery))
|
||||||
(item) => item.path.toLowerCase().includes(lowerQuery) || item.type.toLowerCase().includes(lowerQuery)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (matchingPaths.length > 0) {
|
if (matchingPaths.length > 0) {
|
||||||
return matchingPaths.map((item) => ({
|
return matchingPaths.map((item) => ({
|
||||||
@@ -104,14 +105,14 @@ export function getContextMenuOptions(
|
|||||||
} else {
|
} else {
|
||||||
// If no matches, show all options
|
// If no matches, show all options
|
||||||
return [
|
return [
|
||||||
{ type: "url", value: "URL", icon: "link" },
|
{ type: "url", value: "url", icon: "link" },
|
||||||
{
|
{
|
||||||
type: "problems",
|
type: "problems",
|
||||||
value: "Problems",
|
value: "problems",
|
||||||
icon: "warning",
|
icon: "warning",
|
||||||
},
|
},
|
||||||
{ type: "folder", value: "Folder", icon: "folder" },
|
{ type: "folder", value: "folder", icon: "folder" },
|
||||||
{ type: "file", value: "File", icon: "file" },
|
{ type: "file", value: "file", icon: "file" },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user