Skip to content

Commit 94b831c

Browse files
authored
Merge pull request RooCodeInc#714 from RooVetGit/opened_tabs_and_selection_mentions
Mention shortcuts to open tabs
2 parents a568577 + 70ad037 commit 94b831c

File tree

13 files changed

+117
-222
lines changed

13 files changed

+117
-222
lines changed

.changeset/blue-masks-camp.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Add shortcuts to the currently open tabs in the "Add File" section of @-mentions (thanks @olup!)

src/__mocks__/vscode.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,25 @@ const vscode = {
55
createTextEditorDecorationType: jest.fn().mockReturnValue({
66
dispose: jest.fn(),
77
}),
8+
tabGroups: {
9+
onDidChangeTabs: jest.fn(() => {
10+
return {
11+
dispose: jest.fn(),
12+
}
13+
}),
14+
all: [],
15+
},
816
},
917
workspace: {
1018
onDidSaveTextDocument: jest.fn(),
19+
createFileSystemWatcher: jest.fn().mockReturnValue({
20+
onDidCreate: jest.fn().mockReturnValue({ dispose: jest.fn() }),
21+
onDidDelete: jest.fn().mockReturnValue({ dispose: jest.fn() }),
22+
dispose: jest.fn(),
23+
}),
24+
fs: {
25+
stat: jest.fn(),
26+
},
1127
},
1228
Disposable: class {
1329
dispose() {}
@@ -57,6 +73,17 @@ const vscode = {
5773
Development: 2,
5874
Test: 3,
5975
},
76+
FileType: {
77+
Unknown: 0,
78+
File: 1,
79+
Directory: 2,
80+
SymbolicLink: 64,
81+
},
82+
TabInputText: class {
83+
constructor(uri) {
84+
this.uri = uri
85+
}
86+
},
6087
}
6188

6289
module.exports = vscode

src/core/__tests__/Cline.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ jest.mock("vscode", () => {
128128
visibleTextEditors: [mockTextEditor],
129129
tabGroups: {
130130
all: [mockTabGroup],
131+
onDidChangeTabs: jest.fn(() => ({ dispose: jest.fn() })),
131132
},
132133
},
133134
workspace: {

src/integrations/workspace/WorkspaceTracker.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as vscode from "vscode"
22
import * as path from "path"
33
import { listFiles } from "../../services/glob/list-files"
44
import { ClineProvider } from "../../core/webview/ClineProvider"
5+
import { toRelativePath } from "../../utils/path"
56

67
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
78
const MAX_INITIAL_FILES = 1_000
@@ -48,6 +49,23 @@ class WorkspaceTracker {
4849
)
4950

5051
this.disposables.push(watcher)
52+
53+
this.disposables.push(vscode.window.tabGroups.onDidChangeTabs(() => this.workspaceDidUpdate()))
54+
}
55+
56+
private getOpenedTabsInfo() {
57+
return vscode.window.tabGroups.all.flatMap((group) =>
58+
group.tabs
59+
.filter((tab) => tab.input instanceof vscode.TabInputText)
60+
.map((tab) => {
61+
const path = (tab.input as vscode.TabInputText).uri.fsPath
62+
return {
63+
label: tab.label,
64+
isActive: tab.isActive,
65+
path: toRelativePath(path, cwd || ""),
66+
}
67+
}),
68+
)
5169
}
5270

5371
private workspaceDidUpdate() {
@@ -59,12 +77,12 @@ class WorkspaceTracker {
5977
if (!cwd) {
6078
return
6179
}
80+
81+
const relativeFilePaths = Array.from(this.filePaths).map((file) => toRelativePath(file, cwd))
6282
this.providerRef.deref()?.postMessageToWebview({
6383
type: "workspaceUpdated",
64-
filePaths: Array.from(this.filePaths).map((file) => {
65-
const relativePath = path.relative(cwd, file).toPosix()
66-
return file.endsWith("/") ? relativePath + "/" : relativePath
67-
}),
84+
filePaths: relativeFilePaths,
85+
openedTabs: this.getOpenedTabsInfo(),
6886
})
6987
this.updateTimer = null
7088
}, 300) // Debounce for 300ms

src/integrations/workspace/__tests__/WorkspaceTracker.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ const mockWatcher = {
1616
}
1717

1818
jest.mock("vscode", () => ({
19+
window: {
20+
tabGroups: {
21+
onDidChangeTabs: jest.fn(() => ({ dispose: jest.fn() })),
22+
all: [],
23+
},
24+
},
1925
workspace: {
2026
workspaceFolders: [
2127
{
@@ -61,6 +67,7 @@ describe("WorkspaceTracker", () => {
6167
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
6268
type: "workspaceUpdated",
6369
filePaths: expect.arrayContaining(["file1.ts", "file2.ts"]),
70+
openedTabs: [],
6471
})
6572
expect((mockProvider.postMessageToWebview as jest.Mock).mock.calls[0][0].filePaths).toHaveLength(2)
6673
})
@@ -74,6 +81,7 @@ describe("WorkspaceTracker", () => {
7481
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
7582
type: "workspaceUpdated",
7683
filePaths: ["newfile.ts"],
84+
openedTabs: [],
7785
})
7886
})
7987

@@ -92,6 +100,7 @@ describe("WorkspaceTracker", () => {
92100
expect(mockProvider.postMessageToWebview).toHaveBeenLastCalledWith({
93101
type: "workspaceUpdated",
94102
filePaths: [],
103+
openedTabs: [],
95104
})
96105
})
97106

@@ -106,6 +115,7 @@ describe("WorkspaceTracker", () => {
106115
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
107116
type: "workspaceUpdated",
108117
filePaths: expect.arrayContaining(["newdir"]),
118+
openedTabs: [],
109119
})
110120
const lastCall = (mockProvider.postMessageToWebview as jest.Mock).mock.calls.slice(-1)[0]
111121
expect(lastCall[0].filePaths).toHaveLength(1)
@@ -126,6 +136,7 @@ describe("WorkspaceTracker", () => {
126136
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
127137
type: "workspaceUpdated",
128138
filePaths: expect.arrayContaining(expectedFiles),
139+
openedTabs: [],
129140
})
130141
expect(calls[0][0].filePaths).toHaveLength(1000)
131142

src/shared/ExtensionMessage.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ export interface ExtensionMessage {
5757
lmStudioModels?: string[]
5858
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
5959
filePaths?: string[]
60+
openedTabs?: Array<{
61+
label: string
62+
isActive: boolean
63+
path?: string
64+
}>
6065
partialMessage?: ClineMessage
6166
glamaModels?: Record<string, ModelInfo>
6267
openRouterModels?: Record<string, ModelInfo>

src/utils/path.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,8 @@ export function getReadablePath(cwd: string, relPath?: string): string {
9999
}
100100
}
101101
}
102+
103+
export const toRelativePath = (filePath: string, cwd: string) => {
104+
const relativePath = path.relative(cwd, filePath).toPosix()
105+
return filePath.endsWith("/") ? relativePath + "/" : relativePath
106+
}

webview-ui/src/components/chat/ChatTextArea.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
5050
},
5151
ref,
5252
) => {
53-
const { filePaths, currentApiConfigName, listApiConfigMeta, customModes } = useExtensionState()
53+
const { filePaths, openedTabs, currentApiConfigName, listApiConfigMeta, customModes } = useExtensionState()
5454
const [gitCommits, setGitCommits] = useState<any[]>([])
5555
const [showDropdown, setShowDropdown] = useState(false)
5656

@@ -138,14 +138,21 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
138138
return [
139139
{ type: ContextMenuOptionType.Problems, value: "problems" },
140140
...gitCommits,
141+
...openedTabs
142+
.filter((tab) => tab.path)
143+
.map((tab) => ({
144+
type: ContextMenuOptionType.OpenedFile,
145+
value: "/" + tab.path,
146+
})),
141147
...filePaths
142148
.map((file) => "/" + file)
149+
.filter((path) => !openedTabs.some((tab) => tab.path && "/" + tab.path === path)) // Filter out paths that are already in openedTabs
143150
.map((path) => ({
144151
type: path.endsWith("/") ? ContextMenuOptionType.Folder : ContextMenuOptionType.File,
145152
value: path,
146153
})),
147154
]
148-
}, [filePaths, gitCommits])
155+
}, [filePaths, gitCommits, openedTabs])
149156

150157
useEffect(() => {
151158
const handleClickOutside = (event: MouseEvent) => {

webview-ui/src/components/chat/ContextMenu.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
7474
return <span>Git Commits</span>
7575
}
7676
case ContextMenuOptionType.File:
77+
case ContextMenuOptionType.OpenedFile:
7778
case ContextMenuOptionType.Folder:
7879
if (option.value) {
7980
return (
@@ -100,6 +101,8 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
100101

101102
const getIconForOption = (option: ContextMenuQueryItem): string => {
102103
switch (option.type) {
104+
case ContextMenuOptionType.OpenedFile:
105+
return "window"
103106
case ContextMenuOptionType.File:
104107
return "file"
105108
case ContextMenuOptionType.Folder:
@@ -194,6 +197,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({
194197
{(option.type === ContextMenuOptionType.Problems ||
195198
((option.type === ContextMenuOptionType.File ||
196199
option.type === ContextMenuOptionType.Folder ||
200+
option.type === ContextMenuOptionType.OpenedFile ||
197201
option.type === ContextMenuOptionType.Git) &&
198202
option.value)) && (
199203
<i

0 commit comments

Comments
 (0)