@jbrowse/plugin-linear-genome-view 5.0.0-beta.2 → 5.0.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/esm/LinearGenomeView/afterAttach.d.ts +22 -5
  2. package/esm/LinearGenomeView/afterAttach.js +129 -106
  3. package/esm/LinearGenomeView/axisConfigSchema.d.ts +18 -0
  4. package/esm/LinearGenomeView/axisConfigSchema.js +22 -0
  5. package/esm/LinearGenomeView/components/CenterLine.js +10 -5
  6. package/esm/LinearGenomeView/components/ConnectedHoverHighlight.js +2 -3
  7. package/esm/LinearGenomeView/components/CrisprGuidePanel.js +3 -5
  8. package/esm/LinearGenomeView/components/ExportSvgDialog.d.ts +9 -4
  9. package/esm/LinearGenomeView/components/ExportSvgDialog.js +45 -8
  10. package/esm/LinearGenomeView/components/GetSequenceDialog.js +2 -2
  11. package/esm/LinearGenomeView/components/Header.js +11 -1
  12. package/esm/LinearGenomeView/components/HeaderClearHighlightButton.js +2 -2
  13. package/esm/LinearGenomeView/components/HeaderPanControls.d.ts +3 -3
  14. package/esm/LinearGenomeView/components/HeaderPanControls.js +6 -3
  15. package/esm/LinearGenomeView/components/HeaderSearchBoxes.js +4 -1
  16. package/esm/LinearGenomeView/components/HeaderZoomControls.js +6 -35
  17. package/esm/LinearGenomeView/components/Highlight.js +27 -11
  18. package/esm/LinearGenomeView/components/HighlightBand.d.ts +11 -2
  19. package/esm/LinearGenomeView/components/HighlightBand.js +32 -13
  20. package/esm/LinearGenomeView/components/HighlightChip.d.ts +3 -3
  21. package/esm/LinearGenomeView/components/HighlightChip.js +13 -8
  22. package/esm/LinearGenomeView/components/HighlightLabelDialog.d.ts +6 -0
  23. package/esm/LinearGenomeView/components/HighlightLabelDialog.js +18 -0
  24. package/esm/LinearGenomeView/components/HoverPositionHighlight.js +3 -3
  25. package/esm/LinearGenomeView/components/ImportForm.js +7 -9
  26. package/esm/LinearGenomeView/components/LinearGenomeView.js +5 -12
  27. package/esm/LinearGenomeView/components/LinearGenomeViewContainer.js +13 -14
  28. package/esm/LinearGenomeView/components/MiniControls.js +11 -5
  29. package/esm/LinearGenomeView/components/OverviewHighlight.js +3 -3
  30. package/esm/LinearGenomeView/components/OverviewHighlightBand.d.ts +3 -2
  31. package/esm/LinearGenomeView/components/OverviewHighlightBand.js +6 -5
  32. package/esm/LinearGenomeView/components/OverviewRubberband.js +25 -48
  33. package/esm/LinearGenomeView/components/OverviewScalebar.js +4 -5
  34. package/esm/LinearGenomeView/components/OverviewScalebarPolygon.js +5 -3
  35. package/esm/LinearGenomeView/components/PaddingBlocks.js +3 -8
  36. package/esm/LinearGenomeView/components/RangeSelectOverlay.js +5 -6
  37. package/esm/LinearGenomeView/components/RegionWidthEditorDialog.d.ts +3 -2
  38. package/esm/LinearGenomeView/components/RegionWidthEditorDialog.js +4 -4
  39. package/esm/LinearGenomeView/components/ResizeAllTracksHandle.d.ts +5 -0
  40. package/esm/LinearGenomeView/components/ResizeAllTracksHandle.js +74 -0
  41. package/esm/LinearGenomeView/components/Rubberband.d.ts +1 -2
  42. package/esm/LinearGenomeView/components/Rubberband.js +3 -2
  43. package/esm/LinearGenomeView/components/SVGHighlightBand.d.ts +10 -1
  44. package/esm/LinearGenomeView/components/SVGHighlightBand.js +16 -5
  45. package/esm/LinearGenomeView/components/Scalebar.d.ts +2 -6
  46. package/esm/LinearGenomeView/components/Scalebar.js +8 -5
  47. package/esm/LinearGenomeView/components/ScalebarRefNameLabels.js +51 -36
  48. package/esm/LinearGenomeView/components/SearchBox.js +10 -16
  49. package/esm/LinearGenomeView/components/SearchPanelForm.d.ts +3 -2
  50. package/esm/LinearGenomeView/components/SearchPanelForm.js +4 -2
  51. package/esm/LinearGenomeView/components/SearchResultsDialog.d.ts +4 -8
  52. package/esm/LinearGenomeView/components/SearchResultsDialog.js +7 -4
  53. package/esm/LinearGenomeView/components/SearchResultsTable.d.ts +4 -7
  54. package/esm/LinearGenomeView/components/SearchResultsTable.js +25 -53
  55. package/esm/LinearGenomeView/components/SequenceFeatureHoverHighlight.js +2 -3
  56. package/esm/LinearGenomeView/components/SequenceSearchDialog.d.ts +2 -4
  57. package/esm/LinearGenomeView/components/StrandCheckboxes.d.ts +3 -2
  58. package/esm/LinearGenomeView/components/StrandCheckboxes.js +4 -2
  59. package/esm/LinearGenomeView/components/TrackContainer.d.ts +2 -3
  60. package/esm/LinearGenomeView/components/TrackContainer.js +63 -10
  61. package/esm/LinearGenomeView/components/TrackLabel.js +14 -4
  62. package/esm/LinearGenomeView/components/TrackLabelButtons.js +2 -2
  63. package/esm/LinearGenomeView/components/TrackLabelDragHandle.js +4 -5
  64. package/esm/LinearGenomeView/components/TrackLabelMenu.js +11 -14
  65. package/esm/LinearGenomeView/components/TrackLabelRefNameWarning.d.ts +3 -4
  66. package/esm/LinearGenomeView/components/TrackLabelRefNameWarning.js +23 -10
  67. package/esm/LinearGenomeView/components/TrackRenderingContainer.d.ts +6 -4
  68. package/esm/LinearGenomeView/components/TrackRenderingContainer.js +12 -24
  69. package/esm/LinearGenomeView/components/TracksContainer.js +11 -23
  70. package/esm/LinearGenomeView/components/VerticalGuide.d.ts +1 -2
  71. package/esm/LinearGenomeView/components/VerticalGuide.js +3 -16
  72. package/esm/LinearGenomeView/components/fetchSequence.d.ts +1 -2
  73. package/esm/LinearGenomeView/components/fetchSequence.js +15 -7
  74. package/esm/LinearGenomeView/components/recentLocationsMenu.js +10 -0
  75. package/esm/LinearGenomeView/components/useRangeSelect.d.ts +0 -3
  76. package/esm/LinearGenomeView/components/useRangeSelect.js +26 -61
  77. package/esm/LinearGenomeView/components/useSearchBoxPrefs.d.ts +3 -1
  78. package/esm/LinearGenomeView/components/useSearchBoxPrefs.js +4 -2
  79. package/esm/LinearGenomeView/components/useSideScroll.d.ts +17 -2
  80. package/esm/LinearGenomeView/components/useSideScroll.js +156 -102
  81. package/esm/LinearGenomeView/components/util.d.ts +4 -0
  82. package/esm/LinearGenomeView/components/util.js +4 -2
  83. package/esm/LinearGenomeView/consts.d.ts +2 -0
  84. package/esm/LinearGenomeView/consts.js +3 -1
  85. package/esm/LinearGenomeView/containingLgv.d.ts +21 -0
  86. package/esm/LinearGenomeView/containingLgv.js +23 -0
  87. package/esm/LinearGenomeView/headerFit.d.ts +4 -2
  88. package/esm/LinearGenomeView/headerFit.js +20 -7
  89. package/esm/LinearGenomeView/index.d.ts +2 -7
  90. package/esm/LinearGenomeView/index.js +5 -7
  91. package/esm/LinearGenomeView/keyboardHandler.d.ts +8 -7
  92. package/esm/LinearGenomeView/keyboardHandler.js +61 -23
  93. package/esm/LinearGenomeView/launchKeys.d.ts +1 -1
  94. package/esm/LinearGenomeView/launchKeys.js +11 -6
  95. package/esm/LinearGenomeView/lazyDialogs.d.ts +16 -3
  96. package/esm/LinearGenomeView/lazyDialogs.js +1 -0
  97. package/esm/LinearGenomeView/linkedViewSync.d.ts +3 -11
  98. package/esm/LinearGenomeView/linkedViewSync.js +27 -49
  99. package/esm/LinearGenomeView/menuItems.d.ts +16 -18
  100. package/esm/LinearGenomeView/menuItems.js +264 -164
  101. package/esm/LinearGenomeView/model.d.ts +1192 -560
  102. package/esm/LinearGenomeView/model.js +805 -590
  103. package/esm/LinearGenomeView/rExport/exportLinearView.d.ts +17 -0
  104. package/esm/LinearGenomeView/rExport/exportLinearView.js +117 -0
  105. package/esm/LinearGenomeView/rExport/rScript.d.ts +110 -0
  106. package/esm/LinearGenomeView/rExport/rScript.js +524 -0
  107. package/esm/LinearGenomeView/sharedScaleContainer.d.ts +1 -1
  108. package/esm/LinearGenomeView/sharedScaleContainer.js +2 -12
  109. package/esm/LinearGenomeView/showFiles.d.ts +2 -0
  110. package/esm/LinearGenomeView/showFiles.js +19 -0
  111. package/esm/LinearGenomeView/showRegionsWithUndo.d.ts +1 -1
  112. package/esm/LinearGenomeView/showRegionsWithUndo.js +13 -5
  113. package/esm/LinearGenomeView/stickyChrome.d.ts +5 -9
  114. package/esm/LinearGenomeView/stickyChrome.js +5 -9
  115. package/esm/LinearGenomeView/svgcomponents/SVGGridlines.js +4 -4
  116. package/esm/LinearGenomeView/svgcomponents/SVGHeader.d.ts +4 -1
  117. package/esm/LinearGenomeView/svgcomponents/SVGHeader.js +8 -21
  118. package/esm/LinearGenomeView/svgcomponents/SVGHighlights.d.ts +3 -1
  119. package/esm/LinearGenomeView/svgcomponents/SVGHighlights.js +9 -9
  120. package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.d.ts +3 -1
  121. package/esm/LinearGenomeView/svgcomponents/SVGHighlightsOverlay.js +5 -5
  122. package/esm/LinearGenomeView/svgcomponents/SVGLinearGenomeView.js +13 -18
  123. package/esm/LinearGenomeView/svgcomponents/SVGRegionSeparators.js +2 -5
  124. package/esm/LinearGenomeView/svgcomponents/SVGRowHeader.js +5 -6
  125. package/esm/LinearGenomeView/svgcomponents/SVGRuler.js +9 -10
  126. package/esm/LinearGenomeView/svgcomponents/SVGScalebar.js +3 -3
  127. package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.d.ts +26 -0
  128. package/esm/LinearGenomeView/svgcomponents/SVGStackedRow.js +12 -0
  129. package/esm/LinearGenomeView/svgcomponents/SVGTrackLabel.js +21 -13
  130. package/esm/LinearGenomeView/svgcomponents/SVGTracks.d.ts +4 -5
  131. package/esm/LinearGenomeView/svgcomponents/SVGTracks.js +7 -6
  132. package/esm/LinearGenomeView/svgcomponents/SVGView.d.ts +2 -3
  133. package/esm/LinearGenomeView/svgcomponents/SVGView.js +17 -3
  134. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.d.ts +24 -20
  135. package/esm/LinearGenomeView/svgcomponents/renderViewTracks.js +42 -29
  136. package/esm/LinearGenomeView/svgcomponents/useViewSvgFigure.js +74 -77
  137. package/esm/LinearGenomeView/svgcomponents/util.d.ts +28 -12
  138. package/esm/LinearGenomeView/svgcomponents/util.js +41 -29
  139. package/esm/LinearGenomeView/types.d.ts +13 -10
  140. package/esm/LinearGenomeView/types.js +0 -1
  141. package/esm/LinearGenomeView/util.d.ts +103 -46
  142. package/esm/LinearGenomeView/util.js +222 -170
  143. package/esm/MultiLevelRubberband/MultiLevelRubberband.js +9 -6
  144. package/esm/MultiLevelRubberband/index.d.ts +2 -0
  145. package/esm/MultiLevelRubberband/index.js +2 -0
  146. package/esm/MultiLevelRubberband/menuItems.d.ts +34 -0
  147. package/esm/MultiLevelRubberband/menuItems.js +52 -0
  148. package/esm/MultiLevelRubberband/rowLabel.d.ts +22 -0
  149. package/esm/MultiLevelRubberband/rowLabel.js +27 -0
  150. package/esm/MultiLevelRubberband/types.d.ts +1 -0
  151. package/esm/MultiLevelRubberband/useRangeSelect.d.ts +2 -1
  152. package/esm/MultiLevelRubberband/useRangeSelect.js +45 -59
  153. package/esm/TrackModifiers/index.d.ts +83 -0
  154. package/esm/TrackModifiers/index.js +231 -0
  155. package/esm/displayKitTests/perRegionTestEnv.d.ts +244 -68
  156. package/esm/displayKitTests/perRegionTestEnv.js +30 -26
  157. package/esm/index.d.ts +21 -99
  158. package/esm/index.js +17 -38
  159. package/esm/searchUtils.d.ts +26 -10
  160. package/esm/searchUtils.js +137 -137
  161. package/esm/shared/RubberbandSpan.d.ts +3 -2
  162. package/esm/shared/RubberbandSpan.js +6 -3
  163. package/esm/shared/coordLabels.d.ts +11 -4
  164. package/esm/shared/coordLabels.js +20 -5
  165. package/esm/shared/useWindowDrag.d.ts +18 -0
  166. package/esm/shared/useWindowDrag.js +47 -0
  167. package/package.json +12 -11
  168. package/esm/FeatureTrack/configSchema.d.ts +0 -124
  169. package/esm/FeatureTrack/configSchema.js +0 -54
  170. package/esm/FeatureTrack/index.d.ts +0 -2
  171. package/esm/FeatureTrack/index.js +0 -13
  172. package/esm/LinearGenomeView/initKeys.d.ts +0 -12
  173. package/esm/LinearGenomeView/initKeys.js +0 -68
  174. package/esm/MultiLevelRubberband/VerticalGuide.d.ts +0 -6
  175. package/esm/MultiLevelRubberband/VerticalGuide.js +0 -21
  176. package/esm/lazyPluginExports.d.ts +0 -23
  177. package/esm/lazyPluginExports.js +0 -36
@@ -1,22 +1,26 @@
1
1
  import { RefSequenceResult } from '@jbrowse/core/TextSearch/BaseResults';
2
- import { assembleLocString, dedupe, getEnv, getNotificationSink, getSession, matchRefNames, MAX_GLOB_REGIONS, parseLocString, UnknownRefNameError, } from '@jbrowse/core/util';
2
+ import { groupByPlace, landsInAssembly } from '@jbrowse/core/TextSearch/places';
3
+ import { dedupe, getEnv, getNotificationSink, getSession, matchRefNames, MAX_GLOB_REGIONS, rankRefNames, UnknownRefNameError, } from '@jbrowse/core/util';
3
4
  import { isAlive } from '@jbrowse/mobx-state-tree';
4
5
  import { parseLocStrings } from './LinearGenomeView/util.js';
5
6
  // Budget for refName suggestions from a query. Deliberately far below the
6
- // autocomplete's ~100-row display cap: refNames are returned ahead of the text
7
- // search hits, so a scaffold-heavy assembly would otherwise fill every row with
7
+ // autocomplete's ~100-row display cap: exact and prefix refName hits are
8
+ // returned ahead of the text search hits, so a scaffold-heavy assembly would otherwise fill every row with
8
9
  // scaffolds and push the gene hits out of the list entirely. Raising this to
9
10
  // match the display cap looks like a consistency fix and is not one.
10
11
  const MAX_REFNAME_HITS = 10;
11
12
  // shared dispatch used by SearchBox.onSelect and the LGV ImportForm submit:
12
13
  // route a chosen result to a direct nav, a multi-result dialog, or a generic
13
14
  // locstring/refname resolution
15
+ // Resolves whether the view moved: a hit that raised the picker, or a glob
16
+ // refused as too wide, did not.
14
17
  export async function navigateToSelectedOption({ option, model, assemblyName, }) {
15
18
  if (option.hasLocation()) {
16
19
  await navToOption({ option, model, assemblyName });
20
+ return true;
17
21
  }
18
22
  else if (option.results?.length) {
19
- await showSearchResults({
23
+ return showSearchResults({
20
24
  results: option.results,
21
25
  query: option.getLabel(),
22
26
  model,
@@ -24,7 +28,7 @@ export async function navigateToSelectedOption({ option, model, assemblyName, })
24
28
  });
25
29
  }
26
30
  else {
27
- await handleSelectedRegion({
31
+ return handleSelectedRegion({
28
32
  input: option.getLabel(),
29
33
  assemblyName,
30
34
  model,
@@ -36,37 +40,6 @@ export function isOpenInView(result, model) {
36
40
  const trackId = result.getTrackId();
37
41
  return trackId !== undefined && !!model.getTrack(trackId);
38
42
  }
39
- // One spelling of a locstring, so two indexes that answer `chr1:1-100` and
40
- // `1:1..100` are recognised as one place. An unparseable string is compared
41
- // raw, which can only ever split a group that would otherwise have merged —
42
- // the safe direction, since the picker is what an unprovable match falls back
43
- // to.
44
- function canonicalLocString(locString, assembly) {
45
- try {
46
- const loc = parseLocString(locString, refName => assembly.isValidRefName(refName));
47
- return assembleLocString({
48
- ...loc,
49
- refName: assembly.getCanonicalRefName2(loc.refName),
50
- });
51
- }
52
- catch (e) {
53
- console.warn('failed to parse location string', locString, e);
54
- return locString;
55
- }
56
- }
57
- // What a hit means as a destination: the name it shows and the place it goes.
58
- // A hit with no location has no destination and can never join a group.
59
- function destination(result, assembly) {
60
- const locString = result.getLocation();
61
- return locString
62
- ? [
63
- result.getDisplayString(),
64
- assembly?.initialized
65
- ? canonicalLocString(locString, assembly)
66
- : locString,
67
- ].join('\u0000')
68
- : undefined;
69
- }
70
43
  // Which hit of an agreeing group to travel through, best rung first: a track
71
44
  // the view already has on screen, then one the session could open if asked,
72
45
  // then whatever the ranking put first. The middle rung is not hypothetical —
@@ -86,12 +59,10 @@ function trackRank(result, model, session) {
86
59
  return session.getTrackById(trackId) ? 1 : 3;
87
60
  }
88
61
  }
89
- // The pick is a stable minimum, so a linear scan rather than a sort: only the
90
- // best rung is taken, and keeping the first strictly-lowest one is what leaves
91
- // an agreeing group with nothing to choose between in the order the ranking
92
- // handed it. Ranked once per hit for the reason `destinations` above it is:
93
- // `trackRank` reaches `session.getTrackById`, and a comparator asks O(n log n)
94
- // times for an answer that does not vary.
62
+ // The pick is a stable minimum, so a linear scan rather than a sort: keeping
63
+ // the first strictly-lowest rung leaves a group with nothing to choose between
64
+ // in the order the ranking handed it. A comparator would ask `trackRank`, and
65
+ // through it `session.getTrackById`, O(n log n) times.
95
66
  function bestRanked(results, model, session) {
96
67
  let best = results[0];
97
68
  let bestRank = trackRank(best, model, session);
@@ -104,42 +75,32 @@ function bestRanked(results, model, session) {
104
75
  }
105
76
  return best;
106
77
  }
107
- // The picker exists to disambiguate, and hits that name one feature at one
108
- // place are not ambiguous — they are several indexes having found it. An
109
- // instance carrying a handful of gene tracks turns every gene search into a
110
- // table whose only varying column is Track, which is issues #4302 and #5068.
111
- // When the hits agree there is nothing to ask, so navigate instead.
112
- export function unanimousResult({ results, model, session, assembly, }) {
113
- // computed once per hit rather than once per comparison: a broad query can
114
- // carry a hundred results, and each one costs a locstring parse
115
- const destinations = results.map(r => destination(r, assembly));
116
- const agree = destinations[0] !== undefined &&
117
- destinations.every(d => d === destinations[0]);
118
- return agree ? bestRanked(results, model, session) : undefined;
78
+ // One hit per place, in the order the ranking first reached each. Hits that
79
+ // name one feature at one place are several indexes having found it, not a
80
+ // choice to put to the user: a handful of gene tracks turned every gene search
81
+ // into a picker whose only varying column was Track (issues #4302 and #5068),
82
+ // and two whose gene models end a few bases apart still did.
83
+ export function distinctDestinations({ results, model, session, assembly, }) {
84
+ return groupByPlace(results, assembly).map(group => bestRanked(group, model, session));
119
85
  }
120
86
  // Every multi-hit result set reaches the picker through here, so the two
121
- // search surfaces cannot disagree about when one is worth raising. Returns
122
- // whether the view moved.
87
+ // search surfaces cannot disagree about when one is worth raising: only when
88
+ // the hits name more than one place. Returns whether the view moved.
123
89
  export async function showSearchResults({ results, query, model, assemblyName, grow, showHitTrack, }) {
124
90
  const session = getSession(model);
125
- const unanimous = unanimousResult({
91
+ const places = distinctDestinations({
126
92
  results,
127
93
  model,
128
94
  session,
129
95
  assembly: session.assemblyManager.get(assemblyName),
130
96
  });
131
- if (unanimous) {
132
- await navToOption({
133
- option: unanimous,
134
- model,
135
- assemblyName,
136
- grow,
137
- showHitTrack,
138
- });
97
+ const land = (option) => navToOption({ option, model, assemblyName, grow, showHitTrack });
98
+ if (places.length === 1) {
99
+ await land(places[0]);
139
100
  return true;
140
101
  }
141
102
  else {
142
- model.setSearchResults(results, query, assemblyName);
103
+ model.setSearchResults(places, query, assemblyName, land);
143
104
  return false;
144
105
  }
145
106
  }
@@ -147,6 +108,18 @@ export async function showSearchResults({ results, query, model, assemblyName, g
147
108
  // own edges, which is what this default is; a caller that asked for a specific
148
109
  // padding gets that instead.
149
110
  const SEARCH_HIT_GROW = 0.2;
111
+ // The track to show beside a hit: the one its index came from, when this
112
+ // session has it. A shared index names tracks of whichever config it was built
113
+ // for — the UCSC hg38 names index says `hg38-ncbiRefSeqGff`, which the HPRC
114
+ // pangenome config reuses without carrying — and launching a track the session
115
+ // lacks turned every gene search there into a "Could not resolve identifier"
116
+ // error over a view that had navigated fine.
117
+ export function hitTrackToShow(result, session) {
118
+ const trackId = result.getTrackId();
119
+ return trackId !== undefined && session.getTrackById(trackId)
120
+ ? trackId
121
+ : undefined;
122
+ }
150
123
  // A hit lands with the track its index came from shown, which is what a name
151
124
  // typed into the search box wants. A session spec that named its own tracks
152
125
  // passes false: it asked for those tracks, and a hosted config's full RefSeq
@@ -156,11 +129,11 @@ export async function navToOption({ option, model, assemblyName, grow, showHitTr
156
129
  // but carries no coordinates; treat that as "no location" and fall back to
157
130
  // the label rather than forwarding '' into an empty, view-blanking parse
158
131
  const location = option.getLocation() || option.getLabel();
159
- const trackId = option.getTrackId();
160
132
  const session = getSession(model);
161
133
  const { assemblyManager } = session;
162
134
  await model.navToLocations(parseLocStrings(location, assemblyName, (ref, asm) => assemblyManager.isValidRefName(ref, asm)), assemblyName, grow ?? SEARCH_HIT_GROW);
163
- if (showHitTrack && trackId && isAlive(model)) {
135
+ const trackId = showHitTrack ? hitTrackToShow(option, session) : undefined;
136
+ if (trackId && isAlive(model)) {
164
137
  await model.launchTrack(trackId);
165
138
  }
166
139
  // same detach hazard as showTrack above: navToLocations awaits, and handlers
@@ -189,9 +162,9 @@ export function notifySearchFailure(session, e) {
189
162
  console.error(e);
190
163
  session.notify(e instanceof SearchResultsNotFoundError ? e.message : `${e}`, 'warning');
191
164
  }
192
- // if input is a known ref or locstring, navigate directly;
193
- // otherwise search and: pop a dialog for multiple results, navigate for one,
194
- // or fall back to treating input as a locstring
165
+ // if input parses as one or more locstrings, navigate directly; otherwise
166
+ // search and: pop a dialog for multiple results, navigate for one, or report
167
+ // the miss
195
168
  //
196
169
  // Returns whether the view actually moved. Three branches deliberately do not
197
170
  // navigate — a glob too wide to open, a multi-hit search whose hits disagree
@@ -200,6 +173,7 @@ export function notifySearchFailure(session, e) {
200
173
  // otherwise tell "showing it" from "asked you which one".
201
174
  export async function handleSelectedRegion({ input, model, assemblyName, grow, showHitTrack, }) {
202
175
  const { assemblyManager, textSearchManager } = getSession(model);
176
+ model.closeSearchPicker();
203
177
  // resolves only once regions/aliases are loaded, which isValidRefName needs
204
178
  // (it throws otherwise). A load failure is not this function's to report:
205
179
  // swallow it and let the input fall through to the text-search path below
@@ -207,16 +181,11 @@ export async function handleSelectedRegion({ input, model, assemblyName, grow, s
207
181
  const assembly = await assemblyManager
208
182
  .waitForAssembly(assemblyName)
209
183
  .catch(() => undefined);
210
- // the same predicate navToLocstrings hands parseLocStrings, so a locstring
211
- // that passes here is one the parse below will accept
184
+ await Promise.all(assembliesNamedIn(input).map(name => assemblyManager.requireAssembly(name)));
212
185
  const isRef = (ref) => !!assembly && assemblyManager.isValidRefName(ref, assemblyName);
213
- // navigate treating input as one or more whitespace-separated locstrings
214
- const navToLocstrings = () => model.navToLocations(parseLocStrings(input, assemblyName, (ref, asm) => assemblyManager.isValidRefName(ref, asm)), assemblyName, grow);
215
- if (input
216
- .split(/\s+/)
217
- .filter(Boolean)
218
- .every(entry => checkRef(entry, isRef))) {
219
- await navToLocstrings();
186
+ const parsed = parseOrError(() => parseLocStrings(input, assemblyName, (ref, asm) => assemblyManager.isValidRefName(ref, asm)));
187
+ if (parsed.locations) {
188
+ await model.navToLocations(parsed.locations, assemblyName, grow);
220
189
  return true;
221
190
  }
222
191
  else if (input.includes('*') && !!assembly) {
@@ -265,14 +234,14 @@ export async function handleSelectedRegion({ input, model, assemblyName, grow, s
265
234
  // adapters answer 'exact' by filtering exactly this list. An adapter that
266
235
  // tags nothing simply never wins the exact pass, which is the behaviour it
267
236
  // had when it returned nothing for searchType: 'exact'.
268
- const allResults = await fetchResults({
237
+ const found = await searchNames({
269
238
  queryString: input,
270
239
  assemblyName,
271
240
  textSearchManager,
272
241
  assembly,
273
242
  });
274
- const exactResults = allResults.filter(r => r.isExact());
275
- const results = exactResults.length ? exactResults : allResults;
243
+ const exactResults = found.results.filter(r => r.isExact());
244
+ const results = exactResults.length ? exactResults : found.results;
276
245
  // the view may have been closed/detached while the text-search RPC ran
277
246
  if (!isAlive(model)) {
278
247
  return false;
@@ -302,73 +271,104 @@ export async function handleSelectedRegion({ input, model, assemblyName, grow, s
302
271
  return true;
303
272
  }
304
273
  else {
305
- // no search hits: still try to resolve the input as a locstring (bare
306
- // refname, "ref start end" triplet, etc). if that also can't find a
307
- // refname AND the input is a single bare token (a plausible gene name),
308
- // reframe the unknown-ref error as a clean "no results" message; keep the
309
- // specific ref error for coordinate/multi-part queries
310
- try {
311
- await navToLocstrings();
312
- return true;
313
- }
314
- catch (e) {
315
- const isPlainName = !input.includes(':') && !input.includes(' ');
316
- if (e instanceof UnknownRefNameError && isPlainName) {
317
- throw new SearchResultsNotFoundError(`No results found for "${input}"`);
318
- }
319
- else {
320
- throw e;
321
- }
322
- }
274
+ // no search hits and no locstring either. An input naming no sequence
275
+ // and no coordinate — a gene name, or a phrase like "breast cancer" — is
276
+ // reported as a name miss; coordinate-shaped input keeps its ref error
277
+ const isPlainName = !input.includes(':') &&
278
+ input
279
+ .trim()
280
+ .split(/\s+/)
281
+ .every(token => !isRef(token) && !/^\d[\d,]*$/.test(token));
282
+ throw parsed.error instanceof UnknownRefNameError && isPlainName
283
+ ? nameNotFound(input, assemblyName, found)
284
+ : parsed.error;
323
285
  }
324
286
  }
325
287
  }
326
- export function checkRef(str, isRef) {
327
- const [ref, rest] = splitLast(str, ':');
328
- return isRef(str) || (isRef(ref) && /^\d/.test(rest));
288
+ function parseOrError(parse) {
289
+ try {
290
+ return { locations: parse() };
291
+ }
292
+ catch (error) {
293
+ return { error };
294
+ }
295
+ }
296
+ function assembliesNamedIn(input) {
297
+ return [...input.matchAll(/(?:^|\s)\{([^}]+)\}/g)].map(([, name]) => name);
329
298
  }
330
- export async function fetchResults({ queryString, searchType, assemblyName, textSearchManager, assembly, stopToken, }) {
331
- const textSearchResults = await textSearchManager?.search({
299
+ async function searchNames({ queryString, searchType, assemblyName, textSearchManager, assembly, signal, }) {
300
+ const report = await textSearchManager?.searchIndexes({
332
301
  queryString,
333
302
  searchType,
334
- stopToken,
303
+ signal,
335
304
  }, assemblyName);
336
305
  // ensure aliases are loaded: allRefNames is a pure getter, so reading it does
337
306
  // not itself kick off the lazy load. A load failure just leaves allRefNames
338
307
  // undefined, so this still returns whatever the text search turned up
339
308
  await assembly?.load().catch(() => { });
340
- const refNameResults = assembly
309
+ const { strong, weak } = assembly
341
310
  ? searchRefNames(assembly, queryString, searchType)
342
- : [];
343
- return dedupe([...refNameResults, ...(textSearchResults ?? [])], elt => elt.getId());
311
+ : { strong: [], weak: [] };
312
+ return {
313
+ results: dedupe([
314
+ ...strong,
315
+ ...(report?.results ?? []).filter(r => landsInAssembly(r, assembly)),
316
+ ...weak,
317
+ ], elt => elt.getId()),
318
+ indexCount: report?.indexCount ?? 0,
319
+ failures: report?.failures ?? [],
320
+ };
344
321
  }
345
- // Scan assembly refnames for query matches, resolving aliases (e.g. 'contigB')
346
- // to the canonical refname ('ctgB') so the dropdown shows the name that matches
347
- // the FASTA / displayed regions. allRefNames can hold ~10^6 entries, so stop
348
- // once enough unique canonical hits accumulate rather than lowercasing and
349
- // scanning the entire list on every keystroke.
350
- function searchRefNames(assembly, queryString, searchType) {
351
- const q = queryString.toLowerCase();
352
- // canonical name -> whether any alias of it matched the query exactly, so
353
- // "contigb" is an exact hit on ctgB even though the canonical name is not
354
- // the query. Same collapsing as before, now carrying the flag through it
355
- const canonicalHits = new Map();
356
- for (const ref of assembly.allRefNames ?? []) {
357
- const lower = ref.toLowerCase();
358
- const exact = lower === q;
359
- const isMatch = searchType === 'exact' ? exact : lower.startsWith(q);
360
- if (isMatch) {
361
- const canonical = assembly.getCanonicalRefName2(ref);
362
- canonicalHits.set(canonical, exact || !!canonicalHits.get(canonical));
363
- if (canonicalHits.size >= MAX_REFNAME_HITS) {
364
- break;
365
- }
366
- }
322
+ /**
323
+ * The search box's hits for a query: refNames and text-index results, merged.
324
+ * Pass the session and the function finds the index manager and the assembly
325
+ * itself; passing `textSearchManager` and `assembly` is the older form.
326
+ */
327
+ export async function fetchResults(args) {
328
+ if ('session' in args) {
329
+ const { session, ...rest } = args;
330
+ const assembly = await session.assemblyManager
331
+ .waitForAssembly(args.assemblyName)
332
+ .catch(() => undefined);
333
+ return (await searchNames({
334
+ ...rest,
335
+ textSearchManager: session.textSearchManager,
336
+ assembly,
337
+ })).results;
338
+ }
339
+ else {
340
+ return (await searchNames(args)).results;
367
341
  }
368
- return [...canonicalHits].map(([r, exact]) => new RefSequenceResult({ label: r, refName: r, exact }));
369
342
  }
370
- // splits on the last instance of a character
371
- export function splitLast(str, split) {
372
- const i = str.lastIndexOf(split);
373
- return i === -1 ? [str, ''] : [str.slice(0, i), str.slice(i + 1)];
343
+ // A miss says when an index could not answer: with none answering it is a
344
+ // failure, and with some answering it is still a miss, naming what failed
345
+ export function nameNotFound(input, assemblyName, { indexCount, failures }) {
346
+ const [failure] = failures;
347
+ const miss = `No results found for "${input}"`;
348
+ return failure === undefined
349
+ ? new SearchResultsNotFoundError(indexCount ? miss : `${miss}: ${assemblyName} has no text search index`)
350
+ : failures.length === indexCount
351
+ ? new Error(`Searching for "${input}" failed: ${failure}`, {
352
+ cause: failure,
353
+ })
354
+ : new SearchResultsNotFoundError(`${miss} (a search index failed: ${failure})`, { cause: failure });
355
+ }
356
+ // The refNames a query names, through the same ranking the dropdown's fallback
357
+ // list uses. A refName the query is only a substring of is the weakest kind of
358
+ // hit, so it goes behind the text-search results rather than ahead of them
359
+ function searchRefNames(assembly, queryString, searchType) {
360
+ const hits = rankRefNames(assembly, queryString, MAX_REFNAME_HITS)
361
+ .filter(({ tier }) => searchType !== 'exact' || tier === 'exact')
362
+ .map(({ refName, tier }) => ({
363
+ tier,
364
+ result: new RefSequenceResult({
365
+ label: refName,
366
+ refName,
367
+ exact: tier === 'exact',
368
+ }),
369
+ }));
370
+ return {
371
+ strong: hits.filter(h => h.tier !== 'substring').map(h => h.result),
372
+ weak: hits.filter(h => h.tier === 'substring').map(h => h.result),
373
+ };
374
374
  }
@@ -4,7 +4,7 @@
4
4
  * single view reads one coordinate per edge and the multi-level rubberband reads
5
5
  * one per level.
6
6
  */
7
- export default function RubberbandSpan({ left, width, viewWidth, stickyTop, leftLabel, rightLabel, size, }: {
7
+ declare const RubberbandSpan: ({ left, width, viewWidth, stickyTop, leftLabel, rightLabel, size, }: {
8
8
  left: number;
9
9
  width: number;
10
10
  viewWidth: number;
@@ -12,4 +12,5 @@ export default function RubberbandSpan({ left, width, viewWidth, stickyTop, left
12
12
  leftLabel: React.ReactNode;
13
13
  rightLabel: React.ReactNode;
14
14
  size?: React.ReactNode;
15
- }): import("react").JSX.Element;
15
+ }) => import("react").JSX.Element;
16
+ export default RubberbandSpan;
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { alpha } from '@jbrowse/core/ui/palette';
2
3
  import { makeStyles } from '@jbrowse/core/util/tss-react';
3
- import { Typography, alpha } from '@mui/material';
4
+ import { Typography } from '@mui/material';
5
+ import { observer } from 'mobx-react';
4
6
  import { LABEL_WIDTH, SpanEdgeLabels } from './coordLabels.js';
5
7
  const useStyles = makeStyles()(theme => ({
6
8
  rubberband: {
@@ -22,7 +24,7 @@ const useStyles = makeStyles()(theme => ({
22
24
  * single view reads one coordinate per edge and the multi-level rubberband reads
23
25
  * one per level.
24
26
  */
25
- export default function RubberbandSpan({ left, width, viewWidth, stickyTop, leftLabel, rightLabel, size, }) {
27
+ const RubberbandSpan = observer(function RubberbandSpan({ left, width, viewWidth, stickyTop, leftLabel, rightLabel, size, }) {
26
28
  const { classes } = useStyles();
27
29
  // a label only moves inside its edge when the span can hold it, so a sliver of
28
30
  // a selection at a view border keeps both labels outside and takes the clip
@@ -30,4 +32,5 @@ export default function RubberbandSpan({ left, width, viewWidth, stickyTop, left
30
32
  return (_jsxs("div", { className: classes.rubberband, style: { transform: `translateX(${left}px)`, width }, children: [_jsx(SpanEdgeLabels, { stickyTop: stickyTop, left: leftLabel, right: rightLabel, insideLeft: fitsInside && left < LABEL_WIDTH, insideRight: fitsInside && left + width > viewWidth - LABEL_WIDTH }), size ? (_jsx(Typography, { variant: "h6", className: classes.rubberbandText, style: stickyTop === undefined
31
33
  ? undefined
32
34
  : { position: 'sticky', top: stickyTop }, children: size })) : null] }));
33
- }
35
+ });
36
+ export default RubberbandSpan;
@@ -1,4 +1,11 @@
1
1
  export declare const LABEL_WIDTH = 100;
2
+ /** A red vertical line at `coordX` with `children` labelled above it. */
3
+ export declare const VerticalGuideLine: ({ coordX, viewWidth, stickyTop, children, }: {
4
+ coordX: number;
5
+ viewWidth: number;
6
+ stickyTop: string | undefined;
7
+ children: React.ReactNode;
8
+ }) => import("react").JSX.Element;
2
9
  /**
3
10
  * The bp coordinate under a vertical guide, drawn just above the top of the
4
11
  * guide line so it covers neither the line nor the cursor hovering the scalebar
@@ -19,22 +26,22 @@ export declare const LABEL_WIDTH = 100;
19
26
  * measured once per hover, not per mousemove — only `coordX` changes as the
20
27
  * mouse moves, and that needs no measurement.
21
28
  */
22
- export declare function GuideLabel({ coordX, viewWidth, stickyTop, children, }: {
29
+ export declare const GuideLabel: ({ coordX, viewWidth, stickyTop, children, }: {
23
30
  coordX: number;
24
31
  viewWidth: number;
25
32
  stickyTop: string | undefined;
26
33
  children: React.ReactNode;
27
- }): import("react").JSX.Element;
34
+ }) => import("react").JSX.Element;
28
35
  /**
29
36
  * The bp coordinates of a rubberband selection, one label just outside each edge
30
37
  * of the span. Renders inside the span, so the edges are its own box. A label
31
38
  * with no room outside its edge flips inside instead, which is why the span it
32
39
  * has to fit in is a prop.
33
40
  */
34
- export declare function SpanEdgeLabels({ stickyTop, left, right, insideLeft, insideRight, }: {
41
+ export declare const SpanEdgeLabels: ({ stickyTop, left, right, insideLeft, insideRight, }: {
35
42
  stickyTop: string | undefined;
36
43
  left: React.ReactNode;
37
44
  right: React.ReactNode;
38
45
  insideLeft: boolean;
39
46
  insideRight: boolean;
40
- }): import("react").JSX.Element;
47
+ }) => import("react").JSX.Element;
@@ -1,8 +1,9 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ import { alpha } from '@jbrowse/core/ui/palette';
3
4
  import { MUI_TOOLTIP_Z_INDEX } from '@jbrowse/core/ui/zIndexes';
4
5
  import { makeStyles } from '@jbrowse/core/util/tss-react';
5
- import { alpha } from '@mui/material';
6
+ import { observer } from 'mobx-react';
6
7
  import { createPortal } from 'react-dom';
7
8
  // roughly how wide a coordinate label is. the guide clamps itself half of this
8
9
  // from either border, and a rubberband label that would hang off a border goes
@@ -51,7 +52,21 @@ const useStyles = makeStyles()(theme => ({
51
52
  insideRightEdge: {
52
53
  right: 0,
53
54
  },
55
+ guideLine: {
56
+ pointerEvents: 'none',
57
+ height: '100%',
58
+ width: 1,
59
+ position: 'absolute',
60
+ left: 0,
61
+ background: 'red',
62
+ zIndex: 1001,
63
+ },
54
64
  }));
65
+ /** A red vertical line at `coordX` with `children` labelled above it. */
66
+ export const VerticalGuideLine = observer(function VerticalGuideLine({ coordX, viewWidth, stickyTop, children, }) {
67
+ const { classes } = useStyles();
68
+ return (_jsxs(_Fragment, { children: [_jsx(GuideLabel, { coordX: coordX, viewWidth: viewWidth, stickyTop: stickyTop, children: children }), _jsx("div", { className: classes.guideLine, style: { transform: `translateX(${coordX}px)` } })] }));
69
+ });
55
70
  function anchorStyle(stickyTop) {
56
71
  return stickyTop === undefined
57
72
  ? { position: 'relative' }
@@ -77,7 +92,7 @@ function anchorStyle(stickyTop) {
77
92
  * measured once per hover, not per mousemove — only `coordX` changes as the
78
93
  * mouse moves, and that needs no measurement.
79
94
  */
80
- export function GuideLabel({ coordX, viewWidth, stickyTop, children, }) {
95
+ export const GuideLabel = observer(function GuideLabel({ coordX, viewWidth, stickyTop, children, }) {
81
96
  const { classes, cx } = useStyles();
82
97
  const anchorRef = useRef(null);
83
98
  const [anchorPos, setAnchorPos] = useState();
@@ -106,14 +121,14 @@ export function GuideLabel({ coordX, viewWidth, stickyTop, children, }) {
106
121
  bottom: anchorPos.viewportHeight - anchorPos.top + LABEL_GAP,
107
122
  }, children: children }), document.body)
108
123
  : null] }));
109
- }
124
+ });
110
125
  /**
111
126
  * The bp coordinates of a rubberband selection, one label just outside each edge
112
127
  * of the span. Renders inside the span, so the edges are its own box. A label
113
128
  * with no room outside its edge flips inside instead, which is why the span it
114
129
  * has to fit in is a prop.
115
130
  */
116
- export function SpanEdgeLabels({ stickyTop, left, right, insideLeft, insideRight, }) {
131
+ export const SpanEdgeLabels = observer(function SpanEdgeLabels({ stickyTop, left, right, insideLeft, insideRight, }) {
117
132
  const { classes, cx } = useStyles();
118
133
  return (_jsxs("div", { className: classes.anchor, style: anchorStyle(stickyTop), children: [_jsx("div", { className: cx(classes.label, classes.spanLabel, insideLeft ? classes.insideLeftEdge : classes.leftOfEdge), children: left }), _jsx("div", { className: cx(classes.label, classes.spanLabel, insideRight ? classes.insideRightEdge : classes.rightOfEdge), children: right })] }));
119
- }
134
+ });
@@ -0,0 +1,18 @@
1
+ import type React from 'react';
2
+ export interface DragEnd {
3
+ startX: number;
4
+ endX: number;
5
+ isClick: boolean;
6
+ clientX: number;
7
+ clientY: number;
8
+ }
9
+ /**
10
+ * Follow a press that began at `startX` (px from `ref`'s left edge) through
11
+ * window events until it ends, so the pointer can leave the element mid-drag.
12
+ * A release within 3px of the press is a click. Undefined `startX` is no drag.
13
+ */
14
+ export declare function useWindowDrag(ref: React.RefObject<HTMLElement | null>, startX: number | undefined, handlers: {
15
+ onMove: (x: number) => void;
16
+ onEnd: (end: DragEnd) => void;
17
+ onCancel: () => void;
18
+ }): void;
@@ -0,0 +1,47 @@
1
+ import { useEffect } from 'react';
2
+ import { useEventCallback } from '@jbrowse/core/util/useEventCallback';
3
+ /**
4
+ * Follow a press that began at `startX` (px from `ref`'s left edge) through
5
+ * window events until it ends, so the pointer can leave the element mid-drag.
6
+ * A release within 3px of the press is a click. Undefined `startX` is no drag.
7
+ */
8
+ export function useWindowDrag(ref, startX, handlers) {
9
+ const onMove = useEventCallback(handlers.onMove);
10
+ const onEnd = useEventCallback(handlers.onEnd);
11
+ const onCancel = useEventCallback(handlers.onCancel);
12
+ useEffect(() => {
13
+ const el = ref.current;
14
+ if (startX === undefined || !el) {
15
+ return;
16
+ }
17
+ const { left } = el.getBoundingClientRect();
18
+ const mouseMove = (event) => {
19
+ onMove(event.clientX - left);
20
+ };
21
+ const mouseUp = ({ button, clientX, clientY }) => {
22
+ if (button === 0) {
23
+ const endX = clientX - left;
24
+ onEnd({
25
+ startX,
26
+ endX,
27
+ isClick: Math.abs(endX - startX) <= 3,
28
+ clientX,
29
+ clientY,
30
+ });
31
+ }
32
+ };
33
+ const keyDown = (event) => {
34
+ if (event.key === 'Escape') {
35
+ onCancel();
36
+ }
37
+ };
38
+ window.addEventListener('mousemove', mouseMove);
39
+ window.addEventListener('mouseup', mouseUp);
40
+ window.addEventListener('keydown', keyDown);
41
+ return () => {
42
+ window.removeEventListener('mousemove', mouseMove);
43
+ window.removeEventListener('mouseup', mouseUp);
44
+ window.removeEventListener('keydown', keyDown);
45
+ };
46
+ }, [ref, startX, onMove, onEnd, onCancel]);
47
+ }