import 'dart:async'; //import 'package:dart_language_server/src/protocol/language_server/interface.dart'; //import 'package:dart_language_server/src/protocol/language_server/messages.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:file/memory.dart'; import 'package:jael/jael.dart'; import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc_2; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; import 'package:string_scanner/string_scanner.dart'; import 'package:symbol_table/symbol_table.dart'; import 'analyzer.dart'; import 'object.dart'; import 'protocol/language_server/interface.dart'; import 'protocol/language_server/messages.dart'; class JaelLanguageServer extends LanguageServer { var _diagnostics = new StreamController(); var _done = new Completer(); var _memFs = new MemoryFileSystem(); var _localFs = const LocalFileSystem(); Directory _localRootDir, _memRootDir; var logger = new Logger('jael'); Uri _rootUri; var _workspaceEdits = new StreamController(); @override Stream get diagnostics => _diagnostics.stream; @override Future get onDone => _done.future; @override Stream get workspaceEdits => _workspaceEdits.stream; @override Future shutdown() { if (!_done.isCompleted) _done.complete(); _diagnostics.close(); _workspaceEdits.close(); return super.shutdown(); } @override void setupExtraMethods(json_rpc_2.Peer peer) { peer.registerMethod('textDocument/formatting', (json_rpc_2.Parameters params) async { var documentId = new TextDocumentIdentifier.fromJson(params['textDocument'].asMap); var formattingOptions = new FormattingOptions.fromJson(params['options'].asMap); return await textDocumentFormatting(documentId, formattingOptions); }); } @override Future initialize(int clientPid, String rootUri, ClientCapabilities clientCapabilities, String trace) async { // Find our real root dir. _localRootDir = _localFs.directory(_rootUri = Uri.parse(rootUri)); _memRootDir = _memFs.directory('/'); await _memRootDir.create(recursive: true); _memFs.currentDirectory = _memRootDir; // Copy all real files that end in *.jael (and *.jl for legacy) into the in-memory filesystem. await for (var entity in _localRootDir.list(recursive: true)) { if (entity is File && p.extension(entity.path) == '.jael') { logger.info('HEY ${entity.path}'); var file = _memFs.file(entity.absolute.path); await file.create(recursive: true); await entity .openRead() .cast>() .pipe(file.openWrite(mode: FileMode.write)); logger.info( 'Found Jael file ${file.path}; copied to ${file.absolute.path}'); // Analyze it var documentId = new TextDocumentIdentifier((b) { b..uri = _rootUri.replace(path: file.path).toString(); }); await analyzerForId(documentId); } } return new ServerCapabilities((b) { b ..codeActionProvider = false ..completionProvider = new CompletionOptions((b) { b ..resolveProvider = true ..triggerCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdeghijklmnopqrstuvxwyz' .codeUnits .map((c) => new String.fromCharCode(c)) .toList(); }) ..definitionProvider = true ..documentHighlightProvider = true ..documentRangeFormattingProvider = false ..documentOnTypeFormattingProvider = null ..documentSymbolProvider = true ..documentFormattingProvider = true ..hoverProvider = true ..implementationProvider = true ..referencesProvider = true ..renameProvider = true ..signatureHelpProvider = new SignatureHelpOptions((b) {}) ..textDocumentSync = new TextDocumentSyncOptions((b) { b ..openClose = true ..change = TextDocumentSyncKind.full ..save = new SaveOptions((b) { b..includeText = false; }) ..willSave = false ..willSaveWaitUntil = false; }) ..workspaceSymbolProvider = true; }); } Future fileForId(TextDocumentIdentifier documentId) async { var uri = Uri.parse(documentId.uri); var relativePath = uri.path; var file = _memFs.directory('/').childFile(relativePath); /* logger.info('Searching for $relativePath. All:\n'); await for (var entity in _memFs.directory('/').list(recursive: true)) { if (entity is File) print(' * ${entity.absolute.path}'); } */ if (!await file.exists()) { await file.create(recursive: true); await _localFs .file(uri) .openRead() .cast>() .pipe(file.openWrite()); logger.info('Opened Jael file ${file.path}'); } return file; } Future scannerForId(TextDocumentIdentifier documentId) async { var file = await fileForId(documentId); return scan(await file.readAsString(), sourceUrl: file.uri); } Future analyzerForId(TextDocumentIdentifier documentId) async { var scanner = await scannerForId(documentId); var analyzer = new Analyzer(scanner, logger)..errors.addAll(scanner.errors); analyzer.parseDocument(); emitDiagnostics(documentId.uri, analyzer.errors.map(toDiagnostic).toList()); return analyzer; } Diagnostic toDiagnostic(JaelError e) { return new Diagnostic((b) { b ..message = e.message ..range = toRange(e.span) ..severity = toSeverity(e.severity) ..source = e.span.start.sourceUrl.toString(); }); } int toSeverity(JaelErrorSeverity s) { switch (s) { case JaelErrorSeverity.warning: return DiagnosticSeverity.warning; default: return DiagnosticSeverity.error; } } Range toRange(FileSpan span) { return new Range((b) { b ..start = toPosition(span.start) ..end = toPosition(span.end); }); } Range emptyRange() { return new Range((b) => b ..start = b.end = new Position((b) { b ..character = 1 ..line = 0; })); } Position toPosition(SourceLocation location) { return new Position((b) { b ..line = location.line ..character = location.column; }); } Location toLocation(String uri, FileSpan span) { return new Location((b) { b ..range = toRange(span) ..uri = uri; }); } bool isReachable(JaelObject obj, Position position) { return obj.span.start.line <= position.line && obj.span.start.column <= position.character; } CompletionItem toCompletion(Variable symbol) { var value = symbol.value; if (value is JaelCustomElement) { var name = value.name; return new CompletionItem((b) { b ..kind = CompletionItemKind.classKind ..label = symbol.name ..textEdit = new TextEdit((b) { b ..range = emptyRange() ..newText = '<$name\$1>\n \$2\n'; }); }); } else if (value is JaelVariable) { return new CompletionItem((b) { b ..kind = CompletionItemKind.variable ..label = symbol.name; }); } return null; } void emitDiagnostics(String uri, Iterable diagnostics) { _diagnostics.add(new Diagnostics((b) { logger.info('$uri => ${diagnostics.map((d) => d.message).toList()}'); b ..diagnostics = diagnostics.toList() ..uri = uri.toString(); })); } @override Future textDocumentDidOpen(TextDocumentItem document) async { await analyzerForId( new TextDocumentIdentifier((b) => b..uri = document.uri)); } @override Future textDocumentDidChange(VersionedTextDocumentIdentifier documentId, List changes) async { var id = new TextDocumentIdentifier((b) => b..uri = documentId.uri); var file = await fileForId(id); for (var change in changes) { if (change.text != null) { await file.writeAsString(change.text); } else if (change.range != null) { var contents = await file.readAsString(); int findIndex(Position position) { var lines = contents.split('\n'); // Sum the length of the previous lines. int lineLength = lines .take(position.line - 1) .map((s) => s.length) .reduce((a, b) => a + b); return lineLength + position.character - 1; } if (change.range == null) { contents = change.text; } else { var start = findIndex(change.range.start), end = findIndex(change.range.end); contents = contents.replaceRange(start, end, change.text); } logger.info('${file.path} => $contents'); await file.writeAsString(contents); } } await analyzerForId(id); } @override Future textDocumentCodeAction(TextDocumentIdentifier documentId, Range range, CodeActionContext context) async { // TODO: implement textDocumentCodeAction return []; } @override Future textDocumentCompletion( TextDocumentIdentifier documentId, Position position) async { var analyzer = await analyzerForId(documentId); var symbols = analyzer.scope.allVariables; var reachable = symbols.where((s) => isReachable(s.value, position)); return new CompletionList((b) { b ..isIncomplete = false ..items = reachable.map(toCompletion).toList(); }); } final RegExp _id = new RegExp(r'(([A-Za-z][A-Za-z0-9_]*-)*([A-Za-z][A-Za-z0-9_]*))'); Future currentName( TextDocumentIdentifier documentId, Position position) async { // First, read the file. var file = await fileForId(documentId); var contents = await file.readAsString(); // Next, find the current index. var scanner = new SpanScanner(contents); while (!scanner.isDone && (scanner.state.line != position.line || scanner.state.column != position.character)) { scanner.readChar(); } // Next, just read the name. if (scanner.matches(_id)) { var longest = scanner.lastSpan.text; while (scanner.matches(_id) && scanner.position > 0 && !scanner.isDone) { longest = scanner.lastSpan.text; scanner.position--; } return longest; } else { return null; } } Future currentSymbol( TextDocumentIdentifier documentId, Position position) async { var name = await currentName(documentId, position); if (name == null) return null; var analyzer = await analyzerForId(documentId); var symbols = analyzer.allDefinitions ?? analyzer.scope.allVariables; logger .info('Current symbols, seeking $name: ${symbols.map((v) => v.name)}'); return analyzer.scope.resolve(name)?.value; } @override Future textDocumentDefinition( TextDocumentIdentifier documentId, Position position) async { var symbol = await currentSymbol(documentId, position); if (symbol != null) { return toLocation(documentId.uri, symbol.span); } return null; } @override Future> textDocumentHighlight( TextDocumentIdentifier documentId, Position position) async { var symbol = await currentSymbol(documentId, position); if (symbol != null) { return symbol.usages.map((u) { return new DocumentHighlight((b) { b ..range = toRange(u.span) ..kind = u.type == SymbolUsageType.definition ? DocumentHighlightKind.write : DocumentHighlightKind.read; }); }).toList(); } return []; } @override Future textDocumentHover( TextDocumentIdentifier documentId, Position position) async { var symbol = await currentSymbol(documentId, position); if (symbol != null) { return new Hover((b) { b ..contents = symbol.span.text ..range = toRange(symbol.span); }); } return null; } @override Future> textDocumentImplementation( TextDocumentIdentifier documentId, Position position) async { var defn = await textDocumentDefinition(documentId, position); return defn == null ? [] : [defn]; } @override Future> textDocumentReferences( TextDocumentIdentifier documentId, Position position, ReferenceContext context) async { var symbol = await currentSymbol(documentId, position); if (symbol != null) { return symbol.usages.map((u) { return toLocation(documentId.uri, u.span); }).toList(); } return []; } @override Future textDocumentRename(TextDocumentIdentifier documentId, Position position, String newName) async { var symbol = await currentSymbol(documentId, position); if (symbol != null) { return new WorkspaceEdit((b) { b ..changes = { symbol.name: symbol.usages.map((u) { return new TextEdit((b) { b ..range = toRange(u.span) ..newText = (symbol is JaelCustomElement && u.type == SymbolUsageType.definition) ? '"$newName"' : newName; }); }).toList() }; }); } return new WorkspaceEdit((b) { b..changes = {}; }); } @override Future> textDocumentSymbols( TextDocumentIdentifier documentId) async { var analyzer = await analyzerForId(documentId); return analyzer.allDefinitions.map((symbol) { return new SymbolInformation((b) { b ..kind = SymbolKind.classSymbol ..name = symbol.name ..location = toLocation(documentId.uri, symbol.value.span); }); }).toList(); } @override Future workspaceExecuteCommand(String command, List arguments) async { // TODO: implement workspaceExecuteCommand } @override Future> workspaceSymbol(String query) async { var values = []; await for (var file in _memRootDir.list(recursive: true)) { if (file is File) { var id = new TextDocumentIdentifier((b) { b..uri = file.uri.toString(); }); var analyzer = await analyzerForId(id); values.addAll(analyzer.allDefinitions.map((v) => v.value)); } } return values.map((o) { return new SymbolInformation((b) { b ..name = o.name ..location = toLocation(o.span.sourceUrl.toString(), o.span) ..containerName = p.basename(o.span.sourceUrl.path) ..kind = o is JaelCustomElement ? SymbolKind.classSymbol : SymbolKind.variable; }); }).toList(); } Future> textDocumentFormatting( TextDocumentIdentifier documentId, FormattingOptions formattingOptions) async { try { var errors = []; var file = await fileForId(documentId); var contents = await file.readAsString(); var document = parseDocument(contents, sourceUrl: file.uri, onError: errors.add); if (errors.isNotEmpty) return null; var formatter = new JaelFormatter( formattingOptions.tabSize, formattingOptions.insertSpaces, 80); var formatted = formatter.apply(document); logger.info('Original:${contents}\nFormatted:\n$formatted'); if (formatted.isNotEmpty) await file.writeAsString(formatted); return [ new TextEdit((b) { b ..newText = formatted ..range = document == null ? emptyRange() : toRange(document.span); }) ]; } catch (e, st) { logger.severe('Formatter error', e, st); return null; } } @override void initialized() { // TODO: implement initialized } @override // TODO: implement showMessages Stream get showMessages => null; } abstract class DiagnosticSeverity { static const int error = 0, warning = 1, information = 2, hint = 3; } class FormattingOptions { final num tabSize; final bool insertSpaces; FormattingOptions(this.tabSize, this.insertSpaces); factory FormattingOptions.fromJson(Map json) { return new FormattingOptions( json['tabSize'] as num, json['insertSpaces'] as bool); } }