OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, 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 library trydart.editor; | |
6 | |
7 import 'dart:html'; | |
8 | |
9 import 'package:compiler/src/scanner/string_scanner.dart' show | |
10 StringScanner; | |
11 | |
12 import 'package:compiler/src/tokens/token.dart' show | |
13 ErrorToken, | |
14 Token; | |
15 | |
16 import 'package:compiler/src/tokens/token_constants.dart' show | |
17 EOF_TOKEN; | |
18 | |
19 import 'ui.dart' show | |
20 currentTheme, | |
21 hackDiv, | |
22 interaction, | |
23 mainEditorPane, | |
24 observer, | |
25 outputDiv; | |
26 | |
27 import 'decoration.dart' show | |
28 CodeCompletionDecoration, | |
29 Decoration, | |
30 DiagnosticDecoration, | |
31 error, | |
32 info, | |
33 warning; | |
34 | |
35 import 'selection.dart' show | |
36 isCollapsed; | |
37 | |
38 import 'shadow_root.dart' show | |
39 getShadowRoot; | |
40 | |
41 import 'settings.dart' as settings; | |
42 | |
43 const String INDENT = '\u{a0}\u{a0}'; | |
44 | |
45 Set<String> seenIdentifiers; | |
46 | |
47 Element moveActive(int distance, Node ui) { | |
48 var /* ShadowRoot or Element */ root = getShadowRoot(ui); | |
49 List<Element> entries = root.querySelectorAll('.dart-static>.dart-entry'); | |
50 int activeIndex = -1; | |
51 for (var i = 0; i < entries.length; i++) { | |
52 if (entries[i].classes.contains('activeEntry')) { | |
53 activeIndex = i; | |
54 break; | |
55 } | |
56 } | |
57 int newIndex = activeIndex + distance; | |
58 Element currentEntry; | |
59 if (0 <= newIndex && newIndex < entries.length) { | |
60 currentEntry = entries[newIndex]; | |
61 } | |
62 if (currentEntry == null) return null; | |
63 if (0 <= newIndex && activeIndex != -1) { | |
64 entries[activeIndex].classes.remove('activeEntry'); | |
65 } | |
66 Element staticNode = root.querySelector('.dart-static'); | |
67 String visibility = computeVisibility(currentEntry, staticNode); | |
68 print(visibility); | |
69 var serverResults = root.querySelectorAll('.dart-server>.dart-entry'); | |
70 var serverResultCount = serverResults.length; | |
71 if (serverResultCount > 0) { | |
72 switch (visibility) { | |
73 case obscured: | |
74 case hidden: { | |
75 Rectangle cr = currentEntry.getBoundingClientRect(); | |
76 Rectangle sr = staticNode.getBoundingClientRect(); | |
77 Element entry = serverResults[0]; | |
78 entry.remove(); | |
79 currentEntry.parentNode.insertBefore(entry, currentEntry); | |
80 currentEntry = entry; | |
81 serverResultCount--; | |
82 | |
83 staticNode.style.maxHeight = '${sr.boundingBox(cr).height}px'; | |
84 } | |
85 } | |
86 } else { | |
87 currentEntry.scrollIntoView(); | |
88 } | |
89 if (serverResultCount == 0) { | |
90 root.querySelector('.dart-server').style.display = 'none'; | |
91 } | |
92 if (currentEntry != null) { | |
93 currentEntry.classes.add('activeEntry'); | |
94 } | |
95 // Discard mutations. | |
96 observer.takeRecords(); | |
97 return currentEntry; | |
98 } | |
99 | |
100 const visible = 'visible'; | |
101 const obscured = 'obscured'; | |
102 const hidden = 'hidden'; | |
103 | |
104 String computeVisibility(Element node, [Element parent]) { | |
105 Rectangle nr = node.getBoundingClientRect(); | |
106 if (parent == null) parent = node.parentNode; | |
107 Rectangle pr = parent.getBoundingClientRect(); | |
108 | |
109 if (pr.containsRectangle(nr)) return visible; | |
110 | |
111 if (pr.intersects(nr)) return obscured; | |
112 | |
113 return hidden; | |
114 } | |
115 | |
116 var activeCompletion; | |
117 num minSuggestionWidth = 0; | |
118 | |
119 /// Returns the [Element] which encloses the current collapsed selection, if it | |
120 /// exists. | |
121 Element getElementAtSelection() { | |
122 Selection selection = window.getSelection(); | |
123 if (!isCollapsed(selection)) return null; | |
124 var anchorNode = selection.anchorNode; | |
125 if (!mainEditorPane.contains(anchorNode)) return null; | |
126 if (mainEditorPane == anchorNode) return null; | |
127 int type = anchorNode.nodeType; | |
128 if (type != Node.TEXT_NODE) return null; | |
129 Text text = anchorNode; | |
130 var parent = text.parent; | |
131 if (parent is! Element) return null; | |
132 if (mainEditorPane == parent) return null; | |
133 return parent; | |
134 } | |
135 | |
136 bool isMalformedInput = false; | |
137 | |
138 addDiagnostic(String kind, String message, int begin, int end) { | |
139 observer.disconnect(); | |
140 Selection selection = window.getSelection(); | |
141 int offset = 0; | |
142 int anchorOffset = 0; | |
143 bool hasSelection = false; | |
144 Node anchorNode = selection.anchorNode; | |
145 bool foundNode = false; | |
146 void walk4(Node node) { | |
147 // TODO(ahe): Use TreeWalker when that is exposed. | |
148 int type = node.nodeType; | |
149 if (type == Node.TEXT_NODE || type == Node.CDATA_SECTION_NODE) { | |
150 CharacterData cdata = node; | |
151 // print('walking: ${node.data}'); | |
152 if (anchorNode == node) { | |
153 hasSelection = true; | |
154 anchorOffset = selection.anchorOffset + offset; | |
155 } | |
156 int newOffset = offset + cdata.length; | |
157 if (offset <= begin && begin < newOffset) { | |
158 hasSelection = node == anchorNode; | |
159 anchorOffset = selection.anchorOffset; | |
160 var alert; | |
161 if (kind == 'error') { | |
162 alert = error(message); | |
163 } else if (kind == 'warning') { | |
164 alert = warning(message); | |
165 } else { | |
166 alert = info(message); | |
167 } | |
168 Element parent = node.parentNode; | |
169 if (parent.classes.contains("diagnostic") && | |
170 !interaction.oldDiagnostics.contains(parent)) { | |
171 Element other = parent.firstChild; | |
172 other.remove(); | |
173 SpanElement wrapper = new SpanElement() | |
174 ..classes.add('diagnostic') | |
175 ..style.fontWeight = 'normal'; | |
176 | |
177 var root = getShadowRoot(wrapper); | |
178 if (root is ShadowRoot) { | |
179 // When https://code.google.com/p/chromium/issues/detail?id=313458 | |
180 // is fixed: | |
181 // var link = new LinkElement() | |
182 // ..rel = "stylesheet" | |
183 // ..type = "text/css" | |
184 // ..href = "dartlang-style.css"; | |
185 // root.append(link); | |
186 root.append( | |
187 new StyleElement()..text = '@import url(dartlang-style.css)'); | |
188 } | |
189 root | |
190 ..append(other) | |
191 ..append(alert); | |
192 other.style.display = 'block'; | |
193 alert.style.display = 'block'; | |
194 parent.append(wrapper); | |
195 } else { | |
196 if (interaction.oldDiagnostics.contains(parent)) { | |
197 node.remove(); | |
198 parent.replaceWith(node); | |
199 } | |
200 Node marker = new Text(""); | |
201 node.replaceWith(marker); | |
202 // TODO(ahe): Don't highlight everything in the node. Find the | |
203 // relevant token (works for now as we create a node for each token, | |
204 // which is probably not great for performance). | |
205 marker.replaceWith(diagnostic(node, alert)); | |
206 if (hasSelection) { | |
207 selection.collapse(node, anchorOffset); | |
208 } | |
209 } | |
210 foundNode = true; | |
211 return; | |
212 } | |
213 offset = newOffset; | |
214 } else if (type == Node.ELEMENT_NODE) { | |
215 Element element = node; | |
216 CssClassSet classes = element.classes; | |
217 if (classes.contains('alert') || | |
218 classes.contains('dart-code-completion')) { | |
219 return; | |
220 } | |
221 } | |
222 | |
223 var child = node.firstChild; | |
224 while(child != null && !foundNode) { | |
225 walk4(child); | |
226 child = child.nextNode; | |
227 } | |
228 } | |
229 walk4(mainEditorPane); | |
230 | |
231 if (!foundNode) { | |
232 outputDiv.appendText('$message\n'); | |
233 } | |
234 | |
235 observer.takeRecords(); | |
236 observer.observe( | |
237 mainEditorPane, childList: true, characterData: true, subtree: true); | |
238 } | |
239 | |
240 Decoration getDecoration(Token token) { | |
241 if (token is ErrorToken) { | |
242 // TODO(ahe): Remove side effects from this method. It only leads to | |
243 // confusion. | |
244 isMalformedInput = true; | |
245 return new DiagnosticDecoration('error', token.assertionMessage); | |
246 } | |
247 String tokenValue = token.value; | |
248 String tokenInfo = token.info.value; | |
249 if (tokenInfo == 'string') return currentTheme.string; | |
250 if (tokenInfo == 'identifier') { | |
251 seenIdentifiers.add(tokenValue); | |
252 Decoration decoration = currentTheme.foreground; | |
253 if (settings.enableCodeCompletion.value) { | |
254 decoration = CodeCompletionDecoration.from(decoration); | |
255 } | |
256 return decoration; | |
257 } | |
258 if (tokenInfo == 'keyword') return currentTheme.keyword; | |
259 if (tokenInfo == 'comment') return currentTheme.singleLineComment; | |
260 if (tokenInfo == 'malformed input') { | |
261 // TODO(ahe): Remove side effects from this method. It only leads to | |
262 // confusion. | |
263 isMalformedInput = true; | |
264 return new DiagnosticDecoration('error', tokenValue); | |
265 } | |
266 return currentTheme.foreground; | |
267 } | |
268 | |
269 diagnostic(content, tip) { | |
270 if (content is String) { | |
271 content = new Text(content); | |
272 } | |
273 if (content is! List) { | |
274 content = [content]; | |
275 } | |
276 return new AnchorElement() | |
277 ..classes.add('diagnostic') | |
278 ..append(tip) // Should be first for better Firefox editing. | |
279 ..nodes.addAll(content); | |
280 } | |
OLD | NEW |