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

Side by Side Diff: pkg/analyzer/lib/src/dart/analysis/driver.dart

Issue 2665263002: Streamline AnalysisDriver status tracking. (Closed)
Patch Set: Created 3 years, 10 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
OLDNEW
1 // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 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. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 import 'dart:async'; 5 import 'dart:async';
6 import 'dart:collection'; 6 import 'dart:collection';
7 import 'dart:typed_data'; 7 import 'dart:typed_data';
8 8
9 import 'package:analyzer/context/declared_variables.dart'; 9 import 'package:analyzer/context/declared_variables.dart';
10 import 'package:analyzer/dart/ast/ast.dart'; 10 import 'package:analyzer/dart/ast/ast.dart';
(...skipping 199 matching lines...) Expand 10 before | Expand all | Expand 10 after
210 * The controller for the [results] stream. 210 * The controller for the [results] stream.
211 */ 211 */
212 final _resultController = new StreamController<AnalysisResult>(); 212 final _resultController = new StreamController<AnalysisResult>();
213 213
214 /** 214 /**
215 * Cached results for [_priorityFiles]. 215 * Cached results for [_priorityFiles].
216 */ 216 */
217 final Map<String, AnalysisResult> _priorityResults = {}; 217 final Map<String, AnalysisResult> _priorityResults = {};
218 218
219 /** 219 /**
220 * The instance of the status helper.
221 */
222 final StatusSupport _statusSupport = new StatusSupport();
223
224 /**
225 * The controller for the [exceptions] stream. 220 * The controller for the [exceptions] stream.
226 */ 221 */
227 final StreamController<ExceptionResult> _exceptionController = 222 final StreamController<ExceptionResult> _exceptionController =
228 new StreamController<ExceptionResult>(); 223 new StreamController<ExceptionResult>();
229 224
230 /** 225 /**
231 * The instance of the [Search] helper. 226 * The instance of the [Search] helper.
232 */ 227 */
233 Search _search; 228 Search _search;
234 229
(...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after
317 * exact order in which results are produced is not defined, neither 312 * exact order in which results are produced is not defined, neither
318 * between priority files, nor between priority and non-priority files. 313 * between priority files, nor between priority and non-priority files.
319 */ 314 */
320 void set priorityFiles(List<String> priorityPaths) { 315 void set priorityFiles(List<String> priorityPaths) {
321 _priorityResults.keys 316 _priorityResults.keys
322 .toSet() 317 .toSet()
323 .difference(priorityPaths.toSet()) 318 .difference(priorityPaths.toSet())
324 .forEach(_priorityResults.remove); 319 .forEach(_priorityResults.remove);
325 _priorityFiles.clear(); 320 _priorityFiles.clear();
326 _priorityFiles.addAll(priorityPaths); 321 _priorityFiles.addAll(priorityPaths);
327 _statusSupport.transitionToAnalyzing();
328 _scheduler._notify(this); 322 _scheduler._notify(this);
329 } 323 }
330 324
331 /** 325 /**
332 * Return the [Stream] that produces [AnalysisResult]s for added files. 326 * Return the [Stream] that produces [AnalysisResult]s for added files.
333 * 327 *
334 * Note that the stream supports only one single subscriber. 328 * Note that the stream supports only one single subscriber.
335 * 329 *
336 * Analysis starts when the [AnalysisDriverScheduler] is started and the 330 * Analysis starts when the [AnalysisDriverScheduler] is started and the
337 * driver is added to it. The analysis state transitions to "analyzing" and 331 * driver is added to it. The analysis state transitions to "analyzing" and
(...skipping 18 matching lines...) Expand all
356 * Return the search support for the driver. 350 * Return the search support for the driver.
357 */ 351 */
358 Search get search => _search; 352 Search get search => _search;
359 353
360 /** 354 /**
361 * Return the source factory used to resolve URIs to paths and restore URIs 355 * Return the source factory used to resolve URIs to paths and restore URIs
362 * from file paths. 356 * from file paths.
363 */ 357 */
364 SourceFactory get sourceFactory => _sourceFactory; 358 SourceFactory get sourceFactory => _sourceFactory;
365 359
366 /**
367 * Return the stream that produces [AnalysisStatus] events.
368 */
369 Stream<AnalysisStatus> get status => _statusSupport.stream;
370
371 @visibleForTesting 360 @visibleForTesting
372 AnalysisDriverTestView get test => _testView; 361 AnalysisDriverTestView get test => _testView;
373 362
374 /** 363 /**
375 * Return the priority of work that the driver needs to perform. 364 * Return the priority of work that the driver needs to perform.
376 */ 365 */
377 AnalysisDriverPriority get _workPriority { 366 AnalysisDriverPriority get _workPriority {
378 if (_requestedFiles.isNotEmpty) { 367 if (_requestedFiles.isNotEmpty) {
379 return AnalysisDriverPriority.interactive; 368 return AnalysisDriverPriority.interactive;
380 } 369 }
(...skipping 18 matching lines...) Expand all
399 } 388 }
400 if (_filesToAnalyze.isNotEmpty) { 389 if (_filesToAnalyze.isNotEmpty) {
401 return AnalysisDriverPriority.general; 390 return AnalysisDriverPriority.general;
402 } 391 }
403 if (_changedFiles.isNotEmpty) { 392 if (_changedFiles.isNotEmpty) {
404 return AnalysisDriverPriority.general; 393 return AnalysisDriverPriority.general;
405 } 394 }
406 if (_requestedParts.isNotEmpty || _partsToAnalyze.isNotEmpty) { 395 if (_requestedParts.isNotEmpty || _partsToAnalyze.isNotEmpty) {
407 return AnalysisDriverPriority.general; 396 return AnalysisDriverPriority.general;
408 } 397 }
409 _statusSupport.transitionToIdle();
410 return AnalysisDriverPriority.nothing; 398 return AnalysisDriverPriority.nothing;
411 } 399 }
412 400
413 /** 401 /**
414 * Add the file with the given [path] to the set of files to analyze. 402 * Add the file with the given [path] to the set of files to analyze.
415 * 403 *
416 * The [path] must be absolute and normalized. 404 * The [path] must be absolute and normalized.
417 * 405 *
418 * The results of analysis are eventually produced by the [results] stream. 406 * The results of analysis are eventually produced by the [results] stream.
419 */ 407 */
420 void addFile(String path) { 408 void addFile(String path) {
421 if (!_fsState.hasUri(path)) { 409 if (!_fsState.hasUri(path)) {
422 return; 410 return;
423 } 411 }
424 if (AnalysisEngine.isDartFileName(path)) { 412 if (AnalysisEngine.isDartFileName(path)) {
425 _addedFiles.add(path); 413 _addedFiles.add(path);
426 _filesToAnalyze.add(path); 414 _filesToAnalyze.add(path);
427 _priorityResults.clear(); 415 _priorityResults.clear();
428 } 416 }
429 _statusSupport.transitionToAnalyzing();
430 _scheduler._notify(this); 417 _scheduler._notify(this);
431 } 418 }
432 419
433 /** 420 /**
434 * The file with the given [path] might have changed - updated, added or 421 * The file with the given [path] might have changed - updated, added or
435 * removed. Or not, we don't know. Or it might have, but then changed back. 422 * removed. Or not, we don't know. Or it might have, but then changed back.
436 * 423 *
437 * The [path] must be absolute and normalized. 424 * The [path] must be absolute and normalized.
438 * 425 *
439 * The [path] can be any file - explicitly or implicitly analyzed, or neither. 426 * The [path] can be any file - explicitly or implicitly analyzed, or neither.
440 * 427 *
441 * Causes the analysis state to transition to "analyzing" (if it is not in 428 * Causes the analysis state to transition to "analyzing" (if it is not in
442 * that state already). Schedules the file contents for [path] to be read 429 * that state already). Schedules the file contents for [path] to be read
443 * into the current file state prior to the next time the analysis state 430 * into the current file state prior to the next time the analysis state
444 * transitions to "idle". 431 * transitions to "idle".
445 * 432 *
446 * Invocation of this method will not prevent a [Future] returned from 433 * Invocation of this method will not prevent a [Future] returned from
447 * [getResult] from completing with a result, but the result is not 434 * [getResult] from completing with a result, but the result is not
448 * guaranteed to be consistent with the new current file state after this 435 * guaranteed to be consistent with the new current file state after this
449 * [changeFile] invocation. 436 * [changeFile] invocation.
450 */ 437 */
451 void changeFile(String path) { 438 void changeFile(String path) {
452 _changedFiles.add(path); 439 _changedFiles.add(path);
453 if (_addedFiles.contains(path)) { 440 if (_addedFiles.contains(path)) {
454 _filesToAnalyze.add(path); 441 _filesToAnalyze.add(path);
455 } 442 }
456 _priorityResults.clear(); 443 _priorityResults.clear();
457 _statusSupport.transitionToAnalyzing();
458 _scheduler._notify(this); 444 _scheduler._notify(this);
459 } 445 }
460 446
461 /** 447 /**
462 * Some state on which analysis depends has changed, so the driver needs to be 448 * Some state on which analysis depends has changed, so the driver needs to be
463 * re-configured with the new state. 449 * re-configured with the new state.
464 * 450 *
465 * At least one of the optional parameters should be provided, but only those 451 * At least one of the optional parameters should be provided, but only those
466 * that represent state that has actually changed need be provided. 452 * that represent state that has actually changed need be provided.
467 */ 453 */
468 void configure( 454 void configure(
469 {AnalysisOptions analysisOptions, SourceFactory sourceFactory}) { 455 {AnalysisOptions analysisOptions, SourceFactory sourceFactory}) {
470 if (analysisOptions != null) { 456 if (analysisOptions != null) {
471 _analysisOptions = analysisOptions; 457 _analysisOptions = analysisOptions;
472 } 458 }
473 if (sourceFactory != null) { 459 if (sourceFactory != null) {
474 _sourceFactory = sourceFactory; 460 _sourceFactory = sourceFactory;
475 } 461 }
476 _fillSalt(); 462 _fillSalt();
477 _fsState = new FileSystemState(_logger, _byteStore, _contentOverlay, 463 _fsState = new FileSystemState(_logger, _byteStore, _contentOverlay,
478 _resourceProvider, _sourceFactory, _analysisOptions, _salt); 464 _resourceProvider, _sourceFactory, _analysisOptions, _salt);
479 _filesToAnalyze.addAll(_addedFiles); 465 _filesToAnalyze.addAll(_addedFiles);
480 _statusSupport.transitionToAnalyzing();
481 _scheduler._notify(this); 466 _scheduler._notify(this);
482 } 467 }
483 468
484 /** 469 /**
485 * Notify the driver that the client is going to stop using it. 470 * Notify the driver that the client is going to stop using it.
486 */ 471 */
487 void dispose() { 472 void dispose() {
488 _scheduler._remove(this); 473 _scheduler._remove(this);
489 } 474 }
490 475
(...skipping 27 matching lines...) Expand all
518 analysisResult.errors); 503 analysisResult.errors);
519 } 504 }
520 505
521 /** 506 /**
522 * Return a [Future] that completes with the list of added files that 507 * Return a [Future] that completes with the list of added files that
523 * reference the given external [name]. 508 * reference the given external [name].
524 */ 509 */
525 Future<List<String>> getFilesReferencingName(String name) { 510 Future<List<String>> getFilesReferencingName(String name) {
526 var task = new _FilesReferencingNameTask(this, name); 511 var task = new _FilesReferencingNameTask(this, name);
527 _referencingNameTasks.add(task); 512 _referencingNameTasks.add(task);
528 _statusSupport.transitionToAnalyzing();
529 _scheduler._notify(this); 513 _scheduler._notify(this);
530 return task.completer.future; 514 return task.completer.future;
531 } 515 }
532 516
533 /** 517 /**
534 * Return a [Future] that completes with the [AnalysisDriverUnitIndex] for 518 * Return a [Future] that completes with the [AnalysisDriverUnitIndex] for
535 * the file with the given [path], or with `null` if the file cannot be 519 * the file with the given [path], or with `null` if the file cannot be
536 * analyzed. 520 * analyzed.
537 */ 521 */
538 Future<AnalysisDriverUnitIndex> getIndex(String path) { 522 Future<AnalysisDriverUnitIndex> getIndex(String path) {
539 if (!_fsState.hasUri(path)) { 523 if (!_fsState.hasUri(path)) {
540 return new Future.value(); 524 return new Future.value();
541 } 525 }
542 var completer = new Completer<AnalysisDriverUnitIndex>(); 526 var completer = new Completer<AnalysisDriverUnitIndex>();
543 _indexRequestedFiles 527 _indexRequestedFiles
544 .putIfAbsent(path, () => <Completer<AnalysisDriverUnitIndex>>[]) 528 .putIfAbsent(path, () => <Completer<AnalysisDriverUnitIndex>>[])
545 .add(completer); 529 .add(completer);
546 _statusSupport.transitionToAnalyzing();
547 _scheduler._notify(this); 530 _scheduler._notify(this);
548 return completer.future; 531 return completer.future;
549 } 532 }
550 533
551 /** 534 /**
552 * Return a [Future] that completes with a [AnalysisResult] for the Dart 535 * Return a [Future] that completes with a [AnalysisResult] for the Dart
553 * file with the given [path]. If the file is not a Dart file or cannot 536 * file with the given [path]. If the file is not a Dart file or cannot
554 * be analyzed, the [Future] completes with `null`. 537 * be analyzed, the [Future] completes with `null`.
555 * 538 *
556 * The [path] must be absolute and normalized. 539 * The [path] must be absolute and normalized.
(...skipping 19 matching lines...) Expand all
576 if (result != null) { 559 if (result != null) {
577 return new Future.value(result); 560 return new Future.value(result);
578 } 561 }
579 } 562 }
580 563
581 // Schedule analysis. 564 // Schedule analysis.
582 var completer = new Completer<AnalysisResult>(); 565 var completer = new Completer<AnalysisResult>();
583 _requestedFiles 566 _requestedFiles
584 .putIfAbsent(path, () => <Completer<AnalysisResult>>[]) 567 .putIfAbsent(path, () => <Completer<AnalysisResult>>[])
585 .add(completer); 568 .add(completer);
586 _statusSupport.transitionToAnalyzing();
587 _scheduler._notify(this); 569 _scheduler._notify(this);
588 return completer.future; 570 return completer.future;
589 } 571 }
590 572
591 /** 573 /**
592 * Return a [Future] that completes with the [SourceKind] for the Dart 574 * Return a [Future] that completes with the [SourceKind] for the Dart
593 * file with the given [path]. If the file is not a Dart file or cannot 575 * file with the given [path]. If the file is not a Dart file or cannot
594 * be analyzed, the [Future] completes with `null`. 576 * be analyzed, the [Future] completes with `null`.
595 * 577 *
596 * The [path] must be absolute and normalized. 578 * The [path] must be absolute and normalized.
597 */ 579 */
598 Future<SourceKind> getSourceKind(String path) async { 580 Future<SourceKind> getSourceKind(String path) async {
599 if (AnalysisEngine.isDartFileName(path)) { 581 if (AnalysisEngine.isDartFileName(path)) {
600 FileState file = _fsState.getFileForPath(path); 582 FileState file = _fsState.getFileForPath(path);
601 return file.isPart ? SourceKind.PART : SourceKind.LIBRARY; 583 return file.isPart ? SourceKind.PART : SourceKind.LIBRARY;
602 } 584 }
603 return null; 585 return null;
604 } 586 }
605 587
606 /** 588 /**
607 * Return a [Future] that completes with top-level declarations with the 589 * Return a [Future] that completes with top-level declarations with the
608 * given [name] in all known libraries. 590 * given [name] in all known libraries.
609 */ 591 */
610 Future<List<TopLevelDeclarationInSource>> getTopLevelNameDeclarations( 592 Future<List<TopLevelDeclarationInSource>> getTopLevelNameDeclarations(
611 String name) { 593 String name) {
612 var task = new _TopLevelNameDeclarationsTask(this, name); 594 var task = new _TopLevelNameDeclarationsTask(this, name);
613 _topLevelNameDeclarationsTasks.add(task); 595 _topLevelNameDeclarationsTasks.add(task);
614 _statusSupport.transitionToAnalyzing();
615 _scheduler._notify(this); 596 _scheduler._notify(this);
616 return task.completer.future; 597 return task.completer.future;
617 } 598 }
618 599
619 /** 600 /**
620 * Return a [Future] that completes with the [CompilationUnitElement] for the 601 * Return a [Future] that completes with the [CompilationUnitElement] for the
621 * file with the given [path], or with `null` if the file cannot be analyzed. 602 * file with the given [path], or with `null` if the file cannot be analyzed.
622 */ 603 */
623 Future<CompilationUnitElement> getUnitElement(String path) { 604 Future<CompilationUnitElement> getUnitElement(String path) {
624 if (!_fsState.hasUri(path)) { 605 if (!_fsState.hasUri(path)) {
625 return new Future.value(); 606 return new Future.value();
626 } 607 }
627 var completer = new Completer<CompilationUnitElement>(); 608 var completer = new Completer<CompilationUnitElement>();
628 _unitElementRequestedFiles 609 _unitElementRequestedFiles
629 .putIfAbsent(path, () => <Completer<CompilationUnitElement>>[]) 610 .putIfAbsent(path, () => <Completer<CompilationUnitElement>>[])
630 .add(completer); 611 .add(completer);
631 _statusSupport.transitionToAnalyzing();
632 _scheduler._notify(this); 612 _scheduler._notify(this);
633 return completer.future; 613 return completer.future;
634 } 614 }
635 615
636 /** 616 /**
637 * Return a [Future] that completes with a [ParseResult] for the file 617 * Return a [Future] that completes with a [ParseResult] for the file
638 * with the given [path]. 618 * with the given [path].
639 * 619 *
640 * The [path] must be absolute and normalized. 620 * The [path] must be absolute and normalized.
641 * 621 *
(...skipping 19 matching lines...) Expand all
661 * The results of analysis of the file might still be produced by the 641 * The results of analysis of the file might still be produced by the
662 * [results] stream. The driver will try to stop producing these results, 642 * [results] stream. The driver will try to stop producing these results,
663 * but does not guarantee this. 643 * but does not guarantee this.
664 */ 644 */
665 void removeFile(String path) { 645 void removeFile(String path) {
666 _addedFiles.remove(path); 646 _addedFiles.remove(path);
667 _filesToAnalyze.remove(path); 647 _filesToAnalyze.remove(path);
668 _fsState.removeFile(path); 648 _fsState.removeFile(path);
669 _filesToAnalyze.addAll(_addedFiles); 649 _filesToAnalyze.addAll(_addedFiles);
670 _priorityResults.clear(); 650 _priorityResults.clear();
671 _statusSupport.transitionToAnalyzing();
672 _scheduler._notify(this); 651 _scheduler._notify(this);
673 } 652 }
674 653
675 /** 654 /**
676 * Return a future that will be completed the next time the status is idle.
677 *
678 * If the status is currently idle, the returned future will be signaled
679 * immediately.
680 */
681 Future<Null> waitForIdle() => _statusSupport.waitForIdle();
682
683 /**
684 * Return the cached or newly computed analysis result of the file with the 655 * Return the cached or newly computed analysis result of the file with the
685 * given [path]. 656 * given [path].
686 * 657 *
687 * The result will have the fully resolved unit and will always be newly 658 * The result will have the fully resolved unit and will always be newly
688 * compute only if [withUnit] is `true`. 659 * compute only if [withUnit] is `true`.
689 * 660 *
690 * Return `null` if the file is a part of an unknown library, so cannot be 661 * Return `null` if the file is a part of an unknown library, so cannot be
691 * analyzed yet. But [asIsIfPartWithoutLibrary] is `true`, then the file is 662 * analyzed yet. But [asIsIfPartWithoutLibrary] is `true`, then the file is
692 * analyzed anyway, even without a library. 663 * analyzed anyway, even without a library.
693 */ 664 */
(...skipping 530 matching lines...) Expand 10 before | Expand all | Expand 10 after
1224 */ 1195 */
1225 void start() { 1196 void start() {
1226 if (_started) { 1197 if (_started) {
1227 throw new StateError('The scheduler has already been started.'); 1198 throw new StateError('The scheduler has already been started.');
1228 } 1199 }
1229 _started = true; 1200 _started = true;
1230 _run(); 1201 _run();
1231 } 1202 }
1232 1203
1233 /** 1204 /**
1205 * Return a future that will be completed the next time the status is idle.
1206 *
1207 * If the status is currently idle, the returned future will be signaled
1208 * immediately.
1209 */
1210 Future<Null> waitForIdle() => _statusSupport.waitForIdle();
1211
1212 /**
1234 * Add the given [driver] and schedule it to perform its work. 1213 * Add the given [driver] and schedule it to perform its work.
1235 */ 1214 */
1236 void _add(AnalysisDriver driver) { 1215 void _add(AnalysisDriver driver) {
1237 _drivers.add(driver); 1216 _drivers.add(driver);
1238 _hasWork.notify(); 1217 _hasWork.notify();
1239 } 1218 }
1240 1219
1241 /** 1220 /**
1242 * Notify that there is a change to the [driver], it it might need to 1221 * Notify that there is a change to the [driver], it it might need to
1243 * perform some work. 1222 * perform some work.
1244 */ 1223 */
1245 void _notify(AnalysisDriver driver) { 1224 void _notify(AnalysisDriver driver) {
1246 _hasWork.notify(); 1225 _hasWork.notify();
1226 _statusSupport.preTransitionToAnalyzing();
1247 } 1227 }
1248 1228
1249 /** 1229 /**
1250 * Remove the given [driver] from the scheduler, so that it will not be 1230 * Remove the given [driver] from the scheduler, so that it will not be
1251 * asked to perform any new work. 1231 * asked to perform any new work.
1252 */ 1232 */
1253 void _remove(AnalysisDriver driver) { 1233 void _remove(AnalysisDriver driver) {
1254 _drivers.remove(driver); 1234 _drivers.remove(driver);
1255 _hasWork.notify(); 1235 _hasWork.notify();
1256 } 1236 }
(...skipping 18 matching lines...) Expand all
1275 if (_hasFilesToAnalyze) { 1255 if (_hasFilesToAnalyze) {
1276 _statusSupport.transitionToAnalyzing(); 1256 _statusSupport.transitionToAnalyzing();
1277 analysisSection ??= _logger.enter('Analyzing'); 1257 analysisSection ??= _logger.enter('Analyzing');
1278 } 1258 }
1279 1259
1280 // Find the driver with the highest priority. 1260 // Find the driver with the highest priority.
1281 AnalysisDriver bestDriver; 1261 AnalysisDriver bestDriver;
1282 AnalysisDriverPriority bestPriority = AnalysisDriverPriority.nothing; 1262 AnalysisDriverPriority bestPriority = AnalysisDriverPriority.nothing;
1283 for (AnalysisDriver driver in _drivers) { 1263 for (AnalysisDriver driver in _drivers) {
1284 AnalysisDriverPriority priority = driver._workPriority; 1264 AnalysisDriverPriority priority = driver._workPriority;
1285 if (bestPriority == null || priority.index > bestPriority.index) { 1265 if (priority.index > bestPriority.index) {
1286 bestDriver = driver; 1266 bestDriver = driver;
1287 bestPriority = priority; 1267 bestPriority = priority;
1288 } 1268 }
1289 } 1269 }
1290 1270
1291 // Transition to idle if no files to analyze. 1271 // Transition to idle if no files to analyze.
1292 if (!_hasFilesToAnalyze) { 1272 if (!_hasFilesToAnalyze) {
1293 _statusSupport.transitionToIdle(); 1273 _statusSupport.transitionToIdle();
1294 analysisSection?.exit(); 1274 analysisSection?.exit();
1295 analysisSection = null; 1275 analysisSection = null;
(...skipping 431 matching lines...) Expand 10 before | Expand all | Expand 10 after
1727 libraryDeclarations.add(new TopLevelDeclarationInSource( 1707 libraryDeclarations.add(new TopLevelDeclarationInSource(
1728 file.source, declaration, isExported)); 1708 file.source, declaration, isExported));
1729 } 1709 }
1730 } 1710 }
1731 } 1711 }
1732 1712
1733 // We're not done yet. 1713 // We're not done yet.
1734 return false; 1714 return false;
1735 } 1715 }
1736 } 1716 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698