Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(663)

Side by Side Diff: pkg/dartdoc/search.dart

Issue 10829361: 'Find-as-you-type'-search in dartdoc/apidoc. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: CSS bug fixed Created 8 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 /**
6 * [SearchText] represent the search field text. The text is viewed in three
7 * ways: [text] holds the original search text, used for performing
8 * case-sensitive matches, [lowerCase] holds the lower-case search text, used
9 * for performing case-insenstive matches, [camelCase] holds a camel-case
10 * interpretation of the search text, used to order matches in camel-case.
11 */
12 class SearchText {
13 final String text;
14 final String lowerCase;
15 final String camelCase;
16
17 SearchText(String searchText)
18 : text = searchText,
19 lowerCase = searchText.toLowerCase(),
20 camelCase = searchText.isEmpty() ? ''
21 : '${searchText.substring(0, 1).toUpperCase()}'
22 '${searchText.substring(1)}';
23
24 int get length() => text.length;
25
26 bool isEmpty() => length == 0;
27 }
28
29 /**
30 * [StringMatch] represents the case-insensitive matching of [searchText] as a
31 * substring within a [text]. The matched [text] is split into the [prefixText],
Lasse Reichstein Nielsen 2012/08/20 13:07:58 What variable does [text] refer to?
Johnni Winther 2012/08/23 10:19:50 To the property, that is the getter 'text'.
32 * [infixText], and [suffixText], where [infixText] is the part matching the
Lasse Reichstein Nielsen 2012/08/20 13:07:58 Why not just call it "matchText"? And perhaps "pre
Johnni Winther 2012/08/23 10:19:50 They are used extensively so I prefer creating the
33 * [searchText].
34 */
35 class StringMatch {
36 final SearchText searchText;
37 final String prefixText;
38 final String infixText;
39 final String suffixText;
40
41 StringMatch(this.searchText,
42 this.prefixText, this.infixText, this.suffixText);
43
44 /**
45 * Returns the HTML representation of the match.
46 */
47 String toHtml() {
48 return '$prefixText'
49 '<span class="drop-down-link-highlight">$infixText</span>'
50 '$suffixText';
51 }
52
53 /**
54 * The text in which a substring matches [searchText].
55 */
56 String get text() =>
57 '$prefixText$infixText$suffixText';
58
59 /**
60 * Is `true` iff [searchText] matches the full [text] case-sensitively.
Lasse Reichstein Nielsen 2012/08/20 13:07:58 Drop the quoting around 'true'. If you want someth
Johnni Winther 2012/08/23 10:19:50 Done.
61 */
62 bool get isFullMatch() => text == searchText.text;
63
64 /**
65 * Is `true` iff [searchText] matches a substring of [text] case-sensitively.
66 */
67 bool get isExactMatch() => infixText == searchText.text;
68
69 /**
70 * Is `true` iff [searchText] matches a substring of [text] when [searchText]
71 * is interpreted as camel case.
72 */
73 bool get isCamelCaseMatch() => infixText == searchText.camelCase;
74 }
75
76 /**
77 * [Result] represents a match of the search text on a library, type or member.
78 */
79 class Result {
80 final StringMatch prefix;
81 final StringMatch match;
82
83 final String library;
84 final String type;
85 final String args;
86 final String kind;
87 final String url;
88
89 TableRowElement row;
90
91 Result(this.match, this.kind, this.url,
92 [this.library, this.type, String args, this.prefix])
93 : this.args = args != null ? '&lt;$args&gt;' : '';
94
95 bool get isTopLevel() => prefix == null && type == null;
96
97 void addRow(TableElement table) {
98 if (row != null) return;
99
100 clickHandler(Event event) {
101 window.location.href = url;
102 hideDropDown();
103 }
104
105 row = table.insertRow(table.rows.length);
106 row.classes.add('drop-down-link-tr');
107 row.on.mouseDown.add((event) => hideDropDownSuspend = true);
108 row.on.click.add(clickHandler);
109 row.on.mouseUp.add((event) => hideDropDownSuspend = false);
110 var sb = new StringBuffer();
111 sb.add('<td class="drop-down-link-td">');
112 sb.add('<table class="drop-down-table"><tr><td colspan="2">');
113 if (kind == GETTER) {
114 sb.add('get ');
115 } else if (kind == SETTER) {
116 sb.add('set ');
117 }
118 sb.add(match.toHtml());
119 if (kind == CLASS || kind == INTERFACE || kind == TYPEDEF) {
120 sb.add(args);
121 } else if (kind == CONSTRUCTOR || kind == METHOD) {
122 sb.add('(...)');
123 }
124 sb.add('</td></tr><tr><td class="drop-down-link-kind">');
125 sb.add(kindToString(kind));
126 if (prefix != null) {
127 sb.add(' in ');
128 sb.add(prefix.toHtml());
129 sb.add(args);
130 } else if (type != null) {
131 sb.add(' in ');
132 sb.add(type);
133 sb.add(args);
134 }
135
136 sb.add('</td><td class="drop-down-link-library">');
137 if (library != null) {
138 sb.add('library $library');
139 }
140 sb.add('</td></tr></table></td>');
141 row.innerHTML = sb.toString();
142 }
143 }
144
145 /**
146 * Creates a [StringMatch] object for [text] if a substring matches
147 * [searchText], or returns [: null :] if no match is found.
148 */
149 StringMatch obtainMatch(SearchText searchText, String text) {
150 if (searchText.isEmpty()) {
151 return new StringMatch(searchText, '', '', text);
152 }
153 int offset = text.toLowerCase().indexOf(searchText.lowerCase);
154 if (offset != -1) {
155 String prefixText = text.substring(0, offset);
156 String infixText = text.substring(offset, offset+searchText.length);
Lasse Reichstein Nielsen 2012/08/20 13:07:58 Spaces around '+'. Ditto next line.
Johnni Winther 2012/08/23 10:19:50 Done.
157 String suffixText = text.substring(offset+searchText.length);
158 return new StringMatch(searchText, prefixText, infixText, suffixText);
159 }
160 return null;
161 }
162
163 int compareBools(bool a, bool b) {
Lasse Reichstein Nielsen 2012/08/20 13:07:58 Document that it considers true bigger than false.
Johnni Winther 2012/08/23 10:19:50 Done.
164 if (a && !b) return -1;
165 else if (!a && b) return 1;
166 else return 0;
167 }
168
169 int compareInts(int a, int b) {
170 if (a < b) return -1;
171 else if (a > b) return 1;
172 else return 0;
Lasse Reichstein Nielsen 2012/08/20 13:07:58 Aren't numbers comparable? I.e., a.compareTo(b)?
Johnni Winther 2012/08/23 10:19:50 Done.
173 }
174
175 int resultComparator(Result a, Result b) {
Lasse Reichstein Nielsen 2012/08/20 13:07:58 That's a lot of work to do in a sort comparison fu
Johnni Winther 2012/08/23 10:19:50 Yes, but we have a lot of situations to handle!
176 // Favor top level entities.
177 int result = compareBools(a.isTopLevel, b.isTopLevel);
178 if (result != 0) return result;
179
180 if (a.prefix != null && b.prefix != null) {
181 // Favor full prefix matches.
182 result = compareBools(a.prefix.isFullMatch, b.prefix.isFullMatch);
183 if (result != 0) return result;
184 }
185
186 // Favor matches in the start.
187 result = compareBools(a.match.prefixText.isEmpty(),
188 b.match.prefixText.isEmpty());
189 if (result != 0) return result;
190
191 // Favor exact case-sensitive matches.
192 result = compareBools(a.match.isExactMatch, b.match.isExactMatch);
193 if (result != 0) return result;
194
195 // Favor matches that do not break camel-case.
196 result = compareBools(a.match.isCamelCaseMatch, b.match.isCamelCaseMatch);
197 if (result != 0) return result;
198
199 // Favor matches close to the begining.
200 result = compareInts(a.match.prefixText.length,
201 b.match.prefixText.length);
202 if (result != 0) return result;
203
204 if (a.type != null && b.type != null) {
205 // Favor short type names over long.
206 result = compareInts(a.type.length, b.type.length);
207 if (result != 0) return result;
208
209 // Sort type alphabetically.
210 result = a.type.toLowerCase().compareTo(b.type.toLowerCase());
Lasse Reichstein Nielsen 2012/08/20 13:07:58 This is particularly worrisome. You can have a qua
Johnni Winther 2012/08/23 10:19:50 Yes, I looked for that!
211 if (result != 0) return result;
212 }
213
214 // Sort match alphabetically.
215 return a.match.text.toLowerCase().compareTo(b.match.text.toLowerCase());
216 }
217
218 List libraryList;
219 InputElement searchInput;
220 DivElement dropdown;
Lasse Reichstein Nielsen 2012/08/20 13:07:58 Is this a global variable? That isn't even private
Johnni Winther 2012/08/23 10:19:50 This is a web page and the DOM is global!
221
222 /**
223 * Update the search drop down based on the current search text.
224 */
225 updateDropDown(Event event) {
226 if (libraryList == null) return;
227 if (searchInput == null) return;
228 if (dropdown == null) return;
229
230 var results = <Result>[];
231 String text = searchInput.value;
232 if (text == currentSearchText) {
233 return;
234 }
235 if (text.isEmpty()) {
236 updateResults(text, results);
237 hideDropDown();
238 return;
239 }
240 if (text.contains('.')) {
241 // Search type members.
242 String typeText = text.substring(0, text.indexOf('.'));
243 String memberText = text.substring(text.indexOf('.') + 1);
244
245 if (typeText.isEmpty() && memberText.isEmpty()) {
246 // Don't search on '.'.
247 } else if (typeText.isEmpty()) {
248 // Search text is of the form '.id' => Look up members.
249 var searchText = new SearchText(memberText);
250 for (Map library in libraryList) {
251 String libraryName = library[NAME];
252 if (library.containsKey(TYPES)) {
253 for (Map type in library[TYPES]) {
254 String typeName = type[NAME];
255 if (type.containsKey(MEMBERS)) {
256 for (Map member in type[MEMBERS]) {
257 StringMatch memberMatch = obtainMatch(searchText, member[NAME]);
258 if (memberMatch != null) {
259 results.add(new Result(memberMatch, member[KIND],
260 getTypeMemberUrl(libraryName, typeName, member),
261 library: libraryName, type: typeName, args: type[ARGS]));
262 }
263 }
264 }
265 }
266 }
267 }
268 } else if (memberText.isEmpty()) {
269 // Search text is of the form 'Type.' => Look up members in 'Type'.
270 var searchText = new SearchText(typeText);
271 var emptyText = new SearchText(memberText);
272 for (Map library in libraryList) {
273 String libraryName = library[NAME];
274 if (library.containsKey(TYPES)) {
275 for (Map type in library[TYPES]) {
276 String typeName = type[NAME];
277 StringMatch typeMatch = obtainMatch(searchText, typeName);
278 if (typeMatch != null) {
279 if (type.containsKey(MEMBERS)) {
280 for (Map member in type[MEMBERS]) {
281 StringMatch memberMatch = obtainMatch(emptyText, member[NAME]) ;
Lasse Reichstein Nielsen 2012/08/20 13:07:58 Line too long.
Johnni Winther 2012/08/23 10:19:50 Done.
282 results.add(new Result(memberMatch, member[KIND],
283 getTypeMemberUrl(libraryName, typeName, member),
284 library: libraryName, prefix: typeMatch));
285 }
286 }
287 }
288 }
289 }
290 }
291 } else {
292 // Search text is of the form 'Type.id' => Look up member 'id' in 'Type'.
293 var searchText = new SearchText(text);
294 var typeSearchText = new SearchText(typeText);
295 var memberSearchText = new SearchText(memberText);
296 for (Map library in libraryList) {
297 String libraryName = library[NAME];
298 if (library.containsKey(TYPES)) {
299 for (Map type in library[TYPES]) {
300 String typeName = type[NAME];
301 StringMatch typeMatch = obtainMatch(typeSearchText, typeName);
302 if (typeMatch != null) {
303 if (type.containsKey(MEMBERS)) {
304 for (Map member in type[MEMBERS]) {
305 StringMatch constructorMatch = obtainMatch(searchText,
306 member[NAME]);
307 if (constructorMatch != null) {
308 results.add(new Result(constructorMatch, member[KIND],
309 getTypeMemberUrl(libraryName, typeName, member),
310 library: libraryName));
311 } else {
312 StringMatch memberMatch = obtainMatch(memberSearchText,
313 member[NAME]);
314 if (memberMatch != null) {
315 results.add(new Result(memberMatch, member[KIND],
316 getTypeMemberUrl(libraryName, typeName, member),
317 library: libraryName, prefix: typeMatch,
318 args: type[ARGS]));
319 }
320 }
321 }
322 }
323 }
324 }
Lasse Reichstein Nielsen 2012/08/20 13:07:58 These huge blobs of complex code would be great as
Johnni Winther 2012/08/23 10:19:50 Refactored into several methods.
325 }
326 }
327 }
328 } else {
329 // Search all entities.
330 var searchText = new SearchText(text);
331 for (Map library in libraryList) {
332 String libraryName = library[NAME];
333 StringMatch libraryMatch = obtainMatch(searchText, libraryName);
334 if (libraryMatch != null) {
335 results.add(new Result(libraryMatch, LIBRARY,
336 getLibraryUrl(libraryName)));
337 }
338 if (library.containsKey(MEMBERS)) {
339 for (Map member in library[MEMBERS]) {
340 StringMatch memberMatch = obtainMatch(searchText, member[NAME]);
341 if (memberMatch != null) {
342 results.add(new Result(memberMatch, member[KIND],
343 getLibraryMemberUrl(libraryName, member),
344 library: libraryName));
345 }
346 }
347 }
348 if (library.containsKey(TYPES)) {
349 for (Map type in library[TYPES]) {
350 String typeName = type[NAME];
351 StringMatch typeMatch = obtainMatch(searchText, typeName);
352 if (typeMatch != null) {
353 results.add(new Result(typeMatch, type[KIND],
354 getTypeUrl(libraryName, type),
355 library: libraryName, args: type[ARGS]));
356 }
357 if (type.containsKey(MEMBERS)) {
358 for (Map member in type[MEMBERS]) {
359 StringMatch memberMatch = obtainMatch(searchText, member[NAME]);
360 if (memberMatch != null) {
361 results.add(new Result(memberMatch, member[KIND],
362 getTypeMemberUrl(libraryName, typeName, member),
363 library: libraryName, type: typeName, args: type[ARGS]));
364 }
365 }
366 }
367 }
368 }
369 }
370 }
371 var elements = <Element>[];
372 var table = new TableElement();
373 table.classes.add('drop-down-table');
374 elements.add(table);
375
376 if (results.isEmpty()) {
377 var row = table.insertRow(0);
378 row.innerHTML = "<tr><td>No matches found for '$text'.</td></tr>";
379 } else {
380 results.sort(resultComparator);
381
382 var count = 0;
383 for (Result result in results) {
384 result.addRow(table);
385 if (++count >= 10) {
386 break;
387 }
388 }
389 if (results.length >= 10) {
390 var row = table.insertRow(table.rows.length);
391 row.innerHTML = '<tr><td>+ ${results.length-10} more.</td></tr>';
392 results = results.getRange(0, 10);
393 }
394 }
395 dropdown.elements = elements;
396 updateResults(text, results);
397 showDropDown();
398 }
399
400 String currentSearchText;
401 Result _currentResult;
402 List<Result> currentResults = const <Result>[];
403
404 void updateResults(String searchText, List<Result> results) {
405 currentSearchText = searchText;
406 currentResults = results;
407 if (currentResults.isEmpty()) {
408 _currentResultIndex = -1;
409 currentResult = null;
410 } else {
411 _currentResultIndex = 0;
412 currentResult = currentResults[0];
413 }
414 }
415
416 int _currentResultIndex;
417
418 void set currentResultIndex(int index) {
419 if (index < -1) {
420 return;
421 }
422 if (index >= currentResults.length) {
423 return;
424 }
425 if (index != _currentResultIndex) {
426 _currentResultIndex = index;
427 if (index >= 0) {
428 currentResult = currentResults[_currentResultIndex];
429 } else {
430 currentResult = null;
431 }
432 }
433 }
434
435 int get currentResultIndex() => _currentResultIndex;
436
437 void set currentResult(Result result) {
438 if (_currentResult != result) {
439 if (_currentResult != null) {
440 _currentResult.row.classes.remove('drop-down-link-select');
441 }
442 _currentResult = result;
443 if (_currentResult != null) {
444 _currentResult.row.classes.add('drop-down-link-select');
445 }
446 }
447 }
448
449 Result get currentResult() => _currentResult;
450
451 /**
452 * Navigate the search drop down using up/down inside the search field. Follow
453 * the result link on enter.
454 */
455 void handleUpDown(KeyboardEvent event) {
456 if (event.keyIdentifier == KeyName.UP) {
457 currentResultIndex--;
458 event.preventDefault();
459 } else if (event.keyIdentifier == KeyName.DOWN) {
460 currentResultIndex++;
461 event.preventDefault();
462 } else if (event.keyIdentifier == KeyName.ENTER) {
463 if (currentResult != null) {
464 window.location.href = currentResult.url;
465 event.preventDefault();
466 hideDropDown();
467 }
468 }
469 }
470
471 /** Show the search drop down unless there are no current results. */
472 void showDropDown() {
473 if (currentResults.isEmpty()) {
474 hideDropDown();
475 } else {
476 dropdown.style.visibility = 'visible';
477 }
478 }
479
480 /** Used to prevent hiding the drop down when it is clicked. */
481 bool hideDropDownSuspend = false;
482
483 /** Hide the search drop down unless suspended. */
484 void hideDropDown() {
485 if (hideDropDownSuspend) return;
486
487 dropdown.style.visibility = 'hidden';
488 }
489
490 /** Activate search on Ctrl+F and F3. */
491 void shortcutHandler(KeyboardEvent event) {
492 if (event.keyCode == 70/*F*/ && event.ctrlKey ||
Lasse Reichstein Nielsen 2012/08/20 13:07:58 I prefer hex for keycodes. I.e., 0x46.
Johnni Winther 2012/08/23 10:19:50 Done.
493 event.keyIdentifier == KeyName.F3) {
494 searchInput.focus();
495 event.preventDefault();
496 }
497 }
498
499 /** Setup search hooks. */
500 void setupSearch(var libraries) {
501 libraryList = libraries;
502 searchInput = query('#q');
503 dropdown = query('#drop-down');
504
505 searchInput.on.keyDown.add(handleUpDown);
506 searchInput.on.keyUp.add(updateDropDown);
507 searchInput.on.change.add(updateDropDown);
508 searchInput.on.reset.add(updateDropDown);
509 searchInput.on.focus.add((event) => showDropDown());
510 searchInput.on.blur.add((event) => hideDropDown());
511 window.on.keyDown.add(shortcutHandler);
512 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698