diff --git a/angel_vscode/package.json b/angel_vscode/package.json index e1399ade..efe929d6 100644 --- a/angel_vscode/package.json +++ b/angel_vscode/package.json @@ -1,85 +1,85 @@ { - "name": "angel-dart-vscode", - "displayName": "Angel", - "description": "Snippets and IDE support for the Angel server framework within VSCode.", - "version": "0.0.1", - "repository": { - "type": "git", - "url": "https://github.com/angel-dart/vscode" - }, - "icon": "media/logo.png", - "publisher": "thosakwe0541", - "engines": { - "vscode": "^1.28.0" - }, - "categories": [ - "Snippets" - ], - "keywords": [ - "angel", - "angel-dart", - "dart", - "jael", - "template", - "templating", - "flutter", - "fuchsia" - ], - "activationEvents": [ - "onLanguage:jael" - ], - "main": "./out/extension", - "contributes": { - "_commands": [ - { - "command": "extension.sayHello", - "title": "Hello World" - } + "name": "angel-dart-vscode", + "displayName": "Angel", + "description": "Snippets and IDE support for the Angel server framework within VSCode.", + "version": "0.2.0", + "repository": { + "type": "git", + "url": "https://github.com/angel-dart/vscode" + }, + "icon": "media/logo.png", + "publisher": "thosakwe0541", + "engines": { + "vscode": "^1.28.0" + }, + "categories": [ + "Snippets" ], - "languages": [ - { - "id": "jael", - "aliases": [ - "Jael" + "keywords": [ + "angel", + "angel-dart", + "dart", + "jael", + "template", + "templating", + "flutter", + "fuchsia" + ], + "activationEvents": [ + "onLanguage:jael" + ], + "main": "./out/extension", + "contributes": { + "_commands": [ + { + "command": "extension.sayHello", + "title": "Hello World" + } ], - "extensions": [ - ".jael" + "languages": [ + { + "id": "jael", + "aliases": [ + "Jael" + ], + "extensions": [ + ".jael" + ], + "configuration": "./syntaxes/jael-language-configuration.json" + } ], - "configuration": "./syntaxes/jael-language-configuration.json" - } - ], - "grammars": [ - { - "language": "jael", - "scopeName": "source.jael", - "path": "./syntaxes/jael.json" - } - ], - "snippets": [ - { - "language": "dart", - "path": "./snippets/angel.json" - }, - { - "language": "jael", - "path": "./snippets/jael.json" - } - ] - }, - "scripts": { - "vscode:prepublish": "npm run compile", - "compile": "tsc -p ./", - "watch": "tsc -watch -p ./", - "postinstall": "node ./node_modules/vscode/bin/install", - "test": "npm run compile && node ./node_modules/vscode/bin/test" - }, - "devDependencies": { - "@types/mocha": "^2.2.42", - "@types/node": "^7.0.43", - "typescript": "^2.6.1", - "vscode": "^1.1.6" - }, - "dependencies": { - "vscode-languageclient": "^5.1.1" - } -} + "grammars": [ + { + "language": "jael", + "scopeName": "source.jael", + "path": "./syntaxes/jael.json" + } + ], + "snippets": [ + { + "language": "dart", + "path": "./snippets/angel.json" + }, + { + "language": "jael", + "path": "./snippets/jael.json" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "postinstall": "node ./node_modules/vscode/bin/install", + "test": "npm run compile && node ./node_modules/vscode/bin/test" + }, + "devDependencies": { + "@types/mocha": "^2.2.42", + "@types/node": "^7.0.43", + "typescript": "^2.6.1", + "vscode": "^1.1.6" + }, + "dependencies": { + "vscode-languageclient": "^5.1.1" + } +} \ No newline at end of file diff --git a/angel_vscode/syntaxes/jael.json b/angel_vscode/syntaxes/jael.json index 6da35142..f682c915 100644 --- a/angel_vscode/syntaxes/jael.json +++ b/angel_vscode/syntaxes/jael.json @@ -31,19 +31,40 @@ }, { "begin": "<\\s*(script)[^>]*>", - "end": "<\\s*/\\s*(script)[^>]*>", + "end": "(.*)<\\s*/\\s*(script)[^>]*>", "beginCaptures": { "1": { - "name": "keyword.tag.jael" + "name": "keyword.tag.embedded.js.jael" } }, "endCaptures": { "1": { - "name": "keyword.tag.jael" + "name": "source.js", + "contentName": "source.js" + }, + "2": { + "name": "keyword.tag.embedded.js.jael" } }, + "contentName": "source.js", "patterns": [{ "include": "source.js" }] }, + { + "begin": "<\\s*(style)[^>]*>", + "end": "<\\s*/\\s*(style)[^>]*>", + "beginCaptures": { + "1": { + "name": "keyword.tag.embedded.css.jael" + } + }, + "endCaptures": { + "1": { + "name": "keyword.tag.embedded.css.jael" + } + }, + "contentName": "source.css", + "patterns": [{ "include": "source.css" }] + }, { "match": "\\b(block|declare|for-each|extend|if|include|switch)\\b", "name": "keyword.control.jael" diff --git a/jael_language_server/analysis_options.yaml b/jael_language_server/analysis_options.yaml new file mode 100644 index 00000000..eae1e42a --- /dev/null +++ b/jael_language_server/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/jael_language_server/bin/jael_language_server.dart b/jael_language_server/bin/jael_language_server.dart index f009cd1d..f90548a2 100644 --- a/jael_language_server/bin/jael_language_server.dart +++ b/jael_language_server/bin/jael_language_server.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:args/args.dart'; import 'package:io/ansi.dart'; @@ -8,7 +9,8 @@ import 'package:jael_language_server/jael_language_server.dart'; main(List args) async { var argParser = new ArgParser() ..addFlag('help', - abbr: 'h', negatable: false, help: 'Print this help information.'); + abbr: 'h', negatable: false, help: 'Print this help information.') + ..addOption('log-file', help: 'A path to which to write a log file.'); void printUsage() { print('usage: jael_language_server [options...]\n\nOptions:'); @@ -24,20 +26,39 @@ main(List args) async { } else { var jaelServer = new JaelLanguageServer(); - jaelServer.logger.onRecord.listen((rec) async { - // TODO: Remove this - var f = new File( - '/Users/thosakwe/Source/Angel/vscode/jael_language_server/.dart_tool/log.txt'); + if (argResults.wasParsed('log-file')) { + var f = new File(argResults['log-file'] as String); await f.create(recursive: true); - var sink = await f.openWrite(mode: FileMode.append); - sink.writeln(rec); - if (rec.error != null) sink.writeln(rec.error); - if (rec.stackTrace != null) sink.writeln(rec.stackTrace); - await sink.close(); - }); - var stdio = new StdIOLanguageServer.start(jaelServer); - await stdio.onDone; + jaelServer.logger.onRecord.listen((rec) async { + var sink = await f.openWrite(mode: FileMode.append); + sink.writeln(rec); + if (rec.error != null) sink.writeln(rec.error); + if (rec.stackTrace != null) sink.writeln(rec.stackTrace); + await sink.close(); + }); + } else { + jaelServer.logger.onRecord.listen((rec) async { + var sink = stderr; + sink.writeln(rec); + if (rec.error != null) sink.writeln(rec.error); + if (rec.stackTrace != null) sink.writeln(rec.stackTrace); + }); + } + + var spec = new ZoneSpecification( + handleUncaughtError: (self, parent, zone, error, stackTrace) { + jaelServer.logger.severe('Uncaught', error, stackTrace); + }, + print: (self, parent, zone, line) { + jaelServer.logger.info(line); + }, + ); + var zone = Zone.current.fork(specification: spec); + await zone.run(() async { + var stdio = new StdIOLanguageServer.start(jaelServer); + await stdio.onDone; + }); } } on ArgParserException catch (e) { print('${red.wrap('error')}: ${e.message}\n'); diff --git a/jael_language_server/lib/src/analyzer.dart b/jael_language_server/lib/src/analyzer.dart index 3b699989..3d47d057 100644 --- a/jael_language_server/lib/src/analyzer.dart +++ b/jael_language_server/lib/src/analyzer.dart @@ -17,32 +17,39 @@ class Analyzer extends Parser { SymbolTable get scope => _scope; bool ensureAttributeIsPresent(Element element, String name) { - if (element.getAttribute(name) == null) { - errors.add(new JaelError(JaelErrorSeverity.error, + if (element.getAttribute(name)?.value == null) { + addError(new JaelError(JaelErrorSeverity.error, 'Missing required attribute `$name`.', element.span)); return false; } return true; } + void addError(JaelError e) { + errors.add(e); + logger.severe(e.message, e.span.highlight()); + } + + bool ensureAttributeIsConstantString(Element element, String name) { + var a = element.getAttribute(name); + if (a?.value is! StringLiteral || a?.value == null) { + var e = new JaelError( + JaelErrorSeverity.warning, + "`$name` attribute should be a constant string literal.", + a?.span ?? element.tagName.span); + addError(e); + return false; + } + + return true; + } + @override Element parseElement() { try { _scope = _scope.createChild(); var element = super.parseElement(); - if (element == null) { - // TODO: ??? - if (next(TokenType.lt)) { - var tagName = parseIdentifier(); - if (tagName != null) { - errors.add( - new JaelError(JaelErrorSeverity.error, "Hmm", tagName.span)); - } - } - return null; - } - - logger.info('!!! ${element.tagName.name}'); + if (element == null) return null; // Check if any custom element exists. _scope @@ -52,48 +59,75 @@ class Analyzer extends Parser { ?.add(new SymbolUsage(SymbolUsageType.read, element.span)); // Validate attrs - // TODO: if, for-each + var forEach = element.getAttribute('for-each'); + if (forEach != null) { + var asAttr = element.getAttribute('as'); + if (asAttr != null) { + if (ensureAttributeIsConstantString(element, 'as')) { + var asName = asAttr.string.value; + _scope.create(asName, + value: new JaelVariable(asName, asAttr.span), constant: true); + } + } + + if (forEach.value != null) { + addError(new JaelError(JaelErrorSeverity.error, + 'Missing value for `for-each` directive.', forEach.span)); + } + } + + var iff = element.getAttribute('if'); + if (iff != null) { + if (iff.value != null) { + addError(new JaelError(JaelErrorSeverity.error, + 'Missing value for `iff` directive.', iff.span)); + } + } // Validate the tag itself if (element is RegularElement) { if (element.tagName.name == 'block') { - ensureAttributeIsPresent(element, 'name'); - logger.info('Found at ${element.span.start.toolString}'); + ensureAttributeIsConstantString(element, 'name'); + //logger.info('Found at ${element.span.start.toolString}'); } else if (element.tagName.name == 'case') { ensureAttributeIsPresent(element, 'value'); - logger.info('Found at ${element.span.start.toolString}'); + //logger.info('Found at ${element.span.start.toolString}'); + } else if (element.tagName.name == 'declare') { + if (element.attributes.isEmpty) { + addError(new JaelError( + JaelErrorSeverity.warning, + '`declare` directive does not define any new symbols.', + element.tagName.span)); + } else { + for (var attr in element.attributes) { + _scope.create(attr.name, + value: new JaelVariable(attr.name, attr.span)); + } + } } else if (element.tagName.name == 'element') { - if (ensureAttributeIsPresent(element, 'name')) { - var nameCtx = element.getAttribute('name').value; - - if (nameCtx is! StringLiteral) { - errors.add(new JaelError( - JaelErrorSeverity.warning, - "`name` attribute should be a constant string literal.", - nameCtx.span)); - } else { - var name = (nameCtx as StringLiteral).value; - logger.info( - 'Found custom element $name at ${element.span.start.toolString}'); - try { - var symbol = parentScope.create(name, - value: new JaelCustomElement(name, element.tagName.span), - constant: true); - allDefinitions.add(symbol); - } on StateError catch (e) { - errors.add(new JaelError( - JaelErrorSeverity.error, e.message, element.tagName.span)); - } + if (ensureAttributeIsConstantString(element, 'name')) { + var nameCtx = element.getAttribute('name').value as StringLiteral; + var name = nameCtx.value; + //logger.info( + // 'Found custom element $name at ${element.span.start.toolString}'); + try { + var symbol = parentScope.create(name, + value: new JaelCustomElement(name, element.tagName.span), + constant: true); + allDefinitions.add(symbol); + } on StateError catch (e) { + addError(new JaelError( + JaelErrorSeverity.error, e.message, element.tagName.span)); } } } else if (element.tagName.name == 'extend') { - ensureAttributeIsPresent(element, 'src'); - logger.info('Found at ${element.span.start.toolString}'); + ensureAttributeIsConstantString(element, 'src'); + //logger.info('Found at ${element.span.start.toolString}'); } } else if (element is SelfClosingElement) { if (element.tagName.name == 'include') { - logger.info('Found at ${element.span.start.toolString}'); - ensureAttributeIsPresent(element, 'src'); + //logger.info('Found at ${element.span.start.toolString}'); + ensureAttributeIsConstantString(element, 'src'); } } @@ -103,4 +137,17 @@ class Analyzer extends Parser { return null; } } + + @override + Expression parseExpression(int precedence) { + var expr = super.parseExpression(precedence); + if (expr == null) return null; + + if (expr is Identifier) { + var ref = _scope.resolve(expr.name); + ref?.value?.usages?.add(new SymbolUsage(SymbolUsageType.read, expr.span)); + } + + return expr; + } } diff --git a/jael_language_server/lib/src/formatter.dart b/jael_language_server/lib/src/formatter.dart new file mode 100644 index 00000000..0701758d --- /dev/null +++ b/jael_language_server/lib/src/formatter.dart @@ -0,0 +1,115 @@ +import 'package:jael/jael.dart'; + +class JaelFormatter { + final num tabSize; + final bool insertSpaces; + var _buffer = new StringBuffer(); + int _level = 0; + String _spaces; + + static String _spaceString(int tabSize) { + var b = new StringBuffer(); + for (int i = 0; i < tabSize; i++) { + b.write(' '); + } + return b.toString(); + } + + JaelFormatter(this.tabSize, this.insertSpaces) { + _spaces = insertSpaces ? _spaceString(tabSize.toInt()) : '\t'; + } + + void _indent() { + _level++; + } + + void _outdent() { + if (_level > 0) _level--; + } + + void _applySpacing() { + for (int i = 0; i < _level; i++) _buffer.write(_spaces); + } + + String apply(Document document) { + if (document?.doctype != null) { + _buffer.write(''); + } else if (element is RegularElement) { + if (element.children.length == 1 && + (element.children.first is Text || + element.children.first is TextNode)) { + _buffer.write('>'); + _buffer.write(element.children.first.span.text); + } else { + _buffer.writeln('>'); + _indent(); + element.children.forEach(_formatChild); + _outdent(); + } + + if (element.children.isNotEmpty && + (element.children.last is Text || + element.children.last is TextNode)) { + _buffer.writeln(); + } + + _applySpacing(); + _buffer.writeln(''); + } else { + throw new ArgumentError(); + } + } +} diff --git a/jael_language_server/lib/src/object.dart b/jael_language_server/lib/src/object.dart index 55644a50..d91f31aa 100644 --- a/jael_language_server/lib/src/object.dart +++ b/jael_language_server/lib/src/object.dart @@ -17,6 +17,11 @@ class JaelCustomElement extends JaelObject { JaelCustomElement(this.name, FileSpan span) : super(span); } +class JaelVariable extends JaelObject { + final String name; + JaelVariable(this.name, FileSpan span) : super(span); +} + class SymbolUsage { final SymbolUsageType type; final FileSpan span; diff --git a/jael_language_server/lib/src/server.dart b/jael_language_server/lib/src/server.dart index b2af10a5..e31fd23a 100644 --- a/jael_language_server/lib/src/server.dart +++ b/jael_language_server/lib/src/server.dart @@ -5,19 +5,22 @@ 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 'formatter.dart'; import 'object.dart'; class JaelLanguageServer extends LanguageServer { - var _diagnostics = new StreamController(sync: true); + var _diagnostics = new StreamController(); var _done = new Completer(); var _memFs = new MemoryFileSystem(); var _localFs = const LocalFileSystem(); - Directory _localRootDir; + Directory _localRootDir, _memRootDir; var logger = new Logger('jael'); Uri _rootUri; var _workspaceEdits = new StreamController(); @@ -39,25 +42,40 @@ class JaelLanguageServer extends LanguageServer { 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') { - var relativePath = - p.relative(entity.absolute.path, from: _localRootDir.absolute.path); - var file = _memFs.file(relativePath); + logger.info('HEY ${entity.path}'); + var file = _memFs.file(entity.absolute.path); await file.create(recursive: true); await entity.openRead().pipe(file.openWrite(mode: FileMode.write)); - logger.info('Found Jael file ${file.path}'); + logger.info( + 'Found Jael file ${file.path}; copied to ${file.absolute.path}'); // Analyze it var documentId = new TextDocumentIdentifier((b) { - b..uri = _rootUri.replace(path: relativePath).toString(); + b..uri = _rootUri.replace(path: file.path).toString(); }); await analyzerForId(documentId); @@ -66,7 +84,7 @@ class JaelLanguageServer extends LanguageServer { return new ServerCapabilities((b) { b - ..codeActionProvider = false + ..codeActionProvider = false ..completionProvider = new CompletionOptions((b) { b ..resolveProvider = true @@ -90,7 +108,7 @@ class JaelLanguageServer extends LanguageServer { ..textDocumentSync = new TextDocumentSyncOptions((b) { b ..openClose = true - ..change = TextDocumentSyncKind.incremental + ..change = TextDocumentSyncKind.full ..save = new SaveOptions((b) { b..includeText = false; }) @@ -103,8 +121,16 @@ class JaelLanguageServer extends LanguageServer { Future fileForId(TextDocumentIdentifier documentId) async { var uri = Uri.parse(documentId.uri); - var relativePath = p.relative(uri.path, from: _rootUri.path); - var file = _memFs.file(relativePath); + 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); @@ -124,9 +150,6 @@ class JaelLanguageServer extends LanguageServer { var scanner = await scannerForId(documentId); var analyzer = new Analyzer(scanner, logger)..errors.addAll(scanner.errors); analyzer.parseDocument(); - logger.info( - 'Done ${documentId.uri} ${await (await fileForId(documentId)).readAsString()}'); - logger.info(analyzer.errors); emitDiagnostics(documentId.uri, analyzer.errors.map(toDiagnostic).toList()); return analyzer; } @@ -203,14 +226,20 @@ class JaelLanguageServer extends LanguageServer { ..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) { - if (diagnostics.isEmpty) return; _diagnostics.add(new Diagnostics((b) { + logger.info('$uri => ${diagnostics.map((d) => d.message).toList()}'); b ..diagnostics = diagnostics.toList() ..uri = uri.toString(); @@ -230,9 +259,9 @@ class JaelLanguageServer extends LanguageServer { var file = await fileForId(id); for (var change in changes) { - if (change.range != null) { + if (change.text != null) { await file.writeAsString(change.text); - } else { + } else if (change.range != null) { var contents = await file.readAsString(); int findIndex(Position position) { @@ -254,6 +283,7 @@ class JaelLanguageServer extends LanguageServer { contents = contents.replaceRange(start, end, change.text); } + logger.info('${file.path} => $contents'); await file.writeAsString(contents); } } @@ -281,26 +311,48 @@ class JaelLanguageServer extends LanguageServer { }); } - Future currentSymbol( + 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 { - var analyzer = await analyzerForId(documentId); - var symbols = analyzer.allDefinitions; // analyzer.scope.allVariables; - logger.info('Current synmbols: ${symbols.map((v) => v.name)}'); + // First, read the file. + var file = await fileForId(documentId); + var contents = await file.readAsString(); - for (var s in symbols) { - var v = s.value; + // Next, find the current index. + var scanner = new SpanScanner(contents); - if (position.line == v.span.start.line && - position.character == v.span.start.column) { - logger.info('Success ${s.name}'); - return v; - } else { - logger.info( - 'Nope ${s.name} (${v.span.start.toolString} vs ${position.line}:${position.character})'); - } + while (!scanner.isDone && + (scanner.state.line != position.line || + scanner.state.column != position.character)) { + scanner.readChar(); } - return null; + // 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 @@ -337,9 +389,12 @@ class JaelLanguageServer extends LanguageServer { var symbol = await currentSymbol(documentId, position); if (symbol != null) { return new Hover((b) { - b..range = toRange(symbol.span); + b + ..contents = symbol.span.text + ..range = toRange(symbol.span); }); } + return null; } @@ -377,7 +432,10 @@ class JaelLanguageServer extends LanguageServer { return new TextEdit((b) { b ..range = toRange(u.span) - ..newText = newName; + ..newText = (symbol is JaelCustomElement && + u.type == SymbolUsageType.definition) + ? '"$newName"' + : newName; }); }).toList() }; @@ -409,11 +467,73 @@ class JaelLanguageServer extends LanguageServer { @override Future> workspaceSymbol(String query) async { - // TODO: implement workspaceSymbol - return []; + 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); + 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; + } } } 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); + } +} diff --git a/jael_language_server/pubspec.yaml b/jael_language_server/pubspec.yaml index 46593f79..d9ecb833 100644 --- a/jael_language_server/pubspec.yaml +++ b/jael_language_server/pubspec.yaml @@ -12,9 +12,11 @@ dependencies: io: ^0.3.2 jael: ^2.0.0 jael_preprocessor: ^2.0.0 + json_rpc_2: ^2.0.0 logging: ^0.11.3 path: ^1.0.0 - symbol_table: ^2.0.0 source_span: ^1.0.0 + string_scanner: ^1.0.0 + symbol_table: ^2.0.0 executables: jael_language_server: jael_language_server \ No newline at end of file