OLD | NEW |
| (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]. | |
32 */ | |
33 class StringMatch { | |
34 final SearchText searchText; | |
35 final String text; | |
36 final int matchOffset; | |
37 final int matchEnd; | |
38 | |
39 StringMatch(this.searchText, | |
40 this.text, this.matchOffset, this.matchEnd); | |
41 | |
42 /** | |
43 * Returns the HTML representation of the match. | |
44 */ | |
45 String toHtml() { | |
46 return '${text.substring(0, matchOffset)}' | |
47 '<span class="drop-down-link-highlight">$matchText</span>' | |
48 '${text.substring(matchEnd)}'; | |
49 } | |
50 | |
51 String get matchText => | |
52 text.substring(matchOffset, matchEnd); | |
53 | |
54 /** | |
55 * Is [:true:] iff [searchText] matches the full [text] case-sensitively. | |
56 */ | |
57 bool get isFullMatch => text == searchText.text; | |
58 | |
59 /** | |
60 * Is [:true:] iff [searchText] matches a substring of [text] | |
61 * case-sensitively. | |
62 */ | |
63 bool get isExactMatch => matchText == searchText.text; | |
64 | |
65 /** | |
66 * Is [:true:] iff [searchText] matches a substring of [text] when | |
67 * [searchText] is interpreted as camel case. | |
68 */ | |
69 bool get isCamelCaseMatch => matchText == searchText.camelCase; | |
70 } | |
71 | |
72 /** | |
73 * [Result] represents a match of the search text on a library, type or member. | |
74 */ | |
75 class Result { | |
76 final StringMatch prefix; | |
77 final StringMatch match; | |
78 | |
79 final String library; | |
80 final String type; | |
81 final String args; | |
82 final String kind; | |
83 final String url; | |
84 | |
85 TableRowElement row; | |
86 | |
87 Result(this.match, this.kind, this.url, | |
88 [this.library, this.type, String args, this.prefix]) | |
89 : this.args = args != null ? '<$args>' : ''; | |
90 | |
91 bool get isTopLevel => prefix == null && type == null; | |
92 | |
93 void addRow(TableElement table) { | |
94 if (row != null) return; | |
95 | |
96 clickHandler(Event event) { | |
97 window.location.href = url; | |
98 hideDropDown(); | |
99 } | |
100 | |
101 row = table.insertRow(table.rows.length); | |
102 row.classes.add('drop-down-link-tr'); | |
103 row.on.mouseDown.add((event) => hideDropDownSuspend = true); | |
104 row.on.click.add(clickHandler); | |
105 row.on.mouseUp.add((event) => hideDropDownSuspend = false); | |
106 var sb = new StringBuffer(); | |
107 sb.add('<td class="drop-down-link-td">'); | |
108 sb.add('<table class="drop-down-table"><tr><td colspan="2">'); | |
109 if (kind == GETTER) { | |
110 sb.add('get '); | |
111 } else if (kind == SETTER) { | |
112 sb.add('set '); | |
113 } | |
114 sb.add(match.toHtml()); | |
115 if (kind == CLASS || kind == INTERFACE || kind == TYPEDEF) { | |
116 sb.add(args); | |
117 } else if (kind == CONSTRUCTOR || kind == METHOD) { | |
118 sb.add('(...)'); | |
119 } | |
120 sb.add('</td></tr><tr><td class="drop-down-link-kind">'); | |
121 sb.add(kindToString(kind)); | |
122 if (prefix != null) { | |
123 sb.add(' in '); | |
124 sb.add(prefix.toHtml()); | |
125 sb.add(args); | |
126 } else if (type != null) { | |
127 sb.add(' in '); | |
128 sb.add(type); | |
129 sb.add(args); | |
130 } | |
131 | |
132 sb.add('</td><td class="drop-down-link-library">'); | |
133 if (library != null) { | |
134 sb.add('library $library'); | |
135 } | |
136 sb.add('</td></tr></table></td>'); | |
137 row.innerHTML = sb.toString(); | |
138 } | |
139 } | |
140 | |
141 /** | |
142 * Creates a [StringMatch] object for [text] if a substring matches | |
143 * [searchText], or returns [: null :] if no match is found. | |
144 */ | |
145 StringMatch obtainMatch(SearchText searchText, String text) { | |
146 if (searchText.isEmpty()) { | |
147 return new StringMatch(searchText, text, 0, 0); | |
148 } | |
149 int offset = text.toLowerCase().indexOf(searchText.lowerCase); | |
150 if (offset != -1) { | |
151 return new StringMatch(searchText, text, | |
152 offset, offset + searchText.length); | |
153 } | |
154 return null; | |
155 } | |
156 | |
157 /** | |
158 * Compares [a] and [b], regarding [:true:] smaller than [:false:]. | |
159 * | |
160 * [:null:]-values are not handled. | |
161 */ | |
162 int compareBools(bool a, bool b) { | |
163 if (a == b) return 0; | |
164 return a ? -1 : 1; | |
165 } | |
166 | |
167 /** | |
168 * Used to sort the search results heuristically to show the more relevant match | |
169 * in the top of the dropdown. | |
170 */ | |
171 int resultComparator(Result a, Result b) { | |
172 // Favor top level entities. | |
173 int result = compareBools(a.isTopLevel, b.isTopLevel); | |
174 if (result != 0) return result; | |
175 | |
176 if (a.prefix != null && b.prefix != null) { | |
177 // Favor full prefix matches. | |
178 result = compareBools(a.prefix.isFullMatch, b.prefix.isFullMatch); | |
179 if (result != 0) return result; | |
180 } | |
181 | |
182 // Favor matches in the start. | |
183 result = compareBools(a.match.matchOffset == 0, | |
184 b.match.matchOffset == 0); | |
185 if (result != 0) return result; | |
186 | |
187 // Favor matches to the end. For example, prefer 'cancel' over 'cancelable' | |
188 result = compareBools(a.match.matchEnd == a.match.text.length, | |
189 b.match.matchEnd == b.match.text.length); | |
190 if (result != 0) return result; | |
191 | |
192 // Favor exact case-sensitive matches. | |
193 result = compareBools(a.match.isExactMatch, b.match.isExactMatch); | |
194 if (result != 0) return result; | |
195 | |
196 // Favor matches that do not break camel-case. | |
197 result = compareBools(a.match.isCamelCaseMatch, b.match.isCamelCaseMatch); | |
198 if (result != 0) return result; | |
199 | |
200 // Favor matches close to the begining. | |
201 result = a.match.matchOffset.compareTo(b.match.matchOffset); | |
202 if (result != 0) return result; | |
203 | |
204 if (a.type != null && b.type != null) { | |
205 // Favor short type names over long. | |
206 result = a.type.length.compareTo(b.type.length); | |
207 if (result != 0) return result; | |
208 | |
209 // Sort type alphabetically. | |
210 // TODO(4805): Use [:type.compareToIgnoreCase] when supported. | |
211 result = a.type.toLowerCase().compareTo(b.type.toLowerCase()); | |
212 if (result != 0) return result; | |
213 } | |
214 | |
215 // Sort match alphabetically. | |
216 // TODO(4805): Use [:text.compareToIgnoreCase] when supported. | |
217 return a.match.text.toLowerCase().compareTo(b.match.text.toLowerCase()); | |
218 } | |
OLD | NEW |