Add 'packages/vscode/' from commit '58c04d96c28dc5750e87520b36d526cd692ed2bc'

git-subtree-dir: packages/vscode
git-subtree-mainline: 0aca1c51de
git-subtree-split: 58c04d96c2
This commit is contained in:
Tobe O 2020-02-15 18:28:59 -05:00
commit 24d8c0515d
27 changed files with 5516 additions and 0 deletions

View file

@ -0,0 +1,4 @@
out
node_modules
.vscode-test/
*.vsix

View file

@ -0,0 +1,36 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "npm: watch"
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test"
],
"outFiles": [
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "npm: watch"
}
]
}

View file

@ -0,0 +1,9 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
}
}

View file

@ -0,0 +1,20 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View file

@ -0,0 +1,9 @@
.vscode/**
.vscode-test/**
out/test/**
out/**/*.map
src/**
.gitignore
tsconfig.json
vsc-extension-quickstart.md
tslint.json

View file

@ -0,0 +1,7 @@
# Change Log
All notable changes to the "angel-dart-vscode" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [Unreleased]
- Initial release

View file

@ -0,0 +1,13 @@
# Angel Framework support for VSCode
![The Angel Framework](https://angel-dart.github.io/assets/images/logo.png)
This extension provides IDE support within VSCode for the
[Angel](https://angel-dart.github.io)
Web server framework. The goal of this package is to improve
the overall development experience when working with Angel.
## Features
- Dart snippets for Angel framework code
- Syntax highlighting for the Jael templating engine
- **COMING SOON:** Language server support for Jael

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,85 @@
{
"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"
],
"keywords": [
"angel",
"angel-dart",
"dart",
"jael",
"template",
"templating",
"flutter",
"fuchsia"
],
"activationEvents": [
"onLanguage:jael"
],
"main": "./out/extension",
"contributes": {
"_commands": [
{
"command": "extension.sayHello",
"title": "Hello World"
}
],
"languages": [
{
"id": "jael",
"aliases": [
"Jael"
],
"extensions": [
".jael"
],
"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"
}
}

View file

@ -0,0 +1,115 @@
{
".source.dart": {
"controller": {
"prefix": "angel:controller",
"description": "Generate an Angel Controller.",
"body": [
"import 'package:angel_framework/angel_framework.dart';",
"",
"@Expose('/$1')",
"class $1Controller extends Controller {",
" @Expose('/')",
" String hello() {",
" return 'Hello, world';",
" }",
" $2",
"}"
]
},
"websocket_controller": {
"prefix": "angel:websocket_controller",
"description": "Generate an Angel WebSocketController.",
"body": [
"import 'package:angel_websocket/server.dart';",
"",
"class $1Controller extends WebSocketController {",
" $1Controller(AngelWebSocket ws) : super(ws);",
"",
" @ExposeWs('ping')",
" void hello(WebSocketContext socket) {",
" socket.send('pong', {'message': 'Hello, world!'});",
" }",
" $2",
"}"
]
},
"model": {
"prefix": "angel:model",
"description": "Generate an Angel Model.",
"body": [
"import 'package:angel_model/angel_model.dart';",
"import 'package:angel_serialize/angel_serialize.dart';",
"part '$2.g.dart';",
"part '$2.serializer.g.dart';",
"",
"@serializable",
"abstract class _$1 extends Model {",
" $3",
"}"
]
},
"migration": {
"prefix": "angel:migration",
"description": "Generate an Angel ORM Migration.",
"body": [
"import 'package:angel_migration.dart/angel_migration.dart';",
"",
"class $1Migration extends Migration {",
" @override",
" void up(Schema schema) {",
" schema.create('$2', (table) {",
" table.serial('id').primaryKey();",
" table.date('created_at');",
" table.date('updated_at');",
" $3",
" });",
" }",
"",
" @override",
" void down(Schema schema) {",
" schema.drop('$2');",
" }",
"}"
]
},
"plugin": {
"prefix": "angel:plugin",
"description": "Generate an Angel plugin.",
"body": [
"import 'package:angel_framework/angel_framework.dart';",
"",
"AngelConfigurer $1() {",
" return (Angel app) async {",
" // Work some magic...",
" $2",
" };",
"}"
]
},
"test_driver": {
"prefix": "angel:test",
"description": "Generate an Angel test driver.",
"body": [
"import 'dart:io';",
"import 'package:$1/$1.dart' as $1;",
"import 'package:angel_framework/angel_framework.dart';",
"import 'package:angel_test/angel_test.dart';",
"import 'package:test/test.dart';",
"",
"main() async {",
" TestClient client;",
"",
" setUp(() async {",
" var app = new Angel();",
" await app.configure($1.configureServer);",
" client = await connectTo(app);",
" });",
"",
" tearDown(() => client.close());",
"",
" // Add your tests here...",
"}"
]
}
}
}

View file

@ -0,0 +1,53 @@
{
".source.jael": {
"block": {
"prefix": "block",
"description": "Insert a named <block> tag.",
"body": ["<block name=\"$1\">", " $2", "</block>"]
},
"comment": {
"prefix": "comment",
"description": "Insert a comment.",
"body": ["<!-- $1 -->"]
},
"declare": {
"prefix": "declare",
"description": "Insert a <declare> tag.",
"body": ["<declare $1=$2>", " $3", "</declare>"]
},
"element": {
"prefix": "element",
"description": "Insert a custom <element> tag.",
"body": ["<element name=\"$1\">", " $2", "</element>"]
},
"extend": {
"prefix": "extend",
"description": "Insert an <extend> tag.",
"body": ["<extend src=\"$1\">", " $2", "</extend>"]
},
"for-each": {
"prefix": "for-each",
"description": "Insert a <for-each> tag.",
"body": ["<${1:div} for-each=$2 as=\"$3\">", " $4", "</$1>"]
},
"include": {
"prefix": "include",
"description": "Insert an <include /> tag.",
"body": ["<include src=\"$1\"/>"]
},
"switch": {
"prefix": "switch",
"description": "Insert a <switch> tag.",
"body": [
"<switch value=$1>",
" <case value=$2>",
" $3",
" </case>",
" <default>",
" $4",
" </default>",
"</switch>"
]
}
}
}

View file

@ -0,0 +1,40 @@
"use strict";
import * as vscode from "vscode";
import { workspace } from "vscode";
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind,
Executable
} from "vscode-languageclient";
export function activate(context: vscode.ExtensionContext) {
const runOpts: Executable = {
command: "pub",
args: ["global", "run", "jael_language_server"]
};
const serverOptions: ServerOptions = {
run: runOpts,
debug: runOpts,
transport: TransportKind.stdio
};
const clientOptions: LanguageClientOptions = {
documentSelector: [
{
scheme: "file",
language: "jael"
}
],
synchronize: {
configurationSection: "jael",
fileEvents: workspace.createFileSystemWatcher("**/.jael")
}
};
const lsp = new LanguageClient("jael", "Jael", serverOptions, clientOptions);
context.subscriptions.push(lsp.start());
}
export function deactivate() {}

View file

@ -0,0 +1,28 @@
{
"attribution-notice": {
"author-of-most-of-this-file": "Danny Tuppeny",
"source": "https://github.com/Dart-Code/Dart-Code/blob/master/syntaxes/dart.json"
},
"comments": {
"blockComment": ["<!--", "-->"]
},
"brackets": [["{", "}"], ["[", "]"], ["(", ")"], ["<", ">"]],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "`", "close": "`", "notIn": ["string", "comment"] },
{ "open": "/**", "close": " */", "notIn": ["string"] }
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["<", ">"],
["'", "'"],
["\"", "\""],
["`", "`"]
]
}

View file

@ -0,0 +1,172 @@
{
"fileTypes": ["jael"],
"name": "Jael",
"scopeName": "source.jael",
"patterns": [
{
"include": "#expressions"
},
{
"match": "\\b(DOCTYPE|doctype)\\b",
"name": "keyword.doctype.jael"
},
{
"match": "[:,\\.]",
"name": "punctuation.jael"
},
{
"begin": "{{-?",
"end": "}}",
"beginCaptures": {
"0": {
"name": "keyword.operator.jael"
}
},
"endCaptures": {
"0": {
"name": "keyword.operator.jael"
}
},
"patterns": [{ "include": "#expressions" }, { "include": "#operators" }]
},
{
"begin": "<\\s*(script)[^>]*>",
"end": "(.*)<\\s*/\\s*(script)[^>]*>",
"beginCaptures": {
"1": {
"name": "keyword.tag.embedded.js.jael"
}
},
"endCaptures": {
"1": {
"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"
},
{
"match": "<\\s*/?\\s*([A-Za-z_][A-Za-z0-9_-]*)(\\s*[A-Za-z_][A-Za-z0-9_]*(?!=)\\s*)*\\b",
"captures": {
"1": {
"name": "keyword.tag.jael"
},
"2": {
"name": "storage.name.jael"
}
}
},
{
"match": "@[A-Za-z_][A-Za-z0-9_]*",
"name": "storage.argument.jael"
},
{
"match": "\\b(@?[A-Za-z_][A-Za-z0-9_]*)=",
"captures": {
"1": {
"name": "storage.name.jael"
}
}
},
{
"captures": {
"0": {
"name": "punctuation.definition.comment.jael"
}
},
"begin": "<!--",
"end": "-->",
"name": "comment.jael"
},
{
"include": "#operators"
},
{
"include": "source.html"
}
],
"repository": {
"operators": {
"patterns": [
{
"match": "((!?=)|\\+|\\*|-|/|\\?|(\\?\\.)|%|\\[|\\])",
"name": "keyword.operator.jael"
}
]
},
"expressions": {
"patterns": [
{
"match": "\\b[<>]\\b",
"name": "keyword.operator.jael"
},
{
"begin": "'",
"end": "'",
"name": "string.quoted.single.jael",
"patterns": [
{
"name": "constant.character.escape.jael",
"match": "\\\\[bfnrt']"
}
]
},
{
"begin": "\"",
"end": "\"",
"name": "string.quoted.double.jael",
"patterns": [
{
"name": "constant.character.escape.jael",
"match": "\\\\[bfnrt\"]"
}
]
},
{
"match": "\\b(true|false|null)\\b",
"name": "constant.language.jael"
},
{
"match": "\\b-?[0-9]+(\\.[0-9]+)?([Ee][0-9]+)?\\b",
"name": "constant.numeric.jael"
},
{
"match": "\\b0x[A-Fa-f0-9]+\\b",
"name": "constant.numeric.jael"
},
{
"match": "([A-Za-z_][A-Za-z0-9_]*)\\s*\\(",
"captures": {
"1": {
"name": "entity.name.function.jael"
}
}
}
]
}
}
}

View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "out",
"lib": [
"es6"
],
"sourceMap": true,
"rootDir": "src",
/* Strict Type-Checking Option */
"strict": true, /* enable all strict type-checking options */
/* Additional Checks */
"noUnusedLocals": true /* Report errors on unused locals. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
},
"exclude": [
"node_modules",
".vscode-test"
]
}

View file

@ -0,0 +1,33 @@
# Welcome to your VS Code Extension
## What's in the folder
* This folder contains all of the files necessary for your extension.
* `package.json` - this is the manifest file in which you declare your extension and command.
The sample plugin registers a command and defines its title and command name. With this information
VS Code can show the command in the command palette. It doesnt yet need to load the plugin.
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
The file exports one function, `activate`, which is called the very first time your extension is
activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
We pass the function containing the implementation of the command as the second parameter to
`registerCommand`.
## Get up and running straight away
* Press `F5` to open a new window with your extension loaded.
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
* Set breakpoints in your code inside `src/extension.ts` to debug your extension.
* Find output from your extension in the debug console.
## Make changes
* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
## Explore the API
* You can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts`.
## Run tests
* Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests`.
* Press `F5` to run the tests in a new window with your extension loaded.
* See the output of the test result in the debug console.
* Make changes to `test/extension.test.ts` or create new test files inside the `test` folder.
* By convention, the test runner will only consider files matching the name pattern `**.test.ts`.
* You can create folders inside the `test` folder to structure your tests any way you want.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
# See https://www.dartlang.org/guides/libraries/private-files
# Files and directories created by pub
.dart_tool/
.packages
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
# Avoid committing generated Javascript files:
*.dart.js
*.info.json # Produced by the --dump-info flag.
*.js # When generated by dart2js. Don't specify *.js if your
# project includes source files written in JavaScript.
*.js_
*.js.deps
*.js.map

View file

@ -0,0 +1,3 @@
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,68 @@
import 'dart:async';
import 'dart:io';
import 'package:args/args.dart';
import 'package:io/ansi.dart';
import 'package:io/io.dart';
import 'package:dart_language_server/dart_language_server.dart';
import 'package:jael_language_server/jael_language_server.dart';
main(List<String> args) async {
var argParser = new ArgParser()
..addFlag('help',
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:');
print(argParser.usage);
}
try {
var argResults = argParser.parse(args);
if (argResults['help'] as bool) {
printUsage();
return;
} else {
var jaelServer = new JaelLanguageServer();
if (argResults.wasParsed('log-file')) {
var f = new File(argResults['log-file'] as String);
await f.create(recursive: true);
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');
printUsage();
exitCode = ExitCode.usage.code;
}
}

View file

@ -0,0 +1 @@
export 'src/server.dart';

View file

@ -0,0 +1,153 @@
import 'package:jael/jael.dart';
import 'package:logging/logging.dart';
import 'package:symbol_table/symbol_table.dart';
import 'object.dart';
class Analyzer extends Parser {
final Logger logger;
Analyzer(Scanner scanner, this.logger) : super(scanner);
final errors = <JaelError>[];
var _scope = new SymbolTable<JaelObject>();
var allDefinitions = <Variable<JaelObject>>[];
SymbolTable<JaelObject> get parentScope =>
_scope.isRoot ? _scope : _scope.parent;
SymbolTable<JaelObject> get scope => _scope;
bool ensureAttributeIsPresent(Element element, String name) {
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) return null;
// Check if any custom element exists.
_scope
.resolve(element.tagName.name)
?.value
?.usages
?.add(new SymbolUsage(SymbolUsageType.read, element.span));
// Validate attrs
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') {
ensureAttributeIsConstantString(element, 'name');
//logger.info('Found <block> at ${element.span.start.toolString}');
} else if (element.tagName.name == 'case') {
ensureAttributeIsPresent(element, 'value');
//logger.info('Found <case> 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 (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') {
ensureAttributeIsConstantString(element, 'src');
//logger.info('Found <extend> at ${element.span.start.toolString}');
}
} else if (element is SelfClosingElement) {
if (element.tagName.name == 'include') {
//logger.info('Found <include> at ${element.span.start.toolString}');
ensureAttributeIsConstantString(element, 'src');
}
}
return element;
} finally {
_scope = _scope.parent;
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;
}
}

View file

@ -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('<!doctype');
if (document.doctype.html != null) _buffer.write(' html');
if (document.doctype.public != null) _buffer.write(' public');
if (document.doctype.url != null) {
_buffer.write('${document.doctype.url}');
}
_buffer.writeln();
}
_formatChild(document?.root);
return _buffer.toString();
}
void _formatChild(ElementChild child) {
if (child == null) return;
_applySpacing();
if (child is Text)
_buffer.write(child.text.span.text);
else if (child is TextNode)
_buffer.write(child.text.span.text);
else if (child is Element) _formatElement(child);
}
void _formatElement(Element element) {
_applySpacing();
_buffer.write('<${element.tagName.name}');
for (var attr in element.attributes) {
_buffer.write(' ${attr.name}');
if (attr.value != null) {
if (attr.value is Identifier) {
var id = attr.value as Identifier;
if (id.name == 'true') {
_buffer.write(id.name);
} else if (id.name != 'false') {
if (attr.nequ != null) _buffer.write('!=');
if (attr.equals != null) _buffer.write('=');
_buffer.write(id.name);
}
} else {
if (attr.nequ != null) _buffer.write('!=');
if (attr.equals != null) _buffer.write('=');
_buffer.write(attr.value.span.text);
}
}
}
if (element is SelfClosingElement) {
_buffer.writeln('/>');
} 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('</${element.tagName.name}>');
} else {
throw new ArgumentError();
}
}
}

View file

@ -0,0 +1,32 @@
import 'dart:collection';
import 'package:source_span/source_span.dart';
abstract class JaelObject {
final FileSpan span;
final usages = <SymbolUsage>[];
String get name;
JaelObject(this.span);
}
class JaelCustomElement extends JaelObject {
final String name;
final attributes = new SplayTreeSet<String>();
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;
SymbolUsage(this.type, this.span);
}
enum SymbolUsageType { definition, read }

View file

@ -0,0 +1,539 @@
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 'formatter.dart';
import 'object.dart';
class JaelLanguageServer extends LanguageServer {
var _diagnostics = new StreamController<Diagnostics>();
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<ApplyWorkspaceEditParams>();
@override
Stream<Diagnostics> get diagnostics => _diagnostics.stream;
@override
Future<void> get onDone => _done.future;
@override
Stream<ApplyWorkspaceEditParams> get workspaceEdits => _workspaceEdits.stream;
@override
Future<void> 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<ServerCapabilities> 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().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<File> 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().pipe(file.openWrite());
logger.info('Opened Jael file ${file.path}');
}
return file;
}
Future<Scanner> scannerForId(TextDocumentIdentifier documentId) async {
var file = await fileForId(documentId);
return scan(await file.readAsString(), sourceUrl: file.uri);
}
Future<Analyzer> 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<JaelObject> 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</name>';
});
});
} else if (value is JaelVariable) {
return new CompletionItem((b) {
b
..kind = CompletionItemKind.variable
..label = symbol.name;
});
}
return null;
}
void emitDiagnostics(String uri, Iterable<Diagnostic> 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<TextDocumentContentChangeEvent> 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<List> textDocumentCodeAction(TextDocumentIdentifier documentId,
Range range, CodeActionContext context) async {
// TODO: implement textDocumentCodeAction
return [];
}
@override
Future<CompletionList> 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<String> 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<JaelObject> 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<Location> 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<List<DocumentHighlight>> 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<Hover> 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<List<Location>> textDocumentImplementation(
TextDocumentIdentifier documentId, Position position) async {
var defn = await textDocumentDefinition(documentId, position);
return defn == null ? [] : [defn];
}
@override
Future<List<Location>> 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<WorkspaceEdit> 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<List<SymbolInformation>> 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<void> workspaceExecuteCommand(String command, List arguments) async {
// TODO: implement workspaceExecuteCommand
}
@override
Future<List<SymbolInformation>> workspaceSymbol(String query) async {
var values = <JaelObject>[];
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<List<TextEdit>> textDocumentFormatting(
TextDocumentIdentifier documentId,
FormattingOptions formattingOptions) async {
try {
var errors = <JaelError>[];
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);
}
}

View file

@ -0,0 +1,22 @@
name: jael_language_server
version: 0.0.0
description: Language Server Protocol implementation for the Jael templating engine.
author: Tobe Osakwe <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/vscode
environment:
sdk: ">=2.0.0-dev <3.0.0"
dependencies:
args: ^1.0.0
dart_language_server: ^0.1.3
file: ^5.0.0
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
source_span: ^1.0.0
string_scanner: ^1.0.0
symbol_table: ^2.0.0
executables:
jael_language_server: jael_language_server