<commit_before>// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:developer' as developer;
import 'dart:io';

import 'package:analysis_server/protocol/protocol_constants.dart'
    show PROTOCOL_VERSION;
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/analysis_server_abstract.dart';
import 'package:analysis_server/src/domain_completion.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart'
    show LspAnalysisServer;
import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analysis_server/src/server/http_server.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/socket_server.dart';
import 'package:analysis_server/src/status/ast_writer.dart';
import 'package:analysis_server/src/status/element_writer.dart';
import 'package:analysis_server/src/status/pages.dart';
import 'package:analysis_server/src/utilities/profiling.dart';
import 'package:analyzer/src/context/context_root.dart';
import 'package:analyzer/src/context/source.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/source/package_map_resolver.dart';
import 'package:path/path.dart' as path;

final String kCustomCss = '''
.lead, .page-title+.markdown-body>p:first-child {
  margin-bottom: 30px;
  font-size: 20px;
  font-weight: 300;
  color: #555;
}

.container {
  width: 1160px;
}

.masthead {
  padding-top: 1rem;
  padding-bottom: 1rem;
  margin-bottom: 1.5rem;
  text-align: center;
  background-color: #4078c0;
}

.masthead .masthead-logo {
  display: inline-block;
  font-size: 1.5rem;
  color: #fff;
  float: left;
}

.masthead .mega-octicon {
  font-size: 1.5rem;
}

.masthead-nav {
  float: right;
  margin-top: .5rem;
}

.masthead-nav a:not(:last-child) {
  margin-right: 1.25rem;
}

.masthead a {
  color: rgba(255,255,255,0.5);
  font-size: 1rem;
}

.masthead a:hover {
  color: #fff;
  text-decoration: none;
}

.masthead-nav .active {
  color: #fff;
  font-weight: 500;
}

.counter {
  display: inline-block;
  padding: 2px 5px;
  font-size: 11px;
  font-weight: bold;
  line-height: 1;
  color: #666;
  background-color: #eee;
  border-radius: 20px;
}

.menu-item .counter {
  float: right;
  margin-left: 5px;
}

td.right {
  text-align: right;
}

table td {
  max-width: 600px;
  vertical-align: text-top;
}

td.pre {
  white-space: pre;
}

.nowrap {
  white-space: nowrap;
}

.scroll-table {
  max-height: 190px;
  overflow-x: auto;
}

.footer {
  padding-top: 3rem;
  padding-bottom: 3rem;
  margin-top: 3rem;
  line-height: 1.75;
  color: #7a7a7a;
  border-top: 1px solid #eee;
}

.footer strong {
  color: #333;
}
''';

String get _sdkVersion {
  var version = Platform.version;
  if (version.contains(' ')) {
    version = version.substring(0, version.indexOf(' '));
  }
  return version;
}

String writeOption(String name, dynamic value) {
  return '$name: <code>$value</code><br> ';
}

abstract class AbstractCompletionPage extends DiagnosticPageWithNav {
  AbstractCompletionPage(DiagnosticsSite site)
      : super(site, 'completion', 'Code Completion',
            description: 'Latency statistics for code completion.');

  path.Context get pathContext;

  List<CompletionPerformance> get performanceItems;

  @override
  Future generateContent(Map<String, String> params) async {
    var completions = performanceItems;

    if (completions.isEmpty) {
      blankslate('No completions recorded.');
      return;
    }

    var fastCount =
        completions.where((c) => c.elapsedInMilliseconds <= 100).length;
    p('${completions.length} results; ${printPercentage(fastCount / completions.length)} within 100ms.');

    // draw a chart
    buf.writeln(
        '<div id="chart-div" style="width: 700px; height: 300px;"></div>');
    var rowData = StringBuffer();
    for (var i = completions.length - 1; i >= 0; i--) {
      // [' ', 101.5]
      if (rowData.isNotEmpty) {
        rowData.write(',');
      }
      rowData.write("[' ', ${completions[i].elapsedInMilliseconds}]");
    }
    buf.writeln('''
      <script type="text/javascript">
      google.charts.load('current', {'packages':['bar']});
      google.charts.setOnLoadCallback(drawChart);
      function drawChart() {
        var data = google.visualization.arrayToDataTable([
          ['Completions', 'Time'],
          $rowData
        ]);
        var options = { bars: 'vertical', vAxis: {format: 'decimal'}, height: 300 };
        var chart = new google.charts.Bar(document.getElementById('chart-div'));
        chart.draw(data, google.charts.Bar.convertOptions(options));
      }
      </script>
''');

    // emit the data as a table
    buf.writeln('<table>');
    buf.writeln(
        '<tr><th>Time</th><th>Results</th><th>Source</th><th>Snippet</th></tr>');
    for (var completion in completions) {
      var shortName = pathContext.basename(completion.path);
      buf.writeln('<tr>'
          '<td class="pre right">${printMilliseconds(completion.elapsedInMilliseconds)}</td>'
          '<td class="right">${completion.suggestionCountStr}</td>'
          '<td>${escape(shortName)}</td>'
          '<td><code>${escape(completion.snippet)}</code></td>'
          '</tr>');
    }
    buf.writeln('</table>');
  }
}

class AstPage extends DiagnosticPageWithNav {
  String _description;

  AstPage(DiagnosticsSite site)
      : super(site, 'ast', 'AST', description: 'The AST for a file.');

  @override
  String get description => _description ?? super.description;

  @override
  bool get showInNav => false;

  @override
  Future<void> generateContent(Map<String, String> params) async {
    var filePath = params['file'];
    if (filePath == null) {
      p('No file path provided.');
      return;
    }
    var driver = server.getAnalysisDriver(filePath);
    if (driver == null) {
      p('The file <code>${escape(filePath)}</code> is not being analyzed.',
          raw: true);
      return;
    }
    var result = await driver.getResult(filePath);
    if (result == null) {
      p(
          'An AST could not be produced for the file '
          '<code>${escape(filePath)}</code>.',
          raw: true);
      return;
    }

    var writer = AstWriter(buf);
    result.unit.accept(writer);
  }

  @override
  Future<void> generatePage(Map<String, String> params) async {
    try {
      _description = params['file'];
      await super.generatePage(params);
    } finally {
      _description = null;
    }
  }
}

class CommunicationsPage extends DiagnosticPageWithNav {
  CommunicationsPage(DiagnosticsSite site)
      : super(site, 'communications', 'Communications',
            description:
                'Latency statistics for analysis server communications.');

  @override
  Future generateContent(Map<String, String> params) async {
    void writeRow(List<String> data, {List<String> classes}) {
      buf.write('<tr>');
      for (var i = 0; i < data.length; i++) {
        var c = classes == null ? null : classes[i];
        if (c != null) {
          buf.write('<td class="$c">${escape(data[i])}</td>');
        } else {
          buf.write('<td>${escape(data[i])}</td>');
        }
      }
      buf.writeln('</tr>');
    }

    buf.writeln('<div class="columns">');

    if (server.performanceAfterStartup != null) {
      buf.writeln('<div class="column one-half">');

      h3('Current');
      _writePerformanceTable(server.performanceAfterStartup, writeRow);

      var time = server.uptime.toString();
      if (time.contains('.')) {
        time = time.substring(0, time.indexOf('.'));
      }
      buf.writeln(writeOption('Uptime', time));

      buf.write('</div>');
    }

    buf.writeln('<div class="column one-half">');

    h3('Startup');
    _writePerformanceTable(server.performanceDuringStartup, writeRow);

    if (server.performanceAfterStartup != null) {
      var startupTime = server.performanceAfterStartup.startTime -
          server.performanceDuringStartup.startTime;
      buf.writeln(
          writeOption('Initial analysis time', printMilliseconds(startupTime)));
    }

    buf.write('</div>');

    buf.write('</div>');
  }

  void _writePerformanceTable(ServerPerformance perf,
      void Function(List<String> data, {List<String> classes}) writeRow) {
    var requestCount = perf.requestCount;
    var latencyCount = perf.latencyCount;
    var averageLatency =
        latencyCount > 0 ? (perf.requestLatency ~/ latencyCount) : 0;
    var maximumLatency = perf.maxLatency;
    var slowRequestPercent =
        latencyCount > 0 ? (perf.slowRequestCount / latencyCount) : 0.0;

    buf.write('<table>');
    writeRow([printInteger(requestCount), 'requests'],
        classes: ['right', null]);
    writeRow([printInteger(latencyCount), 'requests with latency information'],
        classes: ['right', null]);
    if (latencyCount > 0) {
      writeRow([printMilliseconds(averageLatency), 'average latency'],
          classes: ['right', null]);
      writeRow([printMilliseconds(maximumLatency), 'maximum latency'],
          classes: ['right', null]);
      writeRow([printPercentage(slowRequestPercent), '> 150 ms latency'],
          classes: ['right', null]);
    }
    buf.write('</table>');
  }
}

class CompletionPage extends AbstractCompletionPage {
  @override
  AnalysisServer server;

  CompletionPage(DiagnosticsSite site, this.server) : super(site);

  CompletionDomainHandler get completionDomain => server.handlers
      .firstWhere((handler) => handler is CompletionDomainHandler);

  @override
  path.Context get pathContext =>
      completionDomain.server.resourceProvider.pathContext;

  @override
  List<CompletionPerformance> get performanceItems =>
      completionDomain.performanceList.items.toList();
}

class ContentsPage extends DiagnosticPageWithNav {
  String _description;

  ContentsPage(DiagnosticsSite site)
      : super(site, 'contents', 'Contents',
            description: 'The Contents/Overlay of a file.');

  @override
  String get description => _description ?? super.description;

  @override
  bool get showInNav => false;

  @override
  Future<void> generateContent(Map<String, String> params) async {
    var filePath = params['file'];
    if (filePath == null) {
      p('No file path provided.');
      return;
    }
    var driver = server.getAnalysisDriver(filePath);
    if (driver == null) {
      p('The file <code>${escape(filePath)}</code> is not being analyzed.',
          raw: true);
      return;
    }
    var file = server.resourceProvider.getFile(filePath);
    if (!file.exists) {
      p('The file <code>${escape(filePath)}</code> does not exist.', raw: true);
      return;
    }

    if (server.resourceProvider.hasOverlay(filePath)) {
      p('Showing overlay for file.');
    } else {
      p('Showing file system contents for file.');
    }

    pre(() {
      buf.write('<code>');
      buf.write(escape(file.readAsStringSync()));
      buf.writeln('</code>');
    });
  }

  @override
  Future<void> generatePage(Map<String, String> params) async {
    try {
      _description = params['file'];
      await super.generatePage(params);
    } finally {
      _description = null;
    }
  }
}

class ContextsPage extends DiagnosticPageWithNav {
  ContextsPage(DiagnosticsSite site)
      : super(site, 'contexts', 'Contexts',
            description:
                'An analysis context defines the options and the set of sources being analyzed.');

  @override
  String get navDetail => printInteger(server.driverMap.length);

  String describe(AnalysisOptionsImpl options) {
    var b = StringBuffer();

    b.write(writeOption('Implicit dynamic', options.implicitDynamic));
    b.write(writeOption('Implicit casts', options.implicitCasts));
    b.write(writeOption('Feature set', options.contextFeatures.toString()));
    b.write('<br>');

    b.write(writeOption('Generate hints', options.hint));

    return b.toString();
  }

  @override
  Future generateContent(Map<String, String> params) async {
    var driverMap = server.driverMap;
    if (driverMap.isEmpty) {
      blankslate('No contexts.');
      return;
    }

    var contextPath = params['context'];
    var folders = driverMap.keys.toList();
    folders
        .sort((first, second) => first.shortName.compareTo(second.shortName));
    var folder =
        folders.firstWhere((f) => f.path == contextPath, orElse: () => null);

    if (folder == null) {
      folder = folders.first;
      contextPath = folder.path;
    }

    var driver = driverMap[folder];

    buf.writeln('<div class="tabnav">');
    buf.writeln('<nav class="tabnav-tabs">');
    for (var f in folders) {
      if (f == folder) {
        buf.writeln(
            '<a class="tabnav-tab selected">${escape(f.shortName)}</a>');
      } else {
        var p = '${this.path}?context=${Uri.encodeQueryComponent(f.path)}';
        buf.writeln(
            '<a href="$p" class="tabnav-tab">${escape(f.shortName)}</a>');
      }
    }
    buf.writeln('</nav>');
    buf.writeln('</div>');

    buf.writeln(writeOption('Context location', escape(contextPath)));
    buf.writeln(writeOption('Analysis options path',
        escape(driver.contextRoot.optionsFilePath ?? 'none')));

    buf.writeln('<div class="columns">');
<commit_msg>report sdk root in status<commit_after>// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:developer' as developer;
import 'dart:io';

import 'package:analysis_server/protocol/protocol_constants.dart'
    show PROTOCOL_VERSION;
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/analysis_server_abstract.dart';
import 'package:analysis_server/src/domain_completion.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart'
    show LspAnalysisServer;
import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analysis_server/src/server/http_server.dart';
import 'package:analysis_server/src/services/completion/completion_performance.dart';
import 'package:analysis_server/src/socket_server.dart';
import 'package:analysis_server/src/status/ast_writer.dart';
import 'package:analysis_server/src/status/element_writer.dart';
import 'package:analysis_server/src/status/pages.dart';
import 'package:analysis_server/src/utilities/profiling.dart';
import 'package:analyzer/src/context/context_root.dart';
import 'package:analyzer/src/context/source.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/source/package_map_resolver.dart';
import 'package:path/path.dart' as path;

final String kCustomCss = '''
.lead, .page-title+.markdown-body>p:first-child {
  margin-bottom: 30px;
  font-size: 20px;
  font-weight: 300;
  color: #555;
}

.container {
  width: 1160px;
}

.masthead {
  padding-top: 1rem;
  padding-bottom: 1rem;
  margin-bottom: 1.5rem;
  text-align: center;
  background-color: #4078c0;
}

.masthead .masthead-logo {
  display: inline-block;
  font-size: 1.5rem;
  color: #fff;
  float: left;
}

.masthead .mega-octicon {
  font-size: 1.5rem;
}

.masthead-nav {
  float: right;
  margin-top: .5rem;
}

.masthead-nav a:not(:last-child) {
  margin-right: 1.25rem;
}

.masthead a {
  color: rgba(255,255,255,0.5);
  font-size: 1rem;
}

.masthead a:hover {
  color: #fff;
  text-decoration: none;
}

.masthead-nav .active {
  color: #fff;
  font-weight: 500;
}

.counter {
  display: inline-block;
  padding: 2px 5px;
  font-size: 11px;
  font-weight: bold;
  line-height: 1;
  color: #666;
  background-color: #eee;
  border-radius: 20px;
}

.menu-item .counter {
  float: right;
  margin-left: 5px;
}

td.right {
  text-align: right;
}

table td {
  max-width: 600px;
  vertical-align: text-top;
}

td.pre {
  white-space: pre;
}

.nowrap {
  white-space: nowrap;
}

.scroll-table {
  max-height: 190px;
  overflow-x: auto;
}

.footer {
  padding-top: 3rem;
  padding-bottom: 3rem;
  margin-top: 3rem;
  line-height: 1.75;
  color: #7a7a7a;
  border-top: 1px solid #eee;
}

.footer strong {
  color: #333;
}
''';

String get _sdkVersion {
  var version = Platform.version;
  if (version.contains(' ')) {
    version = version.substring(0, version.indexOf(' '));
  }
  return version;
}

String writeOption(String name, dynamic value) {
  return '$name: <code>$value</code><br> ';
}

abstract class AbstractCompletionPage extends DiagnosticPageWithNav {
  AbstractCompletionPage(DiagnosticsSite site)
      : super(site, 'completion', 'Code Completion',
            description: 'Latency statistics for code completion.');

  path.Context get pathContext;

  List<CompletionPerformance> get performanceItems;

  @override
  Future generateContent(Map<String, String> params) async {
    var completions = performanceItems;

    if (completions.isEmpty) {
      blankslate('No completions recorded.');
      return;
    }

    var fastCount =
        completions.where((c) => c.elapsedInMilliseconds <= 100).length;
    p('${completions.length} results; ${printPercentage(fastCount / completions.length)} within 100ms.');

    // draw a chart
    buf.writeln(
        '<div id="chart-div" style="width: 700px; height: 300px;"></div>');
    var rowData = StringBuffer();
    for (var i = completions.length - 1; i >= 0; i--) {
      // [' ', 101.5]
      if (rowData.isNotEmpty) {
        rowData.write(',');
      }
      rowData.write("[' ', ${completions[i].elapsedInMilliseconds}]");
    }
    buf.writeln('''
      <script type="text/javascript">
      google.charts.load('current', {'packages':['bar']});
      google.charts.setOnLoadCallback(drawChart);
      function drawChart() {
        var data = google.visualization.arrayToDataTable([
          ['Completions', 'Time'],
          $rowData
        ]);
        var options = { bars: 'vertical', vAxis: {format: 'decimal'}, height: 300 };
        var chart = new google.charts.Bar(document.getElementById('chart-div'));
        chart.draw(data, google.charts.Bar.convertOptions(options));
      }
      </script>
''');

    // emit the data as a table
    buf.writeln('<table>');
    buf.writeln(
        '<tr><th>Time</th><th>Results</th><th>Source</th><th>Snippet</th></tr>');
    for (var completion in completions) {
      var shortName = pathContext.basename(completion.path);
      buf.writeln('<tr>'
          '<td class="pre right">${printMilliseconds(completion.elapsedInMilliseconds)}</td>'
          '<td class="right">${completion.suggestionCountStr}</td>'
          '<td>${escape(shortName)}</td>'
          '<td><code>${escape(completion.snippet)}</code></td>'
          '</tr>');
    }
    buf.writeln('</table>');
  }
}

class AstPage extends DiagnosticPageWithNav {
  String _description;

  AstPage(DiagnosticsSite site)
      : super(site, 'ast', 'AST', description: 'The AST for a file.');

  @override
  String get description => _description ?? super.description;

  @override
  bool get showInNav => false;

  @override
  Future<void> generateContent(Map<String, String> params) async {
    var filePath = params['file'];
    if (filePath == null) {
      p('No file path provided.');
      return;
    }
    var driver = server.getAnalysisDriver(filePath);
    if (driver == null) {
      p('The file <code>${escape(filePath)}</code> is not being analyzed.',
          raw: true);
      return;
    }
    var result = await driver.getResult(filePath);
    if (result == null) {
      p(
          'An AST could not be produced for the file '
          '<code>${escape(filePath)}</code>.',
          raw: true);
      return;
    }

    var writer = AstWriter(buf);
    result.unit.accept(writer);
  }

  @override
  Future<void> generatePage(Map<String, String> params) async {
    try {
      _description = params['file'];
      await super.generatePage(params);
    } finally {
      _description = null;
    }
  }
}

class CommunicationsPage extends DiagnosticPageWithNav {
  CommunicationsPage(DiagnosticsSite site)
      : super(site, 'communications', 'Communications',
            description:
                'Latency statistics for analysis server communications.');

  @override
  Future generateContent(Map<String, String> params) async {
    void writeRow(List<String> data, {List<String> classes}) {
      buf.write('<tr>');
      for (var i = 0; i < data.length; i++) {
        var c = classes == null ? null : classes[i];
        if (c != null) {
          buf.write('<td class="$c">${escape(data[i])}</td>');
        } else {
          buf.write('<td>${escape(data[i])}</td>');
        }
      }
      buf.writeln('</tr>');
    }

    buf.writeln('<div class="columns">');

    if (server.performanceAfterStartup != null) {
      buf.writeln('<div class="column one-half">');

      h3('Current');
      _writePerformanceTable(server.performanceAfterStartup, writeRow);

      var time = server.uptime.toString();
      if (time.contains('.')) {
        time = time.substring(0, time.indexOf('.'));
      }
      buf.writeln(writeOption('Uptime', time));

      buf.write('</div>');
    }

    buf.writeln('<div class="column one-half">');

    h3('Startup');
    _writePerformanceTable(server.performanceDuringStartup, writeRow);

    if (server.performanceAfterStartup != null) {
      var startupTime = server.performanceAfterStartup.startTime -
          server.performanceDuringStartup.startTime;
      buf.writeln(
          writeOption('Initial analysis time', printMilliseconds(startupTime)));
    }

    buf.write('</div>');

    buf.write('</div>');
  }

  void _writePerformanceTable(ServerPerformance perf,
      void Function(List<String> data, {List<String> classes}) writeRow) {
    var requestCount = perf.requestCount;
    var latencyCount = perf.latencyCount;
    var averageLatency =
        latencyCount > 0 ? (perf.requestLatency ~/ latencyCount) : 0;
    var maximumLatency = perf.maxLatency;
    var slowRequestPercent =
        latencyCount > 0 ? (perf.slowRequestCount / latencyCount) : 0.0;

    buf.write('<table>');
    writeRow([printInteger(requestCount), 'requests'],
        classes: ['right', null]);
    writeRow([printInteger(latencyCount), 'requests with latency information'],
        classes: ['right', null]);
    if (latencyCount > 0) {
      writeRow([printMilliseconds(averageLatency), 'average latency'],
          classes: ['right', null]);
      writeRow([printMilliseconds(maximumLatency), 'maximum latency'],
          classes: ['right', null]);
      writeRow([printPercentage(slowRequestPercent), '> 150 ms latency'],
          classes: ['right', null]);
    }
    buf.write('</table>');
  }
}

class CompletionPage extends AbstractCompletionPage {
  @override
  AnalysisServer server;

  CompletionPage(DiagnosticsSite site, this.server) : super(site);

  CompletionDomainHandler get completionDomain => server.handlers
      .firstWhere((handler) => handler is CompletionDomainHandler);

  @override
  path.Context get pathContext =>
      completionDomain.server.resourceProvider.pathContext;

  @override
  List<CompletionPerformance> get performanceItems =>
      completionDomain.performanceList.items.toList();
}

class ContentsPage extends DiagnosticPageWithNav {
  String _description;

  ContentsPage(DiagnosticsSite site)
      : super(site, 'contents', 'Contents',
            description: 'The Contents/Overlay of a file.');

  @override
  String get description => _description ?? super.description;

  @override
  bool get showInNav => false;

  @override
  Future<void> generateContent(Map<String, String> params) async {
    var filePath = params['file'];
    if (filePath == null) {
      p('No file path provided.');
      return;
    }
    var driver = server.getAnalysisDriver(filePath);
    if (driver == null) {
      p('The file <code>${escape(filePath)}</code> is not being analyzed.',
          raw: true);
      return;
    }
    var file = server.resourceProvider.getFile(filePath);
    if (!file.exists) {
      p('The file <code>${escape(filePath)}</code> does not exist.', raw: true);
      return;
    }

    if (server.resourceProvider.hasOverlay(filePath)) {
      p('Showing overlay for file.');
    } else {
      p('Showing file system contents for file.');
    }

    pre(() {
      buf.write('<code>');
      buf.write(escape(file.readAsStringSync()));
      buf.writeln('</code>');
    });
  }

  @override
  Future<void> generatePage(Map<String, String> params) async {
    try {
      _description = params['file'];
      await super.generatePage(params);
    } finally {
      _description = null;
    }
  }
}

class ContextsPage extends DiagnosticPageWithNav {
  ContextsPage(DiagnosticsSite site)
      : super(site, 'contexts', 'Contexts',
            description:
                'An analysis context defines the options and the set of sources being analyzed.');

  @override
  String get navDetail => printInteger(server.driverMap.length);

  String describe(AnalysisOptionsImpl options) {
    var b = StringBuffer();

    b.write(writeOption('Implicit dynamic', options.implicitDynamic));
    b.write(writeOption('Implicit casts', options.implicitCasts));
    b.write(writeOption('Feature set', options.contextFeatures.toString()));
    b.write('<br>');

    b.write(writeOption('Generate hints', options.hint));

    return b.toString();
  }

  @override
  Future generateContent(Map<String, String> params) async {
    var driverMap = server.driverMap;
    if (driverMap.isEmpty) {
      blankslate('No contexts.');
      return;
    }

    var contextPath = params['context'];
    var folders = driverMap.keys.toList();
    folders
        .sort((first, second) => first.shortName.compareTo(second.shortName));
    var folder =
        folders.firstWhere((f) => f.path == contextPath, orElse: () => null);

    if (folder == null) {
      folder = folders.first;
      contextPath = folder.path;
    }

    var driver = driverMap[folder];

    buf.writeln('<div class="tabnav">');
    buf.writeln('<nav class="tabnav-tabs">');
    for (var f in folders) {
      if (f == folder) {
        buf.writeln(
            '<a class="tabnav-tab selected">${escape(f.shortName)}</a>');
      } else {
        var p = '${this.path}?context=${Uri.encodeQueryComponent(f.path)}';
        buf.writeln(
            '<a href="$p" class="tabnav-tab">${escape(f.shortName)}</a>');
      }
    }
    buf.writeln('</nav>');
    buf.writeln('</div>');

    buf.writeln(writeOption('Context location', escape(contextPath)));
    buf.writeln(writeOption('Analysis options path',
        escape(driver.contextRoot.optionsFilePath ?? 'none')));
    buf.writeln(
        writeOption('SDK root', escape(driver.analysisContext.sdkRoot.path)));

    buf.writeln('<div class="columns">');

{'fraction_non_alphanumeric': 0.14766858223612425, 'fraction_numerical': 0.008365461555248418, 'mean_word_length': 3.3844681868920428, 'pattern_counts': {'":': 0, '<': 171, '<?xml version=': 0, '>': 205, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5006634', 'n_tokens_mistral': 8864, 'n_tokens_neox': 8500, 'n_words': 1806}