Skip to content

Commit

Permalink
Initial LSP implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Mateus Felipe C. C. Pinto <mateusfccp@gmail.com>
  • Loading branch information
mateusfccp committed Aug 19, 2024
1 parent e779f3c commit 5281d46
Show file tree
Hide file tree
Showing 17 changed files with 539 additions and 164 deletions.
62 changes: 38 additions & 24 deletions bin/pinto.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import 'dart:convert';
import 'dart:io';

import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/util/sdk.dart';
import 'package:chalkdart/chalkstrings.dart';
import 'package:dart_style/dart_style.dart';
import 'package:exitcode/exitcode.dart';
import 'package:pinto/ast.dart';
import 'package:pinto/compiler.dart';
import 'package:pinto/error.dart';
import 'package:pinto/localization.dart';
import 'package:pinto/semantic.dart';

final _resourceProvider = PhysicalResourceProvider.INSTANCE;

Future<void> main(List<String> args) async {
if (args.length == 1) {
await runFile(args.single);
Expand All @@ -23,7 +30,24 @@ Future<void> runFile(String path) async {

if (await file.exists()) {
final fileString = file.readAsStringSync();
final error = await run(fileString);

stdout.writeln('Included paths: ${file.absolute.path}');

final analysisContextCollection = AnalysisContextCollection(
includedPaths: [file.absolute.path],
resourceProvider: _resourceProvider,
);

final sdk = FolderBasedDartSdk(
_resourceProvider,
_resourceProvider.getFolder(getSdkPath()),
);

final error = await run(
source: fileString,
analysisContextCollection: analysisContextCollection,
sdk: sdk,
);

switch (error) {
case PintoError():
Expand All @@ -37,7 +61,11 @@ Future<void> runFile(String path) async {
}
}

Future<PintoError?> run(String source) async {
Future<PintoError?> run({
required String source,
required AnalysisContextCollection analysisContextCollection,
required AbstractDartSdk sdk,
}) async {
final errorHandler = ErrorHandler();

final lineSplitter = LineSplitter(); // TODO(mateusfccp): Convert the handler into an interface and put this logic inside
Expand Down Expand Up @@ -91,25 +119,7 @@ Future<PintoError?> run(String source) async {
ScanError() => '[${error.location.line}:${error.location.column}]:',
};

final errorMessage = switch (error) {
// Parse errors
ExpectError(:final expectation) => "Expected to find $expectation.",
ExpectAfterError(:final token, :final expectation, :final after) => "Expected to find $expectation after $after. Found '${token.lexeme}'.",
ExpectBeforeError(:final expectation, :final before) => "Expected to find $expectation before $before.",

// Resolve errors
NoSymbolInScopeError(:final token) => "The symbol ${token.lexeme} was not found in the scope.",
TypeAlreadyDefinedError(:final token) => "The type parameter '${token.lexeme}' is already defined for this type. Try removing it or changing it's name.",
WrongNumberOfArgumentsError(:final token, argumentsCount: 1, expectedArgumentsCount: 0) => "The type '${token.lexeme}' don't accept arguments, but 1 argument was provided.",
WrongNumberOfArgumentsError(:final token, :final argumentsCount, expectedArgumentsCount: 0) => "The type '${token.lexeme}' don't accept arguments, but $argumentsCount arguments were provided.",
WrongNumberOfArgumentsError(:final token, argumentsCount: 0, :final expectedArgumentsCount) => "The type '${token.lexeme}' expects $expectedArgumentsCount arguments, but none was provided.",
WrongNumberOfArgumentsError(:final token, argumentsCount: 1, :final expectedArgumentsCount) => "The type '${token.lexeme}' expects $expectedArgumentsCount arguments, but 1 was provided.",
WrongNumberOfArgumentsError(:final token, :final argumentsCount, :final expectedArgumentsCount) =>
"The type '${token.lexeme}' expects $expectedArgumentsCount arguments, but $argumentsCount were provided.",

// Scan errors
UnexpectedCharacterError() => "Unexpected character '${error.character}'.",
};
final errorMessage = messageFromError(error);

final lineHint = switch (error) {
ScanError() => getLineWithErrorPointer(error.location.line, error.location.column, 1),
Expand All @@ -136,11 +146,15 @@ Future<PintoError?> run(String source) async {

final program = parser.parse();

final symbolsResolver = SymbolsResolver(
resourceProvider: _resourceProvider,
analysisContextCollection: analysisContextCollection,
sdk: sdk,
);

final resolver = Resolver(
program: program,
symbolsResolver: SymbolsResolver(
projectRoot: Directory.current.path,
),
symbolsResolver: symbolsResolver,
errorHandler: errorHandler,
);

Expand Down
77 changes: 77 additions & 0 deletions bin/pinto_server.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'dart:io';

import 'package:lsp_server/lsp_server.dart';

import 'src/analyzer.dart';

void main() async {
final connection = Connection(stdin, stdout);
final analyzer = Analyzer();

connection.onInitialize((params) async {
if (params.workspaceFolders case final worksPaceFolders?) {
for (final folder in worksPaceFolders) {
final directory = Directory.fromUri(folder.uri);
analyzer.addFileSystemEntity(directory);
}
} else if (params.rootUri case final uri?) {
final directory = Directory.fromUri(uri);
analyzer.addFileSystemEntity(directory);
} else if (params.rootPath case final path?) {
final directory = Directory(path);
analyzer.addFileSystemEntity(directory);
}

return InitializeResult(
capabilities: ServerCapabilities(
textDocumentSync: const Either2.t1(TextDocumentSyncKind.Full),
),
);
});

// Register a listener for when the client sends a notification when a text
// document was opened.
connection.onDidOpenTextDocument((parameters) async {
final file = File.fromUri(parameters.textDocument.uri);
analyzer.addFileSystemEntity(file);

final diagnostics = await analyzer.analyze(
parameters.textDocument.uri.path,
parameters.textDocument.text,
);

// Send back an event notifying the client of issues we want them to render.
// To clear issues the server is responsible for sending an empty list.
connection.sendDiagnostics(
PublishDiagnosticsParams(
diagnostics: diagnostics,
uri: parameters.textDocument.uri,
),
);
});

// Register a listener for when the client sends a notification when a text
// document was changed.
connection.onDidChangeTextDocument((parameters) async {
final contentChanges = parameters.contentChanges;
final contentChange = TextDocumentContentChangeEvent2.fromJson(
contentChanges[contentChanges.length - 1].toJson() as Map<String, Object?>,
);

final diagnostics = await analyzer.analyze(
parameters.textDocument.uri.path,
contentChange.text,
);

// Send back an event notifying the client of issues we want them to render.
// To clear issues the server is responsible for sending an empty list.
connection.sendDiagnostics(
PublishDiagnosticsParams(
diagnostics: diagnostics,
uri: parameters.textDocument.uri,
),
);
});

await connection.listen();
}
136 changes: 136 additions & 0 deletions bin/src/analyzer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import 'dart:collection';
import 'dart:io';

import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/dart/sdk/sdk.dart';
import 'package:analyzer/src/util/sdk.dart';
import 'package:lsp_server/lsp_server.dart';
import 'package:pinto/ast.dart';
import 'package:pinto/error.dart';
import 'package:pinto/localization.dart';
import 'package:pinto/semantic.dart';
import 'package:quiver/collection.dart';

final _resourceProvider = PhysicalResourceProvider.INSTANCE;

final class Analyzer {
final _analysisEntities = SplayTreeSet<String>();
final _analysisCache = <String, List<Diagnostic>>{};

late AnalysisContextCollection _analysisContextCollection;
late AbstractDartSdk _sdk;

Analyzer() {
_analysisContextCollection = AnalysisContextCollection(
includedPaths: [],
resourceProvider: _resourceProvider,
);

_sdk = FolderBasedDartSdk(
_resourceProvider,
_resourceProvider.getFolder(getSdkPath()),
);
}

void addFileSystemEntity(FileSystemEntity entity) {
final path = entity.absolute.path;
final analysisEntities = [..._analysisEntities];

for (final folder in analysisEntities) {
if (path.startsWith(folder)) {
return;
} else if (folder.startsWith(path)) {
_analysisEntities.remove(folder);
}
}

_analysisEntities.add(path);

if (!listsEqual([..._analysisEntities], analysisEntities)) {
_rebuildAnalysisContext();
}
}

void _rebuildAnalysisContext() {
_analysisContextCollection.dispose();

_analysisContextCollection = AnalysisContextCollection(
includedPaths: [..._analysisEntities],
resourceProvider: _resourceProvider,
);
}

Future<List<Diagnostic>> analyze(String path, String text) async {
final errorHandler = ErrorHandler();

final scanner = Scanner(
source: text,
errorHandler: errorHandler,
);

final tokens = scanner.scanTokens();

final parser = Parser(
tokens: tokens,
errorHandler: errorHandler,
);

final program = parser.parse();

final symbolsResolver = SymbolsResolver(
resourceProvider: _resourceProvider,
analysisContextCollection: _analysisContextCollection,
sdk: _sdk,
);

final resolver = Resolver(
program: program,
symbolsResolver: symbolsResolver,
errorHandler: errorHandler,
);

await resolver.resolve();

final diagnostics = [
for (final error in errorHandler.errors) _diagnosticFromError(error),
];

_analysisCache[path] = diagnostics;

return diagnostics;
}
}

Diagnostic _diagnosticFromError(PintoError error) {
final start = switch (error) {
ScanError(:final location) => Position(
character: location.column - 1,
line: location.line - 1,
),
ParseError(:final token) || ResolveError(:final token) => Position(
character: token.column - token.lexeme.length,
line: token.line - 1,
),
};

final end = switch (error) {
ScanError(:final location) => Position(
character: location.column,
line: location.line - 1,
),
ParseError(:final token) || ResolveError(:final token) => Position(
character: token.column,
line: token.line - 1,
),
};

final range = Range(start: start, end: end);

final message = messageFromError(error);

return Diagnostic(
message: message,
range: range,
);
}
5 changes: 3 additions & 2 deletions grammar.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
<program> ::= <import>* <type_definition>*

<import> ::= "import" ( <dart_import> | <package_import> )
<dart_import> ::= "@" <identifier>
<package_import> ::= <identifier> ( "/" <identifier> )*
<dart_import> ::= "@" <import_identifier>
<package_import> ::= <import_identifier>
<import_identifier> ::= <identifier> ( "/" <identifier> )*

<type_definition> ::= "type" <identifier> ( "(" <named_type_literal> ( "," <named_type_literal> )* ")" )? "=" <type_variation> ( "+" <type_variation> )*
<type_variant> ::= <identifier> ( "(" <type_variant_parameter> ( "," <type_variant_parameter> )* ")" )?
Expand Down
3 changes: 3 additions & 0 deletions lib/localization.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
library;

export 'src/localization.dart';
Loading

0 comments on commit 5281d46

Please sign in to comment.