Skip to content

Commit eae3f1b

Browse files
committed
Migrate fragment symbols from HTML tags to hash symbol syntax
- 'fragment' symbol should not include the actual text, just caret coordinates, so it is unviersal and replaced only later on, also it should not be XML - fragment is replaced by "Error reading file ..." - drop double quotes from 'fragment' symbol - rename symbol "Fragment (5 lines)" to "Pasted 5 lines" - don't show title 'Lines X:Y' attribute ├── apps │ └── editor │ └── src │ ├── context │ │ └── providers │ │ └── workspace │ │ └── workspace-provider.ts │ └── views │ └── prompt │ └── backend │ ├── message-handlers │ │ ├── handle-autofill.ts │ │ ├── handle-copy-prompt.ts │ │ ├── handle-make-api-call.ts │ │ ├── handle-preview-prompt.ts │ │ ├── handle-preview-web-configuration.ts │ │ └── utils │ │ └── build-prompt-payload.ts │ ├── prompt-view-provider.ts │ └── utils │ └── symbols │ ├── clipboard-paths │ │ └── replace-clipboard-paths-symbol.ts │ ├── fragment │ │ └── replace-fragment-symbol.ts │ ├── git │ │ └── replace-git-symbols.ts │ ├── image │ │ └── replace-image-symbol.ts │ ├── pasted-text │ │ └── replace-pasted-text-symbol.ts │ ├── replace-symbols.ts │ ├── saved-context │ │ └── replace-saved-context-symbol.ts │ ├── selection │ │ └── replace-selection-symbol.ts │ ├── skill │ │ └── replace-skill-symbol.ts │ ├── symbol-cache.ts │ └── website │ └── replace-website-symbol.ts └── packages └── ui └── src └── components └── editor └── common └── prompts ├── PromptField │ ├── PromptField.module.scss │ ├── PromptField.stories.tsx │ ├── PromptField.tsx │ ├── hooks │ │ ├── use-drag-drop.ts │ │ ├── use-dropdown.ts │ │ ├── use-ghost-text.ts │ │ ├── use-handlers │ │ │ ├── handle-copy.ts │ │ │ ├── handle-cut.ts │ │ │ ├── handle-input-change.ts │ │ │ ├── handle-input-click.ts │ │ │ ├── handle-key-down.ts │ │ │ ├── handle-paste.ts │ │ │ ├── handle-submit.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── use-handlers.ts │ │ │ └── utils.ts │ │ └── use-keyboard-shortcuts.ts │ └── index.ts ├── Template │ ├── Template.module.scss │ ├── Template.tsx │ └── index.ts └── shared └── symbols ├── caret.ts ├── get-highlighted-text.tsx ├── index.ts ├── position-mapping.ts ├── prompt-symbols.module.scss ├── reconstruct-raw-value.ts └── use-symbol-deletion.ts
1 parent d596dca commit eae3f1b

9 files changed

Lines changed: 131 additions & 173 deletions

File tree

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,69 @@
1-
export const replace_fragment_symbol = (instruction: string): string => {
2-
const regex =
3-
/<fragment path="([^"]+)"(?: start="([^"]+)")?(?: end="([^"]+)")?>([\s\S]*?)<\/fragment>/g
4-
5-
return instruction.replace(regex, (_match, path, start, end, content) => {
6-
let clean_content = content
7-
8-
if (
9-
clean_content.startsWith('\n```\n') &&
10-
clean_content.endsWith('\n```\n')
11-
) {
12-
clean_content = clean_content.slice(5, -5)
13-
} else if (
14-
clean_content.startsWith('```\n') &&
15-
clean_content.endsWith('\n```')
16-
) {
17-
clean_content = clean_content.slice(4, -4)
18-
} else if (
19-
clean_content.startsWith('```') &&
20-
clean_content.endsWith('```')
21-
) {
22-
clean_content = clean_content.slice(3, -3)
23-
} else if (clean_content.startsWith('\n') && clean_content.endsWith('\n')) {
24-
clean_content = clean_content.slice(1, -1)
1+
import * as vscode from 'vscode'
2+
import * as path from 'path'
3+
import * as fs from 'fs'
4+
import { WorkspaceProvider } from '@/context/providers/workspace/workspace-provider'
5+
6+
export const replace_fragment_symbol = async (params: {
7+
instruction: string
8+
workspace_provider: WorkspaceProvider
9+
}): Promise<string> => {
10+
const regex = /#Fragment\((.+?):(\d+):(\d+)-(\d+):(\d+)\)/g
11+
12+
const matches = [...params.instruction.matchAll(regex)]
13+
if (matches.length === 0) return params.instruction
14+
15+
let result = params.instruction
16+
17+
for (const match of matches) {
18+
const full_match = match[0]
19+
const file_path = match[1]
20+
const start_line = parseInt(match[2], 10)
21+
const end_line = parseInt(match[4], 10)
22+
23+
let content = ''
24+
try {
25+
let absolute_path = file_path
26+
if (!path.isAbsolute(file_path)) {
27+
const roots = params.workspace_provider.get_workspace_roots()
28+
for (const root of roots) {
29+
const test_path = path.join(root, file_path)
30+
if (fs.existsSync(test_path)) {
31+
absolute_path = test_path
32+
break
33+
}
34+
35+
if (roots.length > 1) {
36+
const workspace_name = params.workspace_provider.get_workspace_name(root)
37+
const prefix = `${workspace_name}/`
38+
const prefix_win = `${workspace_name}\\`
39+
40+
if (file_path.startsWith(prefix) || file_path.startsWith(prefix_win)) {
41+
const stripped_test_path = path.join(root, file_path.substring(prefix.length))
42+
if (fs.existsSync(stripped_test_path)) {
43+
absolute_path = stripped_test_path
44+
break
45+
}
46+
}
47+
}
48+
}
49+
}
50+
51+
const file_uri = vscode.Uri.file(absolute_path)
52+
const document = await vscode.workspace.openTextDocument(file_uri)
53+
54+
const start_pos = new vscode.Position(start_line - 1, parseInt(match[3], 10) - 1)
55+
const end_pos = new vscode.Position(end_line - 1, parseInt(match[5], 10) - 1)
56+
const valid_range = document.validateRange(new vscode.Range(start_pos, end_pos))
57+
content = document.getText(valid_range)
58+
} catch (e) {
59+
content = `// Error reading file ${file_path}`
2560
}
2661

27-
const range_str = start && end ? ` (${start}-${end})` : ''
62+
const range_str = ` (${start_line}-${end_line})`
63+
const replacement = `\n\n\`${file_path}\`${range_str}\n\n\`\`\`\n${content}\n\`\`\`\n\n`
64+
65+
result = result.replace(full_match, replacement)
66+
}
2867

29-
return `\n\n\`${path}\`${range_str}\n\n\`\`\`\n${clean_content}\n\`\`\`\n\n`
30-
})
68+
return result
3169
}

‎apps/editor/src/views/prompt/backend/utils/symbols/replace-symbols.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,11 @@ export const replace_symbols = async (params: {
9999
})
100100
}
101101

102-
if (processed_instructions.includes('<fragment')) {
103-
processed_instructions = replace_fragment_symbol(processed_instructions)
102+
if (processed_instructions.includes('#Fragment(')) {
103+
processed_instructions = await replace_fragment_symbol({
104+
instruction: processed_instructions,
105+
workspace_provider: params.workspace_provider
106+
})
104107
}
105108

106109
return { instructions: processed_instructions, skill_definitions }

‎packages/ui/src/components/editor/common/prompts/PromptField/hooks/use-handlers/handle-input-click.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ export const create_handle_input_click = ({
4949
)
5050
if (pasted_lines_symbol_element) {
5151
const path = pasted_lines_symbol_element.dataset.path
52-
const start = pasted_lines_symbol_element.dataset.start
53-
const end = pasted_lines_symbol_element.dataset.end
54-
if (path) {
55-
props.on_pasted_lines_click(path, start, end)
52+
const start_line = pasted_lines_symbol_element.dataset.startLine
53+
const start_col = pasted_lines_symbol_element.dataset.startCol
54+
const end_line = pasted_lines_symbol_element.dataset.endLine
55+
const end_col = pasted_lines_symbol_element.dataset.endCol
56+
if (path && start_line && start_col && end_line && end_col) {
57+
props.on_pasted_lines_click(path, `${start_line}:${start_col}`, `${end_line}:${end_col}`)
5658
}
5759
}
5860

‎packages/ui/src/components/editor/common/prompts/PromptField/hooks/use-handlers/handle-paste.ts‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,7 @@ export const create_perform_paste =
4747
) {
4848
const { start_line, start_col, end_line, end_col } =
4949
props.current_selection
50-
const is_multiline = text.includes('\n')
51-
const formatted_text = is_multiline
52-
? `\n\`\`\`\n${text}\n\`\`\`\n`
53-
: `\`\`\`${text}\`\`\``
54-
text_to_insert = `<fragment path="${props.currently_open_file_path}" start="${start_line}:${start_col}" end="${end_line}:${end_col}">${formatted_text}</fragment>`
50+
text_to_insert = `#Fragment(${props.currently_open_file_path}:${start_line}:${start_col}-${end_line}:${end_col})`
5551
}
5652

5753
const new_value =

‎packages/ui/src/components/editor/common/prompts/PromptField/hooks/use-handlers/utils.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const get_symbol_ranges = (params: {
1010
}): { start: number; end: number }[] => {
1111
const ranges: { start: number; end: number }[] = []
1212
const regex =
13-
/`([^`]+)`|(#Changes\([^)]+\))|(#Selection)|(#SavedContext\((?:WorkspaceState|JSON) "(?:\\.|[^"\\])*"\))|(#(?:Commit|CommitMessage)\([^:]+:[^\s"]+ "(?:\\.|[^"\\])*"\))|(<fragment path="[^"]+"(?: [^>]+)?>[\s\S]*?<\/fragment>)|(#Skill\([^)]+\))|(#Image\([a-fA-F0-9]+\))|(#PastedText\([a-fA-F0-9]+:\d+\))|(#Website\([^)]+\))|(#ClipboardPaths)/g
13+
/`([^`]+)`|(#Changes\([^)]+\))|(#Selection)|(#SavedContext\((?:WorkspaceState|JSON) "(?:\\.|[^"\\])*"\))|(#(?:Commit|CommitMessage)\([^:]+:[^\s"]+ "(?:\\.|[^"\\])*"\))|(#Fragment\(.+?:\d+:\d+-\d+:\d+\))|(#Skill\([^)]+\))|(#Image\([a-fA-F0-9]+\))|(#PastedText\([a-fA-F0-9]+:\d+\))|(#Website\([^)]+\))|(#ClipboardPaths)/g
1414

1515
let match
1616
while ((match = regex.exec(params.text)) !== null) {

‎packages/ui/src/components/editor/common/prompts/shared/symbols/get-highlighted-text.tsx‎

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const get_highlighted_text = (params: {
6161
'#(?:Commit|CommitMessage)\\([^:]+:[^\\s"]+ "(?:\\\\.|[^"\\\\])*"\\)'
6262

6363
const fragment_regex_part =
64-
'<fragment path="[^"]+"(?: [^>]+)?>[\\s\\S]*?<\\/fragment>'
64+
'#Fragment\\(.+?:\\d+:\\d+-\\d+:\\d+\\)'
6565

6666
const skill_regex_part = '#Skill\\([^)]+\\)'
6767

@@ -81,44 +81,27 @@ export const get_highlighted_text = (params: {
8181
let result = parts
8282
.map((part) => {
8383
const fragment_match = part.match(
84-
/^<fragment path="([^"]+)"(?: start="([^"]+)")?(?: end="([^"]+)")?>([\s\S]*?)<\/fragment>$/
84+
/^#Fragment\((.+?):(\d+):(\d+)-(\d+):(\d+)\)$/
8585
)
8686
if (part && fragment_match) {
8787
const path = fragment_match[1]
88-
const start = fragment_match[2]
89-
const end = fragment_match[3]
90-
let content = fragment_match[4]
91-
92-
if (content.startsWith('\n```\n') && content.endsWith('\n```\n')) {
93-
content = content.slice(5, -5)
94-
} else if (content.startsWith('```\n') && content.endsWith('\n```')) {
95-
content = content.slice(4, -4)
96-
} else if (content.startsWith('```') && content.endsWith('```')) {
97-
content = content.slice(3, -3)
98-
} else if (content.startsWith('\n') && content.endsWith('\n')) {
99-
content = content.slice(1, -1)
100-
}
88+
const start_line = parseInt(fragment_match[2], 10)
89+
const start_col = parseInt(fragment_match[3], 10)
90+
const end_line = parseInt(fragment_match[4], 10)
91+
const end_col = parseInt(fragment_match[5], 10)
10192

102-
const line_count = content.split('\n').length
93+
const line_count = Math.max(1, end_line - start_line + 1)
10394
const lines_text = line_count == 1 ? 'line' : 'lines'
10495

10596
const is_error = !params.context_file_paths.includes(path)
10697

107-
const title = is_error
108-
? 'File of the selection is not in context'
109-
: content
110-
11198
return `<span class="${cn(
11299
styles['symbol'],
113100
styles['symbol--pasted-lines'],
114101
{ [styles['symbol--error']]: is_error }
115-
)}" data-type="pasted-lines-symbol" title="${escape_html(
116-
title
117-
)}" data-path="${escape_html(path)}" data-content="${escape_html(
118-
content
119-
)}"${start ? ` data-start="${escape_html(start)}"` : ''}${
120-
end ? ` data-end="${escape_html(end)}"` : ''
121-
}><span class="${
102+
)}" data-type="pasted-lines-symbol"${
103+
is_error ? ' title="File with selection is not selected"' : ''
104+
} data-path="${escape_html(path)}" data-start-line="${start_line}" data-start-col="${start_col}" data-end-line="${end_line}" data-end-col="${end_col}"><span class="${
122105
styles['symbol__icon']
123106
}" data-role="symbol-icon"></span><span class="${
124107
styles['symbol__text']

‎packages/ui/src/components/editor/common/prompts/shared/symbols/position-mapping.ts‎

Lines changed: 19 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const map_display_pos_to_raw_pos = (params: {
88
let last_raw_index = 0
99

1010
const regex =
11-
/`([^`]+)`|(#Changes\([^)]+\))|(#Selection)|(#SavedContext\((?:WorkspaceState|JSON) "((?:\\.|[^"\\])*)"\))|(#(?:Commit|CommitMessage)\([^:]+:([^\s"]+) "(?:\\.|[^"\\])*"\))|(<fragment path="[^"]+"(?: [^>]+)?>([\s\S]*?)<\/fragment>)|(#Skill\([^)]+\))|(#Image\([a-fA-F0-9]+\))|(#PastedText\([a-fA-F0-9]+:\d+\))|(#Website\([^)]+\))|(#ClipboardPaths)/g
11+
/`([^`]+)`|(#Changes\([^)]+\))|(#Selection)|(#SavedContext\((?:WorkspaceState|JSON) "((?:\\.|[^"\\])*)"\))|(#(?:Commit|CommitMessage)\([^:]+:([^\s"]+) "(?:\\.|[^"\\])*"\))|(#Fragment\((.+?):(\d+):(\d+)-(\d+):(\d+)\))|(#Skill\([^)]+\))|(#Image\([a-fA-F0-9]+\))|(#PastedText\([a-fA-F0-9]+:\d+\))|(#Website\([^)]+\))|(#ClipboardPaths)/g
1212
let match
1313

1414
while ((match = regex.exec(params.raw_text)) !== null) {
@@ -20,12 +20,13 @@ export const map_display_pos_to_raw_pos = (params: {
2020
const commit_symbol = match[6]
2121
const commit_hash = match[7]
2222
const fragment_symbol = match[8]
23-
let fragment_content = match[9]
24-
const skill_symbol = match[10]
25-
const image_symbol = match[11]
26-
const pasted_text_symbol = match[12]
27-
const website_symbol = match[13]
28-
const clipboard_paths_symbol = match[14]
23+
const fragment_start_line = match[10] ? parseInt(match[10], 10) : 0
24+
const fragment_end_line = match[12] ? parseInt(match[12], 10) : 0
25+
const skill_symbol = match[14]
26+
const image_symbol = match[15]
27+
const pasted_text_symbol = match[16]
28+
const website_symbol = match[17]
29+
const clipboard_paths_symbol = match[18]
2930

3031
let is_replacement_match = false
3132
let display_match_length = 0
@@ -52,28 +53,7 @@ export const map_display_pos_to_raw_pos = (params: {
5253
display_match_length = short_hash.length
5354
is_replacement_match = true
5455
} else if (fragment_symbol) {
55-
if (
56-
fragment_content.startsWith('\n```\n') &&
57-
fragment_content.endsWith('\n```\n')
58-
) {
59-
fragment_content = fragment_content.slice(5, -5)
60-
} else if (
61-
fragment_content.startsWith('```\n') &&
62-
fragment_content.endsWith('\n```')
63-
) {
64-
fragment_content = fragment_content.slice(4, -4)
65-
} else if (
66-
fragment_content.startsWith('```') &&
67-
fragment_content.endsWith('```')
68-
) {
69-
fragment_content = fragment_content.slice(3, -3)
70-
} else if (
71-
fragment_content.startsWith('\n') &&
72-
fragment_content.endsWith('\n')
73-
) {
74-
fragment_content = fragment_content.slice(1, -1)
75-
}
76-
const line_count = fragment_content.split('\n').length
56+
const line_count = Math.max(1, fragment_end_line - fragment_start_line + 1)
7757
const lines_text = line_count === 1 ? 'line' : 'lines'
7858
display_match_length = `Pasted ${line_count} ${lines_text}`.length
7959
is_replacement_match = true
@@ -152,7 +132,7 @@ export const map_raw_pos_to_display_pos = (params: {
152132
let last_raw_index = 0
153133

154134
const regex =
155-
/`([^`]+)`|(#Changes\([^)]+\))|(#Selection)|(#SavedContext\((?:WorkspaceState|JSON) "((?:\\.|[^"\\])*)"\))|(#(?:Commit|CommitMessage)\([^:]+:([^\s"]+) "(?:\\.|[^"\\])*"\))|(<fragment path="[^"]+"(?: [^>]+)?>([\s\S]*?)<\/fragment>)|(#Skill\([^)]+\))|(#Image\([a-fA-F0-9]+\))|(#PastedText\([a-fA-F0-9]+:\d+\))|(#Website\([^)]+\))|(#ClipboardPaths)/g
135+
/`([^`]+)`|(#Changes\([^)]+\))|(#Selection)|(#SavedContext\((?:WorkspaceState|JSON) "((?:\\.|[^"\\])*)"\))|(#(?:Commit|CommitMessage)\([^:]+:([^\s"]+) "(?:\\.|[^"\\])*"\))|(#Fragment\((.+?):(\d+):(\d+)-(\d+):(\d+)\))|(#Skill\([^)]+\))|(#Image\([a-fA-F0-9]+\))|(#PastedText\([a-fA-F0-9]+:\d+\))|(#Website\([^)]+\))|(#ClipboardPaths)/g
156136
let match
157137

158138
while ((match = regex.exec(params.raw_text)) !== null) {
@@ -164,12 +144,13 @@ export const map_raw_pos_to_display_pos = (params: {
164144
const commit_symbol = match[6]
165145
const commit_hash = match[7]
166146
const fragment_symbol = match[8]
167-
let fragment_content = match[9]
168-
const skill_symbol = match[10]
169-
const image_symbol = match[11]
170-
const pasted_text_symbol = match[12]
171-
const website_symbol = match[13]
172-
const clipboard_paths_symbol = match[14]
147+
const fragment_start_line = match[10] ? parseInt(match[10], 10) : 0
148+
const fragment_end_line = match[12] ? parseInt(match[12], 10) : 0
149+
const skill_symbol = match[14]
150+
const image_symbol = match[15]
151+
const pasted_text_symbol = match[16]
152+
const website_symbol = match[17]
153+
const clipboard_paths_symbol = match[18]
173154

174155
let is_replacement_match = false
175156
let display_match_length = 0
@@ -196,29 +177,8 @@ export const map_raw_pos_to_display_pos = (params: {
196177
display_match_length = short_hash.length
197178
is_replacement_match = true
198179
} else if (fragment_symbol) {
199-
if (
200-
fragment_content.startsWith('\n```\n') &&
201-
fragment_content.endsWith('\n```\n')
202-
) {
203-
fragment_content = fragment_content.slice(5, -5)
204-
} else if (
205-
fragment_content.startsWith('```\n') &&
206-
fragment_content.endsWith('\n```')
207-
) {
208-
fragment_content = fragment_content.slice(4, -4)
209-
} else if (
210-
fragment_content.startsWith('```') &&
211-
fragment_content.endsWith('```')
212-
) {
213-
fragment_content = fragment_content.slice(3, -3)
214-
} else if (
215-
fragment_content.startsWith('\n') &&
216-
fragment_content.endsWith('\n')
217-
) {
218-
fragment_content = fragment_content.slice(1, -1)
219-
}
220-
const line_count = fragment_content.split('\n').length
221-
const lines_text = line_count == 1 ? 'line' : 'lines'
180+
const line_count = Math.max(1, fragment_end_line - fragment_start_line + 1)
181+
const lines_text = line_count === 1 ? 'line' : 'lines'
222182
display_match_length = `Pasted ${line_count} ${lines_text}`.length
223183
is_replacement_match = true
224184
} else if (skill_symbol) {

‎packages/ui/src/components/editor/common/prompts/shared/symbols/reconstruct-raw-value.ts‎

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -85,31 +85,22 @@ export const reconstruct_raw_value_from_node = (node: Node): string => {
8585
}
8686
} else if (el.dataset.type == 'pasted-lines-symbol') {
8787
const path = el.dataset.path
88-
const content = el.dataset.content
89-
const start = el.dataset.start
90-
const end = el.dataset.end
91-
if (!path || content === undefined) return ''
92-
93-
let attributes = `path="${path}"`
94-
if (start) attributes += ` start="${start}"`
95-
if (end) attributes += ` end="${end}"`
96-
97-
const is_multiline = content.includes('\n')
98-
const formatted_content = is_multiline
99-
? `\n\`\`\`\n${content}\n\`\`\`\n`
100-
: `\`\`\`${content}\`\`\``
101-
const line_count = is_multiline ? content.split('\n').length : 1
102-
const lines_text = line_count === 1 ? 'line' : 'lines'
103-
const label = `Pasted ${line_count} ${lines_text}`
104-
const index = inner_content.indexOf(label)
105-
106-
if (index != -1) {
107-
const prefix = inner_content.substring(0, index)
108-
const suffix = inner_content.substring(index + label.length)
109-
return `${prefix}<fragment ${attributes}>${formatted_content}</fragment>${suffix}`
88+
const start_line = el.dataset.startLine
89+
const start_col = el.dataset.startCol
90+
const end_line = el.dataset.endLine
91+
const end_col = el.dataset.endCol
92+
if (!path || !start_line || !start_col || !end_line || !end_col) return ''
93+
94+
const regex = /Pasted \d+ lines?/
95+
const match = inner_content.match(regex)
96+
97+
if (match && match.index !== undefined) {
98+
const prefix = inner_content.substring(0, match.index)
99+
const suffix = inner_content.substring(match.index + match[0].length)
100+
return `${prefix}#Fragment(${path}:${start_line}:${start_col}-${end_line}:${end_col})${suffix}`
110101
}
111102

112-
return `<fragment ${attributes}>${formatted_content}</fragment>`
103+
return `#Fragment(${path}:${start_line}:${start_col}-${end_line}:${end_col})`
113104
} else if (el.dataset.type == 'skill-symbol') {
114105
const agent = el.dataset.agent
115106
const repo = el.dataset.repo

0 commit comments

Comments
 (0)