Skip to content

Commit 7719672

Browse files
committed
Add line range selection functionality to files in the workspace context view for targeted code context usage
1 parent ac2f244 commit 7719672

8 files changed

Lines changed: 289 additions & 12 deletions

File tree

packages/vscode/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,11 @@
280280
"title": "Generate Commit Message with CWC",
281281
"category": "Code Web Chat",
282282
"icon": "$(sparkle)"
283+
},
284+
{
285+
"command": "codeWebChat.setRange",
286+
"title": "Set Range",
287+
"category": "Code Web Chat"
283288
}
284289
],
285290
"menus": {
@@ -367,6 +372,10 @@
367372
{
368373
"command": "codeWebChat.generateCommitMessage",
369374
"when": "false"
375+
},
376+
{
377+
"command": "codeWebChat.setRange",
378+
"when": "false"
370379
}
371380
],
372381
"view/title": [
@@ -494,6 +503,11 @@
494503
"when": "view == codeWebChatViewOpenEditors && viewItem == openEditor",
495504
"group": "1_modification@3"
496505
},
506+
{
507+
"command": "codeWebChat.setRange",
508+
"when": "view == codeWebChatViewOpenEditors && viewItem == openEditor",
509+
"group": "1_modification@4"
510+
},
497511
{
498512
"command": "codeWebChat.newFile",
499513
"when": "view == codeWebChatViewWorkspace && viewItem == directory",
@@ -518,6 +532,11 @@
518532
"command": "codeWebChat.referenceInPrompt",
519533
"when": "view == codeWebChatViewWorkspace || view == codeWebChatViewContext",
520534
"group": "1_modification@3"
535+
},
536+
{
537+
"command": "codeWebChat.setRange",
538+
"when": "(view == codeWebChatViewWorkspace || view == codeWebChatViewContext) && viewItem == file",
539+
"group": "1_modification@4"
521540
}
522541
]
523542
},

packages/vscode/src/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ export * from './save-all-command'
1616
export * from './save-context-command'
1717
export * from './apply-context-command'
1818
export * from './find-paths-in-clipboard-command'
19+
export * from './set-range-command'
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as vscode from 'vscode'
2+
import * as path from 'path'
3+
import * as fs from 'fs/promises'
4+
import { FileItem } from '../context/providers/workspace-provider'
5+
6+
export const set_range_command = () => {
7+
return vscode.commands.registerCommand(
8+
'codeWebChat.setRange',
9+
async (item: FileItem) => {
10+
if (!item || !item.resourceUri) {
11+
return
12+
}
13+
const file_path = item.resourceUri.fsPath
14+
const workspace_folder = vscode.workspace.getWorkspaceFolder(
15+
item.resourceUri
16+
)
17+
18+
if (!workspace_folder) {
19+
vscode.window.showErrorMessage('File is not in a workspace folder.')
20+
return
21+
}
22+
23+
const ranges_file_path = path.join(
24+
workspace_folder.uri.fsPath,
25+
'.vscode',
26+
'ranges.json'
27+
)
28+
let ranges: Record<string, string> = {}
29+
try {
30+
const content = await fs.readFile(ranges_file_path, 'utf-8')
31+
ranges = JSON.parse(content)
32+
} catch (error) {
33+
// File might not exist, which is fine
34+
}
35+
36+
const relative_path = path.relative(
37+
workspace_folder.uri.fsPath,
38+
file_path
39+
)
40+
const current_range = ranges[relative_path]
41+
42+
const new_range = await vscode.window.showInputBox({
43+
prompt: `Set range of lines for ${path.basename(file_path)}`,
44+
title: 'Range',
45+
placeHolder: 'e.g., 100-300 400- -500 or empty to clear',
46+
value: current_range,
47+
validateInput: (value) => {
48+
if (value.trim() == '') return null
49+
50+
const parts = value.trim().split(/\s+/)
51+
const parsed_ranges: {
52+
start: number
53+
end: number
54+
original: string
55+
}[] = []
56+
57+
for (const part of parts) {
58+
const match = part.match(/^(\d+)?-(\d+)?$/)
59+
if (!match) {
60+
return `Invalid format in "${part}". Use formats like 100-300, 100-, -300.`
61+
}
62+
63+
const [, start_str, end_str] = match
64+
65+
if (!start_str && !end_str) {
66+
return 'Invalid range "-". Specify at least a start or an end line.'
67+
}
68+
69+
const start = start_str ? parseInt(start_str, 10) : null
70+
const end = end_str ? parseInt(end_str, 10) : null
71+
72+
if (start !== null && start < 1) {
73+
return `Start line must be 1 or greater in "${part}".`
74+
}
75+
76+
if (end !== null && end < 1) {
77+
return `End line must be 1 or greater in "${part}".`
78+
}
79+
80+
if (start !== null && end !== null) {
81+
if (start > end) {
82+
return `Start line cannot be greater than end line in "${part}".`
83+
}
84+
if (start == end) {
85+
return `Start and end lines cannot be the same in "${part}".`
86+
}
87+
}
88+
parsed_ranges.push({
89+
start: start ?? 1,
90+
end: end ?? Number.MAX_SAFE_INTEGER,
91+
original: part
92+
})
93+
}
94+
parsed_ranges.sort((a, b) => a.start - b.start)
95+
96+
for (let i = 0; i < parsed_ranges.length - 1; i++) {
97+
if (parsed_ranges[i].end >= parsed_ranges[i + 1].start) {
98+
return `Ranges cannot overlap: "${
99+
parsed_ranges[i].original
100+
}" and "${parsed_ranges[i + 1].original}".`
101+
}
102+
}
103+
return null
104+
}
105+
})
106+
107+
if (new_range === undefined) {
108+
return
109+
}
110+
111+
if (new_range) {
112+
ranges[relative_path] = new_range.trim().split(/\s+/).join(' ')
113+
} else {
114+
delete ranges[relative_path]
115+
}
116+
117+
try {
118+
await fs.mkdir(path.dirname(ranges_file_path), { recursive: true })
119+
await fs.writeFile(ranges_file_path, JSON.stringify(ranges, null, 2))
120+
} catch (error: any) {
121+
vscode.window.showErrorMessage(`Failed to save range: ${error.message}`)
122+
}
123+
}
124+
)
125+
}

packages/vscode/src/context/context-initialization.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,15 @@ export const context_initialization = async (
268268
const file_uri = vscode.Uri.file(file_path)
269269
const content_uint8_array =
270270
await vscode.workspace.fs.readFile(file_uri)
271-
const content = new TextDecoder().decode(content_uint8_array)
271+
let content = new TextDecoder().decode(content_uint8_array)
272+
273+
const range = workspace_provider.get_range(file_path)
274+
if (range) {
275+
content = workspace_provider.apply_range_to_content(
276+
content,
277+
range
278+
)
279+
}
272280

273281
let display_path: string
274282
const workspace_folder =

packages/vscode/src/context/providers/open-editors-provider.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class OpenEditorsProvider
2121
private _file_token_counts: Map<string, number> = new Map()
2222
private _tab_change_handler: vscode.Disposable
2323
private _file_change_watcher: vscode.Disposable
24+
private _workspace_change_handler: vscode.Disposable
2425
private _initialized: boolean = false
2526
private _opened_from_workspace_view: Set<string> = new Set()
2627
private _non_preview_files: Set<string> = new Set()
@@ -39,6 +40,9 @@ export class OpenEditorsProvider
3940
this._shared_state = SharedFileState.get_instance()
4041
this.workspace_provider = workspace_provider
4142

43+
this._workspace_change_handler =
44+
this.workspace_provider.onDidChangeTreeData(() => this.refresh())
45+
4246
this._update_preview_tabs_state()
4347

4448
this._tab_change_handler = vscode.window.tabGroups.onDidChangeTabs((e) => {
@@ -147,6 +151,7 @@ export class OpenEditorsProvider
147151
}
148152

149153
dispose(): void {
154+
this._workspace_change_handler.dispose()
150155
this._tab_change_handler.dispose()
151156
this._file_change_watcher.dispose()
152157
this._config_change_handler.dispose()
@@ -196,15 +201,28 @@ export class OpenEditorsProvider
196201

197202
const token_count = element.tokenCount
198203

204+
let final_description = element.description || '' // relative path
205+
206+
const prefix_parts: string[] = []
199207
if (token_count !== undefined) {
200-
const formatted_token_count = display_token_count(token_count)
201-
if (element.description) {
202-
element.description = `${formatted_token_count} · ${element.description}`
208+
prefix_parts.push(display_token_count(token_count))
209+
}
210+
if (element.range) {
211+
prefix_parts.push(element.range)
212+
}
213+
214+
const prefix = prefix_parts.join(' · ')
215+
216+
if (prefix) {
217+
if (final_description) {
218+
final_description = `${prefix} · ${final_description}`
203219
} else {
204-
element.description = formatted_token_count
220+
final_description = prefix
205221
}
206222
}
207223

224+
element.description = final_description
225+
208226
return element
209227
}
210228

@@ -283,6 +301,7 @@ export class OpenEditorsProvider
283301
description = path.basename(workspace_root)
284302
}
285303

304+
const range = this.workspace_provider.get_range(file_path)
286305
const token_count =
287306
await this.workspace_provider.calculate_file_tokens(file_path)
288307

@@ -296,7 +315,9 @@ export class OpenEditorsProvider
296315
true,
297316
token_count,
298317
undefined, // selectedTokenCount is undefined for open editor files
299-
description
318+
description,
319+
false,
320+
range
300321
)
301322

302323
items.push(item)

0 commit comments

Comments
 (0)