blob: aff08f1a8cff0d92dc1d90eb8de0d2a3ea60e3a5 [file] [log] [blame]
// Copyright (c) 2018, 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 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/src/context_manager.dart'
show ContextManagerImpl;
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analysis_server/src/lsp/source_edits.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:path/path.dart' show dirname, join;
class TextDocumentChangeHandler
extends MessageHandler<DidChangeTextDocumentParams, void> {
TextDocumentChangeHandler(LspAnalysisServer server) : super(server);
Method get handlesMessage => Method.textDocument_didChange;
@override
LspJsonHandler<DidChangeTextDocumentParams> get jsonHandler =>
DidChangeTextDocumentParams.jsonHandler;
ErrorOr<void> handle(
DidChangeTextDocumentParams params, CancellationToken token) {
final path = pathOfDoc(params.textDocument);
return path.mapResult((path) => _changeFile(path, params));
}
ErrorOr<void> _changeFile(String path, DidChangeTextDocumentParams params) {
String oldContents;
if (server.resourceProvider.hasOverlay(path)) {
oldContents = server.resourceProvider.getFile(path).readAsStringSync();
}
// If we didn't have the file contents, the server and client are out of sync
// and this is a serious failure.
if (oldContents == null) {
return error(
ServerErrorCodes.ClientServerInconsistentState,
'Unable to edit document because the file was not previously opened: $path',
null,
);
}
final newContents =
applyEdits(oldContents, params.contentChanges, failureIsCritical: true);
return newContents.mapResult((newcontents) {
server.documentVersions[path] = params.textDocument;
server.updateOverlay(path, newContents.result);
return success();
});
}
}
class TextDocumentCloseHandler
extends MessageHandler<DidCloseTextDocumentParams, void> {
/// Whether analysis roots are based on open files and should be updated.
bool updateAnalysisRoots;
TextDocumentCloseHandler(LspAnalysisServer server, this.updateAnalysisRoots)
: super(server);
Method get handlesMessage => Method.textDocument_didClose;
@override
LspJsonHandler<DidCloseTextDocumentParams> get jsonHandler =>
DidCloseTextDocumentParams.jsonHandler;
ErrorOr<void> handle(
DidCloseTextDocumentParams params, CancellationToken token) {
final path = pathOfDoc(params.textDocument);
return path.mapResult((path) {
server.removePriorityFile(path);
server.documentVersions.remove(path);
server.updateOverlay(path, null);
if (updateAnalysisRoots) {
// If there are no other open files in this context, we can remove it
// from the analysis roots.
final contextFolder = server.contextManager.getContextFolderFor(path);
var hasOtherFilesInContext = false;
for (var otherDocPath in server.documentVersions.keys) {
if (server.contextManager.getContextFolderFor(otherDocPath) ==
contextFolder) {
hasOtherFilesInContext = true;
break;
}
}
if (!hasOtherFilesInContext) {
final projectFolder =
_findProjectFolder(server.resourceProvider, path);
server.updateAnalysisRoots([], [projectFolder]);
}
}
return success();
});
}
}
class TextDocumentOpenHandler
extends MessageHandler<DidOpenTextDocumentParams, void> {
/// Whether analysis roots are based on open files and should be updated.
bool updateAnalysisRoots;
TextDocumentOpenHandler(LspAnalysisServer server, this.updateAnalysisRoots)
: super(server);
Method get handlesMessage => Method.textDocument_didOpen;
@override
LspJsonHandler<DidOpenTextDocumentParams> get jsonHandler =>
DidOpenTextDocumentParams.jsonHandler;
ErrorOr<void> handle(
DidOpenTextDocumentParams params, CancellationToken token) {
final doc = params.textDocument;
final path = pathOfDocItem(doc);
return path.mapResult((path) {
// We don't get a VersionedTextDocumentIdentifier with a didOpen but we
// do get the necessary info to create one.
server.documentVersions[path] = new VersionedTextDocumentIdentifier(
params.textDocument.version,
params.textDocument.uri,
);
server.updateOverlay(path, doc.text);
final driver = server.contextManager.getDriverFor(path);
// If the file did not exist, and is "overlay only", it still should be
// analyzed. Add it to driver to which it should have been added.
driver?.addFile(path);
// If there was no current driver for this file, then we may need to add
// its project folder as an analysis root.
if (updateAnalysisRoots && driver == null) {
final projectFolder = _findProjectFolder(server.resourceProvider, path);
if (projectFolder != null) {
server.updateAnalysisRoots([projectFolder], []);
} else {
// There was no pubspec - ideally we should add just the file
// here but we don't currently support that.
// https://github.com/dart-lang/sdk/issues/32256
// Send a warning to the user, but only if we haven't already in the
// last 60 seconds.
if (lastSentAnalyzeOpenFilesWarnings == null ||
(DateTime.now()
.difference(lastSentAnalyzeOpenFilesWarnings)
.inSeconds >
60)) {
lastSentAnalyzeOpenFilesWarnings = DateTime.now();
server.showMessageToUser(
MessageType.Warning,
'When using onlyAnalyzeProjectsWithOpenFiles, files opened that '
'are not contained within project folders containing pubspec.yaml, '
'.packages or BUILD files will not be analyzed.');
}
}
}
server.addPriorityFile(path);
return success();
});
}
DateTime lastSentAnalyzeOpenFilesWarnings;
}
/// Finds the nearest ancestor to [filePath] that contains a pubspec/.packages/build file.
String _findProjectFolder(ResourceProvider resourceProvider, String filePath) {
// TODO(dantup): Is there something we can reuse for this?
var folder = dirname(filePath);
while (folder != dirname(folder)) {
final pubspec =
resourceProvider.getFile(join(folder, ContextManagerImpl.PUBSPEC_NAME));
final packages = resourceProvider
.getFile(join(folder, ContextManagerImpl.PACKAGE_SPEC_NAME));
final build = resourceProvider.getFile(join(folder, 'BUILD'));
if (pubspec.exists || packages.exists || build.exists) {
return folder;
}
folder = dirname(folder);
}
return null;
}