blob: 999ee2c7599cc49ba6e34b3a5bbb0667b3e42dc4 [file] [log] [blame]
// Copyright (c) 2019, 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:async';
import 'dart:io';
import 'package:analysis_server/src/edit/nnbd_migration/migration_info.dart';
import 'package:analysis_server/src/edit/nnbd_migration/migration_state.dart';
import 'package:analysis_server/src/edit/nnbd_migration/path_mapper.dart';
import 'package:analysis_server/src/edit/preview/dart_file_page.dart';
import 'package:analysis_server/src/edit/preview/exception_page.dart';
import 'package:analysis_server/src/edit/preview/highlight_css_page.dart';
import 'package:analysis_server/src/edit/preview/highlight_js_page.dart';
import 'package:analysis_server/src/edit/preview/http_preview_server.dart';
import 'package:analysis_server/src/edit/preview/index_file_page.dart';
import 'package:analysis_server/src/edit/preview/navigation_tree_page.dart';
import 'package:analysis_server/src/edit/preview/not_found_page.dart';
import 'package:analysis_server/src/edit/preview/region_page.dart';
import 'package:analysis_server/src/status/pages.dart';
import 'package:analyzer/file_system/file_system.dart';
/// The site used to serve pages for the preview tool.
class PreviewSite extends Site implements AbstractGetHandler {
/// The path of the CSS page used to style the semantic highlighting within a
/// Dart file.
static const highlightCssPath = '/highlight.css';
/// The path of the JS page used to associate highlighting within a Dart file.
static const highlightJsPath = '/highlight.pack.js';
static const navigationTreePath = '/_preview/navigationTree.json';
/// The state of the migration being previewed.
final MigrationState migrationState;
/// A table mapping the paths of files to the information about the
/// compilation units at those paths.
final Map<String, UnitInfo> unitInfoMap = {};
/// Initialize a newly created site to serve a preview of the results of an
/// NNBD migration.
PreviewSite(this.migrationState) : super('NNBD Migration Preview') {
Set<UnitInfo> unitInfos = migrationInfo.units;
ResourceProvider provider = pathMapper.provider;
for (UnitInfo unit in unitInfos) {
unitInfoMap[unit.path] = unit;
}
for (UnitInfo unit in migrationInfo.unitMap.values) {
if (!unitInfos.contains(unit)) {
if (unit.content == null) {
try {
unit.content = provider.getFile(unit.path).readAsStringSync();
} catch (_) {
// If we can't read the content of the file, then skip it.
continue;
}
}
unitInfoMap[unit.path] = unit;
}
}
}
/// Return the information about the migration that will be used to serve up
/// pages.
MigrationInfo get migrationInfo => migrationState.migrationInfo;
/// Return the path mapper used to map paths from the unit infos to the paths
/// being served.
PathMapper get pathMapper => migrationState.pathMapper;
@override
Page createExceptionPage(String message, StackTrace trace) {
// Use createExceptionPageWithPath instead.
throw UnimplementedError();
}
/// Return a page used to display an exception that occurred while attempting
/// to render another page. The [path] is the path to the page that was being
/// rendered when the exception was thrown. The [message] and [stackTrace] are
/// those from the exception.
Page createExceptionPageWithPath(
String path, String message, StackTrace stackTrace) {
return ExceptionPage(this, path, message, stackTrace);
}
@override
Page createUnknownPage(String unknownPath) {
return NotFoundPage(this, unknownPath.substring(1));
}
@override
Future<void> handleGetRequest(HttpRequest request) async {
Uri uri = request.uri;
if (uri.queryParameters.containsKey('replacement')) {
// TODO(devoncarew): We should only perform work on a 'POST' request.
performEdit(uri);
respondOk(request);
return;
}
String path = uri.path;
try {
if (path == highlightCssPath) {
// Note: `return await` needed due to
// https://github.com/dart-lang/sdk/issues/39204
return await respond(request, HighlightCssPage(this));
} else if (path == highlightJsPath) {
// Note: `return await` needed due to
// https://github.com/dart-lang/sdk/issues/39204
return await respond(request, HighlightJSPage(this));
} else if (path == navigationTreePath) {
// Note: `return await` needed due to
// https://github.com/dart-lang/sdk/issues/39204
return await respond(request, NavigationTreePage(this));
} else if (path == '/' ||
path == migrationInfo.includedRoot ||
path == '${migrationInfo.includedRoot}/') {
// Note: `return await` needed due to
// https://github.com/dart-lang/sdk/issues/39204
return await respond(request, IndexFilePage(this));
}
UnitInfo unitInfo = unitInfoMap[path];
if (unitInfo != null) {
if (uri.queryParameters.containsKey('inline')) {
// TODO(devoncarew): Ensure that we don't serve content outside of our project.
// Note: `return await` needed due to
// https://github.com/dart-lang/sdk/issues/39204
return await respond(request, DartFilePage(this, unitInfo));
} else if (uri.queryParameters.containsKey('region')) {
// TODO(devoncarew): Ensure that we don't serve content outside of our project.
// Note: `return await` needed due to
// https://github.com/dart-lang/sdk/issues/39204
return await respond(request, RegionPage(this, unitInfo));
} else {
// Note: `return await` needed due to
// https://github.com/dart-lang/sdk/issues/39204
return await respond(request, IndexFilePage(this));
}
}
// Note: `return await` needed due to
// https://github.com/dart-lang/sdk/issues/39204
return await respond(
request, createUnknownPage(path), HttpStatus.notFound);
} catch (exception, stackTrace) {
try {
await respond(
request,
createExceptionPageWithPath(path, '$exception', stackTrace),
HttpStatus.internalServerError);
} catch (exception, stackTrace) {
HttpResponse response = request.response;
response.statusCode = HttpStatus.internalServerError;
response.headers.contentType = ContentType.text;
response.write('$exception\n\n$stackTrace');
response.close();
}
}
}
/// Perform the edit indicated by the [uri].
void performEdit(Uri uri) {
//
// Update the code on disk.
//
Map<String, String> params = uri.queryParameters;
String path = uri.path;
int offset = int.parse(params['offset']);
int end = int.parse(params['end']);
String replacement = params['replacement'];
File file = pathMapper.provider.getFile(path);
String oldContent = file.readAsStringSync();
String newContent = oldContent.replaceRange(offset, end, replacement);
file.writeAsStringSync(newContent);
//
// Update the graph by adding or removing an edge.
//
int length = end - offset;
if (length == 0) {
throw UnsupportedError('Implement insertions');
} else {
throw UnsupportedError('Implement removals');
}
//
// Refresh the state of the migration.
//
//migrationState.refresh();
}
@override
Future<void> respond(HttpRequest request, Page page,
[int code = HttpStatus.ok]) async {
HttpResponse response = request.response;
response.statusCode = code;
if (page is HighlightCssPage) {
response.headers.contentType =
ContentType('text', 'css', charset: 'utf-8');
} else if (page is HighlightJSPage) {
response.headers.contentType =
ContentType('application', 'javascript', charset: 'utf-8');
} else {
response.headers.contentType = ContentType.html;
}
response.write(await page.generate(request.uri.queryParameters));
response.close();
}
}