Skip to content

Commit cd498c9

Browse files
committed
Support escaped quotes in commit messages for hash sign quick pick selections
1 parent 4953385 commit cd498c9

7 files changed

Lines changed: 640 additions & 604 deletions

File tree

packages/ui/src/components/editor/panel/PromptField/PromptField.stories.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,27 @@ export const WithCommit = () => (
137137
/>
138138
)
139139

140+
export const WithCommitWithQuotes = () => (
141+
<PromptField
142+
value='Ask about #Commit:my-repo:a1b2c3d "feat: add "cool" feature"'
143+
chat_history={[]}
144+
on_change={(value) => console.log('Changed:', value)}
145+
on_submit={() => console.log('Submitted')}
146+
on_copy={() => console.log('Copied')}
147+
is_connected={true}
148+
is_in_code_completions_mode={false}
149+
has_active_editor={true}
150+
has_active_selection={false}
151+
on_caret_position_change={(pos) => console.log('Caret position:', pos)}
152+
on_search_click={() => console.log('Search clicked')}
153+
on_at_sign_click={() => console.log('@ clicked')}
154+
on_hash_sign_click={() => console.log('# clicked')}
155+
is_web_mode={false}
156+
on_submit_with_control={() => console.log('Submitted with control')}
157+
context_file_paths={[]}
158+
on_curly_braces_click={() => {}}
159+
/>
160+
)
140161
export const WithContextAtCommit = () => (
141162
<PromptField
142163
value='Ask about #ContextAtCommit:my-repo:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 "Initial commit"'

packages/ui/src/components/editor/panel/PromptField/hooks/use-handlers.ts

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const getKeywordRanges = (
2121
const ranges: { start: number; end: number }[] = []
2222
// This regex is a combination of all keyword types
2323
const regex =
24-
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#Selection)|(#SavedContext:(?:WorkspaceState|JSON)\s+"[^"]+")|(#(?:Commit|ContextAtCommit):[^:]+:[^\s"]+\s+"[^"]*")/g
24+
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#Selection)|(#SavedContext:(?:WorkspaceState|JSON)\s+"[^"]+")|(#(?:Commit|ContextAtCommit):[^:]+:[^\s"]+\s+"(?:\\.|[^"\\])*")/g
2525

2626
let match
2727
while ((match = regex.exec(text)) !== null) {
@@ -48,25 +48,33 @@ const reconstruct_raw_value_from_node = (node: Node): string => {
4848

4949
if (node.nodeType === Node.ELEMENT_NODE) {
5050
const el = node as HTMLElement
51+
let inner_content = ''
52+
for (const child of Array.from(el.childNodes)) {
53+
inner_content += reconstruct_raw_value_from_node(child)
54+
}
5155

5256
switch (el.dataset.type) {
5357
case 'file-keyword': {
5458
const path = el.dataset.path
5559
if (!path) return ''
5660
const filename = path.split('/').pop() || path
57-
if (el.textContent?.startsWith(filename)) {
58-
const extra = el.textContent.substring(filename.length)
59-
return `\`${path}\`${extra}`
61+
const index = inner_content.indexOf(filename)
62+
if (index !== -1) {
63+
const prefix = inner_content.substring(0, index)
64+
const suffix = inner_content.substring(index + filename.length)
65+
return `${prefix}\`${path}\`${suffix}`
6066
}
6167
break
6268
}
6369
case 'changes-keyword': {
6470
const branchName = el.dataset.branchName
6571
if (!branchName) return ''
6672
const expected_text = `Diff with ${branchName}`
67-
if (el.textContent?.startsWith(expected_text)) {
68-
const extra = el.textContent.substring(expected_text.length)
69-
return `#Changes:${branchName}${extra}`
73+
const index = inner_content.indexOf(expected_text)
74+
if (index !== -1) {
75+
const prefix = inner_content.substring(0, index)
76+
const suffix = inner_content.substring(index + expected_text.length)
77+
return `${prefix}#Changes:${branchName}${suffix}`
7078
}
7179
break
7280
}
@@ -75,17 +83,21 @@ const reconstruct_raw_value_from_node = (node: Node): string => {
7583
const contextName = el.dataset.contextName
7684
if (!contextType || !contextName) return ''
7785
const expected_text = `Context "${contextName}"`
78-
if (el.textContent?.startsWith(expected_text)) {
79-
const extra = el.textContent.substring(expected_text.length)
80-
return `#SavedContext:${contextType} "${contextName}"${extra}`
86+
const index = inner_content.indexOf(expected_text)
87+
if (index !== -1) {
88+
const prefix = inner_content.substring(0, index)
89+
const suffix = inner_content.substring(index + expected_text.length)
90+
return `${prefix}#SavedContext:${contextType} "${contextName}"${suffix}`
8191
}
8292
break
8393
}
8494
case 'selection-keyword': {
8595
const expected_text = 'Selection'
86-
if (el.textContent?.startsWith(expected_text)) {
87-
const extra = el.textContent.substring(expected_text.length)
88-
return `#Selection${extra}`
96+
const index = inner_content.indexOf(expected_text)
97+
if (index !== -1) {
98+
const prefix = inner_content.substring(0, index)
99+
const suffix = inner_content.substring(index + expected_text.length)
100+
return `${prefix}#Selection${suffix}`
89101
}
90102
break
91103
}
@@ -97,9 +109,14 @@ const reconstruct_raw_value_from_node = (node: Node): string => {
97109
return ''
98110
}
99111
const short_hash = commit_hash.substring(0, 7)
100-
if (el.textContent?.startsWith(short_hash)) {
101-
const extra = el.textContent.substring(short_hash.length)
102-
return `#Commit:${repo_name}:${commit_hash} "${commit_message}"${extra}`
112+
const index = inner_content.indexOf(short_hash)
113+
if (index !== -1) {
114+
const prefix = inner_content.substring(0, index)
115+
const suffix = inner_content.substring(index + short_hash.length)
116+
return `${prefix}#Commit:${repo_name}:${commit_hash} "${commit_message.replace(
117+
/"/g,
118+
'\\"'
119+
)}"${suffix}`
103120
}
104121
break
105122
}
@@ -111,23 +128,20 @@ const reconstruct_raw_value_from_node = (node: Node): string => {
111128
return ''
112129
}
113130
const short_hash = commit_hash.substring(0, 7)
114-
if (el.textContent?.startsWith(short_hash)) {
115-
const extra = el.textContent.substring(short_hash.length)
116-
return `#ContextAtCommit:${repo_name}:${commit_hash} "${commit_message}"${extra}`
131+
const index = inner_content.indexOf(short_hash)
132+
if (index !== -1) {
133+
const prefix = inner_content.substring(0, index)
134+
const suffix = inner_content.substring(index + short_hash.length)
135+
return `${prefix}#ContextAtCommit:${repo_name}:${commit_hash} "${commit_message.replace(
136+
/"/g,
137+
'\\"'
138+
)}"${suffix}`
117139
}
118140
break
119141
}
120142
}
121143

122-
if (el.childNodes.length > 0) {
123-
let content = ''
124-
for (const child of Array.from(el.childNodes)) {
125-
content += reconstruct_raw_value_from_node(child)
126-
}
127-
return content
128-
}
129-
130-
return el.textContent || ''
144+
return inner_content
131145
}
132146

133147
return ''
@@ -292,7 +306,7 @@ export const use_handlers = (
292306
const commit_message = keyword_element.dataset.commitMessage
293307
if (!repo_name || !commit_hash || commit_message === undefined) return
294308

295-
const search_pattern = `#Commit:${repo_name}:${commit_hash} "${commit_message}"`
309+
const search_pattern = `#Commit:${repo_name}:${commit_hash} "${commit_message.replace(/"/g, '\\"')}"`
296310
const start_index = props.value.indexOf(search_pattern)
297311

298312
if (start_index !== -1) {
@@ -309,7 +323,7 @@ export const use_handlers = (
309323
const commit_message = keyword_element.dataset.commitMessage
310324
if (!repo_name || !commit_hash || commit_message === undefined) return
311325

312-
const search_pattern = `#ContextAtCommit:${repo_name}:${commit_hash} "${commit_message}"`
326+
const search_pattern = `#ContextAtCommit:${repo_name}:${commit_hash} "${commit_message.replace(/"/g, '\\"')}"`
313327
const start_index = props.value.indexOf(search_pattern)
314328

315329
if (start_index !== -1) {
@@ -544,7 +558,7 @@ export const use_handlers = (
544558
): boolean => {
545559
const text_before_cursor = props.value.substring(0, raw_pos)
546560
const match = text_before_cursor.match(
547-
/#(?:Commit|ContextAtCommit):[^:]+:[^\s"]+\s+"[^"]*"$/
561+
/#(?:Commit|ContextAtCommit):[^:]+:[^\s"]+\s+"(?:\\.|[^"\\])*"$/
548562
)
549563

550564
if (match) {

packages/ui/src/components/editor/panel/PromptField/utils/get-display-text.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const get_display_text = (
33
context_file_paths: string[]
44
): string => {
55
const regex =
6-
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#SavedContext:(?:WorkspaceState|JSON)\s+"([^"]+)")|(#(?:Commit|ContextAtCommit):[^:]+:([^\s"]+)\s+"[^"]*")/g
6+
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#SavedContext:(?:WorkspaceState|JSON)\s+"([^"]+)")|(#(?:Commit|ContextAtCommit):[^:]+:([^\s"]+)\s+"(?:\\.|[^"\\])*")/g
77
return text.replace(
88
regex,
99
(

packages/ui/src/components/editor/panel/PromptField/utils/get-highlighted-text.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export const get_highlighted_text = (params: {
8282
}
8383

8484
const commit_regex_part =
85-
'#(?:Commit|ContextAtCommit):[^:]+:[^\\s"]+\\s+"[^"]*"'
85+
'#(?:Commit|ContextAtCommit):[^:]+:[^\\s"]+\\s+"(?:\\\\.|[^"\\\\])*"'
8686
const regex = new RegExp(
8787
`(#Selection|#Changes:[^\\s,;:!?]+|${saved_context_regex_part}|${commit_regex_part})`,
8888
'g'
@@ -135,13 +135,13 @@ export const get_highlighted_text = (params: {
135135
)}"</span></span>`
136136
}
137137
const commit_match = part.match(
138-
/^#(Commit|ContextAtCommit):([^:]+):([^\s"]+)\s+"([^"]*)"$/
138+
/^#(Commit|ContextAtCommit):([^:]+):([^\s"]+)\s+"((?:\\.|[^"\\])*)"$/
139139
)
140140
if (part && commit_match) {
141141
const symbol = commit_match[1]
142142
const repo_name = commit_match[2]
143143
const commit_hash = commit_match[3]
144-
const commit_message = commit_match[4]
144+
const commit_message = commit_match[4].replace(/\\"/g, '"')
145145
const short_hash = commit_hash.substring(0, 7)
146146
return `<span class="${cn(
147147
styles['keyword'],

packages/ui/src/components/editor/panel/PromptField/utils/position-mapping.ts

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

1010
const regex =
11-
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#Selection)|(#SavedContext:(?:WorkspaceState|JSON)\s+"([^"]+)")|(#(?:Commit|ContextAtCommit):[^:]+:([^\s"]+)\s+"[^"]*")/g
11+
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#Selection)|(#SavedContext:(?:WorkspaceState|JSON)\s+"([^"]+)")|(#(?:Commit|ContextAtCommit):[^:]+:([^\s"]+)\s+"(?:\\.|[^"\\])*")/g
1212
let match
1313

1414
while ((match = regex.exec(raw_text)) !== null) {
@@ -85,7 +85,7 @@ export const map_raw_pos_to_display_pos = (
8585
let last_raw_index = 0
8686

8787
const regex =
88-
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#Selection)|(#SavedContext:(?:WorkspaceState|JSON)\s+"([^"]+)")|(#(?:Commit|ContextAtCommit):[^:]+:([^\s"]+)\s+"[^"]*")/g
88+
/`([^\s`]*\.[^\s`]+)`|(#Changes:[^\s,;:!?]+)|(#Selection)|(#SavedContext:(?:WorkspaceState|JSON)\s+"([^"]+)")|(#(?:Commit|ContextAtCommit):[^:]+:([^\s"]+)\s+"(?:\\.|[^"\\])*")/g
8989
let match
9090

9191
while ((match = regex.exec(raw_text)) !== null) {

0 commit comments

Comments
 (0)