mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
Support multiple files in drag-and-drop
This commit is contained in:
parent
8faac4f734
commit
a4e58700ce
3 changed files with 272 additions and 8 deletions
5
.changeset/orange-zoos-train.md
Normal file
5
.changeset/orange-zoos-train.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Support multiple files in drag-and-drop
|
||||
|
|
@ -590,15 +590,36 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
const files = Array.from(e.dataTransfer.files)
|
||||
const text = e.dataTransfer.getData("text")
|
||||
if (text) {
|
||||
// Convert the path to a mention-friendly format
|
||||
const mentionText = convertToMentionPath(text, cwd)
|
||||
// Split text on newlines to handle multiple files
|
||||
const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "")
|
||||
|
||||
const newValue =
|
||||
inputValue.slice(0, cursorPosition) + mentionText + " " + inputValue.slice(cursorPosition)
|
||||
setInputValue(newValue)
|
||||
const newCursorPosition = cursorPosition + mentionText.length + 1
|
||||
setCursorPosition(newCursorPosition)
|
||||
setIntendedCursorPosition(newCursorPosition)
|
||||
if (lines.length > 0) {
|
||||
// Process each line as a separate file path
|
||||
let newValue = inputValue.slice(0, cursorPosition)
|
||||
let totalLength = 0
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
// Convert each path to a mention-friendly format
|
||||
const mentionText = convertToMentionPath(line, cwd)
|
||||
newValue += mentionText
|
||||
totalLength += mentionText.length
|
||||
|
||||
// Add space after each mention except the last one
|
||||
if (index < lines.length - 1) {
|
||||
newValue += " "
|
||||
totalLength += 1
|
||||
}
|
||||
})
|
||||
|
||||
// Add space after the last mention and append the rest of the input
|
||||
newValue += " " + inputValue.slice(cursorPosition)
|
||||
totalLength += 1
|
||||
|
||||
setInputValue(newValue)
|
||||
const newCursorPosition = cursorPosition + totalLength
|
||||
setCursorPosition(newCursorPosition)
|
||||
setIntendedCursorPosition(newCursorPosition)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import ChatTextArea from "../ChatTextArea"
|
|||
import { useExtensionState } from "../../../context/ExtensionStateContext"
|
||||
import { vscode } from "../../../utils/vscode"
|
||||
import { defaultModeSlug } from "../../../../../src/shared/modes"
|
||||
import * as pathMentions from "../../../utils/path-mentions"
|
||||
|
||||
// Mock modules
|
||||
jest.mock("../../../utils/vscode", () => ({
|
||||
|
|
@ -12,9 +13,20 @@ jest.mock("../../../utils/vscode", () => ({
|
|||
}))
|
||||
jest.mock("../../../components/common/CodeBlock")
|
||||
jest.mock("../../../components/common/MarkdownBlock")
|
||||
jest.mock("../../../utils/path-mentions", () => ({
|
||||
convertToMentionPath: jest.fn((path, cwd) => {
|
||||
// Simple mock implementation that mimics the real function's behavior
|
||||
if (cwd && path.toLowerCase().startsWith(cwd.toLowerCase())) {
|
||||
const relativePath = path.substring(cwd.length)
|
||||
return "@" + (relativePath.startsWith("/") ? relativePath : "/" + relativePath)
|
||||
}
|
||||
return path
|
||||
}),
|
||||
}))
|
||||
|
||||
// Get the mocked postMessage function
|
||||
const mockPostMessage = vscode.postMessage as jest.Mock
|
||||
const mockConvertToMentionPath = pathMentions.convertToMentionPath as jest.Mock
|
||||
|
||||
// Mock ExtensionStateContext
|
||||
jest.mock("../../../context/ExtensionStateContext")
|
||||
|
|
@ -160,4 +172,230 @@ describe("ChatTextArea", () => {
|
|||
expect(setInputValue).toHaveBeenCalledWith("Enhanced test prompt")
|
||||
})
|
||||
})
|
||||
|
||||
describe("multi-file drag and drop", () => {
|
||||
const mockCwd = "/Users/test/project"
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(useExtensionState as jest.Mock).mockReturnValue({
|
||||
filePaths: [],
|
||||
openedTabs: [],
|
||||
cwd: mockCwd,
|
||||
})
|
||||
mockConvertToMentionPath.mockClear()
|
||||
})
|
||||
|
||||
it("should process multiple file paths separated by newlines", () => {
|
||||
const setInputValue = jest.fn()
|
||||
|
||||
const { container } = render(
|
||||
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Initial text" />,
|
||||
)
|
||||
|
||||
// Create a mock dataTransfer object with text data containing multiple file paths
|
||||
const dataTransfer = {
|
||||
getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n/Users/test/project/file2.js"),
|
||||
files: [],
|
||||
}
|
||||
|
||||
// Simulate drop event
|
||||
fireEvent.drop(container.querySelector(".chat-text-area")!, {
|
||||
dataTransfer,
|
||||
preventDefault: jest.fn(),
|
||||
})
|
||||
|
||||
// Verify convertToMentionPath was called for each file path
|
||||
expect(mockConvertToMentionPath).toHaveBeenCalledTimes(2)
|
||||
expect(mockConvertToMentionPath).toHaveBeenCalledWith("/Users/test/project/file1.js", mockCwd)
|
||||
expect(mockConvertToMentionPath).toHaveBeenCalledWith("/Users/test/project/file2.js", mockCwd)
|
||||
|
||||
// Verify setInputValue was called with the correct value
|
||||
// The mock implementation of convertToMentionPath will convert the paths to @/file1.js and @/file2.js
|
||||
expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Initial text")
|
||||
})
|
||||
|
||||
it("should filter out empty lines in the dragged text", () => {
|
||||
const setInputValue = jest.fn()
|
||||
|
||||
const { container } = render(
|
||||
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Initial text" />,
|
||||
)
|
||||
|
||||
// Create a mock dataTransfer object with text data containing empty lines
|
||||
const dataTransfer = {
|
||||
getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n\n/Users/test/project/file2.js\n\n"),
|
||||
files: [],
|
||||
}
|
||||
|
||||
// Simulate drop event
|
||||
fireEvent.drop(container.querySelector(".chat-text-area")!, {
|
||||
dataTransfer,
|
||||
preventDefault: jest.fn(),
|
||||
})
|
||||
|
||||
// Verify convertToMentionPath was called only for non-empty lines
|
||||
expect(mockConvertToMentionPath).toHaveBeenCalledTimes(2)
|
||||
|
||||
// Verify setInputValue was called with the correct value
|
||||
expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Initial text")
|
||||
})
|
||||
|
||||
it("should correctly update cursor position after adding multiple mentions", () => {
|
||||
const setInputValue = jest.fn()
|
||||
const initialCursorPosition = 5
|
||||
|
||||
const { container } = render(
|
||||
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Hello world" />,
|
||||
)
|
||||
|
||||
// Set the cursor position manually
|
||||
const textArea = container.querySelector("textarea")
|
||||
if (textArea) {
|
||||
textArea.selectionStart = initialCursorPosition
|
||||
textArea.selectionEnd = initialCursorPosition
|
||||
}
|
||||
|
||||
// Create a mock dataTransfer object with text data
|
||||
const dataTransfer = {
|
||||
getData: jest.fn().mockReturnValue("/Users/test/project/file1.js\n/Users/test/project/file2.js"),
|
||||
files: [],
|
||||
}
|
||||
|
||||
// Simulate drop event
|
||||
fireEvent.drop(container.querySelector(".chat-text-area")!, {
|
||||
dataTransfer,
|
||||
preventDefault: jest.fn(),
|
||||
})
|
||||
|
||||
// The cursor position should be updated based on the implementation in the component
|
||||
expect(setInputValue).toHaveBeenCalledWith("@/file1.js @/file2.js Hello world")
|
||||
})
|
||||
|
||||
it("should handle very long file paths correctly", () => {
|
||||
const setInputValue = jest.fn()
|
||||
|
||||
const { container } = render(<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />)
|
||||
|
||||
// Create a very long file path
|
||||
const longPath =
|
||||
"/Users/test/project/very/long/path/with/many/nested/directories/and/a/very/long/filename/with/extension.typescript"
|
||||
|
||||
// Create a mock dataTransfer object with the long path
|
||||
const dataTransfer = {
|
||||
getData: jest.fn().mockReturnValue(longPath),
|
||||
files: [],
|
||||
}
|
||||
|
||||
// Simulate drop event
|
||||
fireEvent.drop(container.querySelector(".chat-text-area")!, {
|
||||
dataTransfer,
|
||||
preventDefault: jest.fn(),
|
||||
})
|
||||
|
||||
// Verify convertToMentionPath was called with the long path
|
||||
expect(mockConvertToMentionPath).toHaveBeenCalledWith(longPath, mockCwd)
|
||||
|
||||
// The mock implementation will convert it to @/very/long/path/...
|
||||
expect(setInputValue).toHaveBeenCalledWith(
|
||||
"@/very/long/path/with/many/nested/directories/and/a/very/long/filename/with/extension.typescript ",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle paths with special characters correctly", () => {
|
||||
const setInputValue = jest.fn()
|
||||
|
||||
const { container } = render(<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />)
|
||||
|
||||
// Create paths with special characters
|
||||
const specialPath1 = "/Users/test/project/file with spaces.js"
|
||||
const specialPath2 = "/Users/test/project/file-with-dashes.js"
|
||||
const specialPath3 = "/Users/test/project/file_with_underscores.js"
|
||||
const specialPath4 = "/Users/test/project/file.with.dots.js"
|
||||
|
||||
// Create a mock dataTransfer object with the special paths
|
||||
const dataTransfer = {
|
||||
getData: jest
|
||||
.fn()
|
||||
.mockReturnValue(`${specialPath1}\n${specialPath2}\n${specialPath3}\n${specialPath4}`),
|
||||
files: [],
|
||||
}
|
||||
|
||||
// Simulate drop event
|
||||
fireEvent.drop(container.querySelector(".chat-text-area")!, {
|
||||
dataTransfer,
|
||||
preventDefault: jest.fn(),
|
||||
})
|
||||
|
||||
// Verify convertToMentionPath was called for each path
|
||||
expect(mockConvertToMentionPath).toHaveBeenCalledTimes(4)
|
||||
expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath1, mockCwd)
|
||||
expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath2, mockCwd)
|
||||
expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath3, mockCwd)
|
||||
expect(mockConvertToMentionPath).toHaveBeenCalledWith(specialPath4, mockCwd)
|
||||
|
||||
// Verify setInputValue was called with the correct value
|
||||
expect(setInputValue).toHaveBeenCalledWith(
|
||||
"@/file with spaces.js @/file-with-dashes.js @/file_with_underscores.js @/file.with.dots.js ",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle paths outside the current working directory", () => {
|
||||
const setInputValue = jest.fn()
|
||||
|
||||
const { container } = render(<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="" />)
|
||||
|
||||
// Create paths outside the current working directory
|
||||
const outsidePath = "/Users/other/project/file.js"
|
||||
|
||||
// Mock the convertToMentionPath function to return the original path for paths outside cwd
|
||||
mockConvertToMentionPath.mockImplementationOnce((path, cwd) => {
|
||||
return path // Return original path for this test
|
||||
})
|
||||
|
||||
// Create a mock dataTransfer object with the outside path
|
||||
const dataTransfer = {
|
||||
getData: jest.fn().mockReturnValue(outsidePath),
|
||||
files: [],
|
||||
}
|
||||
|
||||
// Simulate drop event
|
||||
fireEvent.drop(container.querySelector(".chat-text-area")!, {
|
||||
dataTransfer,
|
||||
preventDefault: jest.fn(),
|
||||
})
|
||||
|
||||
// Verify convertToMentionPath was called with the outside path
|
||||
expect(mockConvertToMentionPath).toHaveBeenCalledWith(outsidePath, mockCwd)
|
||||
|
||||
// Verify setInputValue was called with the original path
|
||||
expect(setInputValue).toHaveBeenCalledWith("/Users/other/project/file.js ")
|
||||
})
|
||||
|
||||
it("should do nothing when dropped text is empty", () => {
|
||||
const setInputValue = jest.fn()
|
||||
|
||||
const { container } = render(
|
||||
<ChatTextArea {...defaultProps} setInputValue={setInputValue} inputValue="Initial text" />,
|
||||
)
|
||||
|
||||
// Create a mock dataTransfer object with empty text
|
||||
const dataTransfer = {
|
||||
getData: jest.fn().mockReturnValue(""),
|
||||
files: [],
|
||||
}
|
||||
|
||||
// Simulate drop event
|
||||
fireEvent.drop(container.querySelector(".chat-text-area")!, {
|
||||
dataTransfer,
|
||||
preventDefault: jest.fn(),
|
||||
})
|
||||
|
||||
// Verify convertToMentionPath was not called
|
||||
expect(mockConvertToMentionPath).not.toHaveBeenCalled()
|
||||
|
||||
// Verify setInputValue was not called
|
||||
expect(setInputValue).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue