Skip to content

Commit 7bbcf4e

Browse files
marcoscaceresCopilotCopilot
authored
fix(xref/ui): disambiguate overloaded method signatures in search results (#495)
* fix(xref): disambiguate overloaded method signatures in search results When multiple xref results share the same term and context (e.g., overloaded methods like Window/postMessage), extract parameter info from the URI fragment to show distinct linking text suggestions. Closes #383 * fix(xref): embed full parameter signature in overloaded cite text Instead of appending a <small> hint after the citation, embed the parameter names directly inside the {{ }} brackets. This produces copy-pasteable cite text that resolves without ambiguity, e.g. {{Window/postMessage(message, options)}} instead of {{Window/postMessage()}} <small>(message, options)</small>. * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(xref): preserve fragment casing in hints; group overloads per citation Agent-Logs-Url: https://github.com/speced/respec-web-services/sessions/3856cbfc-2f26-4105-b882-db96f03098c9 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> * fix(xref): track overloaded pairs per (uri, forContext) not just uri Agent-Logs-Url: https://github.com/speced/respec-web-services/sessions/ddb44eac-0c8b-4050-9210-f242b70f3e59 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 1d05651 commit 7bbcf4e

1 file changed

Lines changed: 108 additions & 6 deletions

File tree

static/xref/script.js

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ function renderResults(entries, query) {
144144
return;
145145
}
146146

147+
// Detect overloaded IDL entries that would produce identical citations.
148+
// Build a set of "uri|forContext" pairs for entries that need disambiguation.
149+
const overloadedPairs = detectOverloadedEntries(entries, term);
150+
147151
let html = '';
148152
for (const entry of entries) {
149153
// Use the canonical matched term when available (case-insensitive fallback
@@ -153,7 +157,7 @@ function renderResults(entries, query) {
153157
const link = new URL(entry.uri, specInfo.url).href;
154158
const title = escapeHTML(specInfo.title);
155159
const cite = metadata.types.idl.has(entry.type)
156-
? howToCiteIDL(citeTerm, entry)
160+
? howToCiteIDL(citeTerm, entry, overloadedPairs)
157161
: metadata.types.markup.has(entry.type)
158162
? howToCiteMarkup(citeTerm, entry)
159163
: metadata.types.css.has(entry.type) ||
@@ -172,26 +176,124 @@ function renderResults(entries, query) {
172176
output.innerHTML = html;
173177
}
174178

175-
function howToCiteIDL(term, entry) {
179+
/**
180+
* Detects IDL entries that would produce identical citations (overloaded
181+
* methods/constructors). Returns a Set of "uri|forContext" keys for entries
182+
* that need disambiguation. Entries without a `for` list use an empty string
183+
* as the forContext part.
184+
*/
185+
function detectOverloadedEntries(entries, term) {
186+
const citationGroups = new Map();
187+
for (const entry of entries) {
188+
if (!metadata.types.idl.has(entry.type)) continue;
189+
const specKey = entry.spec || '';
190+
const statusKey = entry.status || '';
191+
const forList = entry.for || [];
192+
// Group per individual rendered citation (one per `f`), so overlapping
193+
// `for` contexts across entries are correctly detected as ambiguous.
194+
if (forList.length > 0) {
195+
for (const f of forList) {
196+
const key = `${f}|${term}|${specKey}|${statusKey}`;
197+
const group = citationGroups.get(key) ?? [];
198+
if (!group.length) citationGroups.set(key, group);
199+
group.push({ entry, f });
200+
}
201+
} else {
202+
const key = `|${term}|${specKey}|${statusKey}`;
203+
const group = citationGroups.get(key) ?? [];
204+
if (!group.length) citationGroups.set(key, group);
205+
group.push({ entry, f: '' });
206+
}
207+
}
208+
// Build a Set of "uri|forContext" strings for ambiguous (entry, f) pairs.
209+
const overloadedPairs = new Set();
210+
for (const group of citationGroups.values()) {
211+
if (group.length > 1) {
212+
for (const { entry, f } of group) {
213+
overloadedPairs.add(`${entry.uri}|${f}`);
214+
}
215+
}
216+
}
217+
return overloadedPairs;
218+
}
219+
220+
function howToCiteIDL(term, entry, overloadedPairs = null) {
176221
const { type, for: forList } = entry;
177222
const safeTerm = escapeHTML(term);
178223
if (forList) {
179224
return forList
180225
.map(f => {
181226
const safeF = escapeHTML(f);
182-
const termPart = type === 'enum-value' ? `"${safeTerm}"` : safeTerm;
183-
return `{{${safeF}/${safeTerm ? termPart : '""'}}}`;
227+
let displayTerm = type === 'enum-value' ? `"${safeTerm}"` : safeTerm;
228+
if (overloadedPairs?.has(`${entry.uri}|${f}`)) {
229+
const hint = extractOverloadHint(entry.uri, f, term);
230+
if (hint) {
231+
displayTerm = displayTerm.replace('()', `(${escapeHTML(hint)})`);
232+
}
233+
}
234+
return `{{${safeF}/${displayTerm ? displayTerm : '""'}}}`;
184235
})
185236
.join('<br>');
186237
}
238+
let cite;
187239
switch (type) {
188240
case 'exception':
189241
if (!exceptionExceptions.has(term)) {
190-
return `{{"${safeTerm}"}}`;
242+
cite = `{{"${safeTerm}"}}`;
243+
break;
191244
}
192245
default:
193-
return `{{${safeTerm}}}`;
246+
cite = `{{${safeTerm}}}`;
194247
}
248+
if (overloadedPairs?.has(`${entry.uri}|`)) {
249+
const hint = extractOverloadHint(entry.uri, null, term);
250+
if (hint) {
251+
cite = cite.replace('()', `(${escapeHTML(hint)})`);
252+
}
253+
}
254+
return cite;
255+
}
256+
257+
/**
258+
* Extracts a human-readable overload hint from a URI fragment.
259+
*
260+
* URI fragments for WebIDL definitions follow the pattern:
261+
* #dom-<interface>-<method>-<param1>-<param2>-...
262+
*
263+
* For example:
264+
* #dom-window-postmessage-message-targetorigin-transfer
265+
* #dom-window-postmessage-message-options
266+
*
267+
* This function strips the known prefix (interface + method) and returns
268+
* the remaining parts as a parameter list, e.g. "message, targetorigin, transfer".
269+
*/
270+
function extractOverloadHint(uri, forContext, term) {
271+
if (!uri) return '';
272+
const hash = uri.includes('#') ? uri.split('#')[1] : uri;
273+
if (!hash) return '';
274+
275+
const originalParts = hash.split('-');
276+
const lowerParts = hash.toLowerCase().split('-');
277+
278+
// Build the prefix to strip: typically "dom", interface, method
279+
const prefixParts = ['dom'];
280+
if (forContext) {
281+
prefixParts.push(...forContext.toLowerCase().split('-'));
282+
}
283+
const cleanTerm = term.replace(/\(.*\)$/, '').toLowerCase();
284+
if (cleanTerm) {
285+
prefixParts.push(...cleanTerm.split('-'));
286+
}
287+
288+
const matches =
289+
prefixParts.length <= lowerParts.length &&
290+
prefixParts.every((p, i) => lowerParts[i] === p);
291+
292+
if (matches && prefixParts.length < lowerParts.length) {
293+
return originalParts.slice(prefixParts.length).join(', ');
294+
}
295+
296+
return '';
195297
}
196298

197299
function howToCiteMarkup(term, entry) {

0 commit comments

Comments
 (0)