f93b8662afd701763c24634879d05dc08b3178de
max
  Fri Jun 5 02:24:16 2026 -0700
Add exon search: jump to GENE exon N from position box

I'm comitting this thinking that the way that we implement searches
leads to duplication of code that doesn't look great to me. While this
feature looks good, the code duplication across C/JS should probably
get reduced with a different approach to the "quick jump" way of the
page. We have currently three ways to quick jump, I think:
- chr:start-end
- rsxxxxx
- gene symbol + autosuggest pick
- HGVS?

They are recognized by both the javascript and the C code with regexes.

I think all of these should be probably be only implemented in the C
code. The JS only sends the current string to the C code and then
gets back if this can be autocompleted and to which position and what
to show in the autosuggest area. For example if you type
"SOD1<space>e" the C code could send back "Continue typing to jump to
exon" and once you're at "SOD1<space>exon 5" the C code sends back
"Hit enter to jump to chrX:123123-123213". This would work with any
type of identifier and the code would stay in the C code, not more
duplication and it would be much clearer to the user what is recognized
in the search box.

Users can now type "TP53 exon 5" or "TP53:e.5[+/-offset]" in the
genome browser position/search box to navigate directly to that exon.

The ":e.N" notation follows the VICC Gene Fusion Specification.
An optional intronic offset (":e.5+2") lands N bases past the exon
boundary, useful for splice site inspection.

C (hgFind.c): findGeneExon() resolves the query against the SQL
genePred tables listed in the hg.conf "geneTracks" key (default:
mane, ncbiRefSeqSelect, knownGene, ncbiRefSeq, ncbiRefSeqHistorical).
bigGenePred tracks (e.g. mane) are supported via bigBedOpenExtraIndex.
Uses the existing exonToPos() function for strand-aware exon lookup.
fixSinglePos() is called so hgp->singlePos is populated for callers.

hgApi.c: new cmd=geneExonToPos returns {"pos":"chrom:start-end"} JSON
so JS can navigate in place without a full page redirect to hgSearch.
Direct URL links (hgTracks?position=GENE+exon+N) also work because
findGeneExon() is hooked into hgPositionsFind().

JS: autocomplete.js injects a local "Jump to exon N" suggestion as
soon as the exon pattern is detected, or a hint item when the query
is still partial ("GENE ex"). Selecting either navigates via hgApi.
hgTracks.js routes the two new autocomplete item types to the hgApi
call. utils.js adds the two regexes (geneExonExp, geneExonCoordExp).

query.html: documents both syntaxes; the :e.N notation links to the
VICC Gene Fusion Specification at fusions.cancervariants.org.

diff --git src/hg/js/hgTracks.js src/hg/js/hgTracks.js
index 19d6115ad8c..3734aaa7c89 100644
--- src/hg/js/hgTracks.js
+++ src/hg/js/hgTracks.js
@@ -5499,31 +5499,52 @@
         }
         reg = new RegExp("(<span class='trackTiming'>[\\S\\s]+?</span>)");
         a = reg.exec(response);
         if (a && a[1]) {
             $('.trackTiming').replaceWith(a[1]);
         }
     },
 
     loadSuggestBox: function ()
     {
         if ($('#positionInput').length) {
             if (!suggestBox.initialized) { // only call init once
                 suggestBox.init(getDb(),
                             $("#suggestTrack").length > 0,
                             function (item) {
-                                if (["helpDocs", "publicHubs", "trackDb"].includes(item.type) ||
+                                if (item.type === "geneExon") {
+                                    // Complete "GENE exon N" — resolve via hgApi and navigate
+                                    $.ajax({
+                                        type: "GET",
+                                        url: "../cgi-bin/hgApi",
+                                        data: cart.varsToUrlData({ 'hgsid': getHgsid(), 'db': getDb(),
+                                              'cmd': 'geneExonToPos', 'symbol': item.symbol,
+                                              'num': item.num, 'offset': item.offset || 0 }),
+                                        trueSuccess: rightClick.handleZoomCodon,
+                                        success: catchErrorOrDispatch,
+                                        error: function() {
+                                            window.location.assign("../cgi-bin/hgSearch?search=" +
+                                                encodeURIComponent(item.value.trim()) + "&hgsid=" + getHgsid());
+                                        },
+                                        cache: false
+                                    });
+                                    return;
+                                } else if (item.type === "geneExonHint") {
+                                    // Partial — input is now "GENE exon "; keep focus so user types the number
+                                    $('#positionInput').focus();
+                                    return;
+                                } else if (["helpDocs", "publicHubs", "trackDb"].includes(item.type) ||
                                         item.id.startsWith("hgc")) {
                                     if (item.geneSymbol) {
                                         selectedGene = item.geneSymbol;
                                         // Overwrite item's long value with symbol after the autocomplete plugin is done:
                                         window.setTimeout($('#positionInput').val(item.geneSymbol), 0);
                                     } else {
                                         selectedGene = item.value;
                                     }
                                     window.location.assign(item.id);
                                 } else if (item.type === "hgvs") {
                                     let newPos = genomePos.setByCoordinates(item.chrom, item.start, item.end);
                                     dragSelect.highlightThisRegion(newPos, true, "#fcfcac");
                                     let withPadding = genomePos.setByCoordinates(item.chrom, item.start-5, item.end+5);
                                     $("#goButton").trigger("click");
                                 } else {
@@ -6265,30 +6286,55 @@
             term = encodeURIComponent(term.replace(/^[\s]*/,'').replace(/[\s]*$/,''));
             function onSuccess(jqXHR, textStatus) {
                 if (jqXHR.chromName !== null) {
                     imageV2.markAsDirtyPage();
                     imageV2.navigateInPlace("db=" + getDb() + "&position=" + encodeURIComponent(newPos), null, false);
                     window.scrollTo(0,0);
                 } else  {
                     window.location.assign("../cgi-bin/hgSearch?search=" + term  + "&hgsid="+ getHgsid());
                 }
             }
             function onFail(jqXHR, textStatus) {
                 window.location.assign("../cgi-bin/hgSearch?search=" + term  + "&hgsid="+ getHgsid());
             }
 
             // redirect to search disambiguation page if it looks like we didn't enter a regular position:
+            // "BRCA1 exon 5" or "BRCA1:e.5+2" — resolve via hgApi and navigate in place
+            var exonMatch = newPos.match(geneExonExp) || newPos.match(geneExonCoordExp);
+            if (exonMatch) {
+                var symbol, num, offset = 0;
+                var m = newPos.match(geneExonExp);
+                if (m) {
+                    symbol = m[1]; num = parseInt(m[2], 10);
+                } else {
+                    m = newPos.match(geneExonCoordExp);
+                    symbol = m[1]; num = parseInt(m[2], 10);
+                    if (m[3]) offset = parseInt(m[3], 10);
+                }
+                $.ajax({
+                    type: "GET",
+                    url: "../cgi-bin/hgApi",
+                    data: cart.varsToUrlData({ 'hgsid': getHgsid(), 'db': getDb(),
+                          'cmd': 'geneExonToPos', 'symbol': symbol, 'num': num, 'offset': offset }),
+                    trueSuccess: rightClick.handleZoomCodon,
+                    success: catchErrorOrDispatch,
+                    error: function() { window.location.assign("../cgi-bin/hgSearch?search=" + term + "&hgsid=" + getHgsid()); },
+                    cache: false
+                });
+                return false;
+            }
+
             var canonMatch = newPos.match(canonicalRangeExp);
             var gbrowserMatch = newPos.match(gbrowserRangeExp);
             var lengthMatch = newPos.match(lengthRangeExp);
             var bedMatch = newPos.match(bedRangeExp);
             var sqlMatch = newPos.match(sqlRangeExp);
             var singleMatch = newPos.match(singleBaseExp);
             var gnomadRangeMatch = newPos.match(gnomadRangeExp);
             var gnomadVarMatch = newPos.match(gnomadVarExp);
             var positionMatch = canonMatch || gbrowserMatch || lengthMatch || bedMatch || sqlMatch || singleMatch || gnomadRangeMatch || gnomadVarMatch;
             if (positionMatch === null) {
                 // user may have entered a full chromosome name, check for that asynchronosly:
                 $.ajax({
                     type: "GET",
                     url: "../cgi-bin/hgSearch",
                     data: cart.varsToUrlData({ 'cjCmd': '{"getChromName": {"db": "' + getDb() + '", "searchTerm": "' + term + '"}}' }),