From 14f3a14af55258b9de83f7690b688cef063f97e2 Mon Sep 17 00:00:00 2001 From: fireflyc Date: Tue, 8 Nov 2016 16:17:39 +0800 Subject: [PATCH] support formatter(use tidy) clear up directory structure ignore diectory `typing` --- .gitignore | 3 +- package.json | 168 +++++++++++------- priv/erl_tidy.escript | 52 ++++++ priv/fmt_test.erl | 20 +++ src/common/edit.ts | 126 +++++++++++++ src/common/output.ts | 17 ++ src/common/setting.ts | 34 ++++ src/common/utils.ts | 28 +++ src/extension.ts | 48 ++--- .../completion.ts} | 51 +++--- src/provider/formatter.ts | 32 ++++ typings.json | 5 + 12 files changed, 467 insertions(+), 117 deletions(-) create mode 100644 priv/erl_tidy.escript create mode 100644 priv/fmt_test.erl create mode 100644 src/common/edit.ts create mode 100644 src/common/output.ts create mode 100644 src/common/setting.ts create mode 100644 src/common/utils.ts rename src/{completion_provider.ts => provider/completion.ts} (57%) create mode 100644 src/provider/formatter.ts create mode 100644 typings.json diff --git a/.gitignore b/.gitignore index 8e5962e..8d9fd9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ out -node_modules \ No newline at end of file +node_modules +typings \ No newline at end of file diff --git a/package.json b/package.json index b75be4c..64e1acd 100644 --- a/package.json +++ b/package.json @@ -1,71 +1,103 @@ { - "name": "erlang-otp", - "displayName": "Erlang/OTP", - "description": "Erlang/OTP support with syntax highlighting, auto-indent and snippets", - "version": "0.2.1", - "author": { - "name": "Yuce Tekol" - }, - "publisher": "yuce", - "license": "SEE LICENSE IN LICENSE.md", - "engines": { - "vscode": "^0.10.6" - }, - "categories": [ - "Languages", - "Snippets" - ], - "activationEvents": [ - "onLanguage:erlang" - ], - "main": "./out/src/extension", - "contributes": { - "languages": [{ - "id": "erlang", - "aliases": ["Erlang", "erlang"], - "extensions": [".erl", ".hrl", ".yrl", ".escript", ".app.src", ".config"], - "filenames": [ - "rebar.lock" - ] - }], - "grammars": [{ - "language": "erlang", - "scopeName": "source.erlang", - "path": "./syntaxes/erlang.tmLanguage" - }], - "snippets": [ - { - "language": "erlang", - "path": "./snippets/erlang.json" - } + "name": "erlang-otp", + "displayName": "Erlang/OTP", + "description": "Erlang/OTP support with syntax highlighting, auto-indent and snippets", + "version": "0.2.1", + "author": { + "name": "Yuce Tekol" + }, + "publisher": "yuce", + "license": "SEE LICENSE IN LICENSE.md", + "engines": { + "vscode": "^0.10.6" + }, + "categories": [ + "Languages", + "Snippets" + ], + "activationEvents": [ + "onLanguage:erlang" + ], + "main": "./out/src/extension", + "contributes": { + "languages": [ + { + "id": "erlang", + "aliases": [ + "Erlang", + "erlang" ], - "configuration": { - "title": "Erlang configuration", - "properties": { - "erlang.enableExperimentalAutoComplete": { - "type": "boolean", - "default": false, - "description": "Enables experimental auto completion for Erlang standard library" - } - } + "extensions": [ + ".erl", + ".hrl", + ".yrl", + ".escript", + ".app.src", + ".config" + ], + "filenames": [ + "rebar.lock" + ] + } + ], + "grammars": [ + { + "language": "erlang", + "scopeName": "source.erlang", + "path": "./syntaxes/erlang.tmLanguage" + } + ], + "snippets": [ + { + "language": "erlang", + "path": "./snippets/erlang.json" + } + ], + "configuration": { + "title": "Erlang configuration", + "properties": { + "erlang.enableExperimentalAutoComplete": { + "type": "boolean", + "default": false, + "description": "Enables experimental auto completion for Erlang standard library" + }, + "erlang.escriptPath": { + "type": "string", + "default": "escript", + "description": "escriptPath" + }, + "erlang.rebar3Path": { + "type": "string", + "default": "rebar3", + "description": "rebar3Path" + }, + "erlang.erlangPath": { + "type": "string", + "default": "erl", + "description": "erlangPath" } - }, - "scripts": { - "vscode:prepublish": "node ./node_modules/vscode/bin/compile", - "compile": "node ./node_modules/vscode/bin/compile -watch -p ./", - "postinstall": "node ./node_modules/vscode/bin/install" - }, - "devDependencies": { - "typescript": "^1.7.5", - "vscode": "^0.11.0" - }, - "repository": { - "type": "git", - "url": "https://github.com/yuce/erlang-vscode" - }, - "icon": "images/erlang.png", - "bugs": { - "url": "https://github.com/yuce/erlang-vscode/issues" - }, - "homepage": "https://github.com/yuce/erlang-vscode/README.md" -} \ No newline at end of file + } + } + }, + "scripts": { + "vscode:prepublish": "node ./node_modules/vscode/bin/compile", + "compile": "node ./node_modules/vscode/bin/compile -watch -p ./", + "postinstall": "node ./node_modules/vscode/bin/install" + }, + "devDependencies": { + "typescript": "^1.7.5", + "vscode": "^0.11.0" + }, + "dependencies": { + "diff": "^3.0.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/yuce/erlang-vscode" + }, + "icon": "images/erlang.png", + "bugs": { + "url": "https://github.com/yuce/erlang-vscode/issues" + }, + "homepage": "https://github.com/yuce/erlang-vscode/README.md" +} diff --git a/priv/erl_tidy.escript b/priv/erl_tidy.escript new file mode 100644 index 0000000..db44682 --- /dev/null +++ b/priv/erl_tidy.escript @@ -0,0 +1,52 @@ +#!/usr/bin/env escript +%% -*- erlang -*- +%%! -smp enable -sname erl_tidy debug verbose + +-mode(compile). +-export([main/1]). + +%% erl_tidy.escript: does not support HRL ESCRIPT APP.SRC. + +main([]) -> + io:format(standard_error + ,"Usage: ~s \n" + ,[escript:script_name()] + ), + halt(1); +main(Files) -> + tidy_files(Files). + +%% Internals + +tidy_files(Paths) -> + lists:foreach(fun tidy/1, Paths). + +printer(AST, Options) -> + erl_prettypr:format(AST, [{paper, 115} + ,{ribbon, 100} + | Options + ]). + +tidy(Path) -> + case {filelib:is_regular(Path), filelib:is_dir(Path)} of + {true, _} -> + case filename:extension(Path) of + ".erl" -> + erl_tidy:file(Path, [{keep_unused, true} ,{stdout, true}, {printer, fun printer/2}]); + _ -> + skip(Path) + end; + {_, true} -> + RegExp = "\\.erl$", + Paths = filelib:fold_files(Path, RegExp, true, fun cons/2, []), + tidy_files(Paths); + _ -> + skip(Path) + end. + +cons(H, T) -> [H | T]. + +skip(Path) -> + io:format(standard_error, "Skipping ~s\n", [Path]). + +%% End of Module \ No newline at end of file diff --git a/priv/fmt_test.erl b/priv/fmt_test.erl new file mode 100644 index 0000000..c81e7d5 --- /dev/null +++ b/priv/fmt_test.erl @@ -0,0 +1,20 @@ +-module(binary_string). +-author("fireflyc"). + +%% API +-export([to_decimal/1]). + +to_decimal(String) -> + try + {_, Result} = lists:foldr(fun to_decimal/2, {0, 0}, String), + Result + + catch + _:_ -> 0 + + end. + + + +to_decimal($0, {N, Acc}) -> {N + 1, Acc}; +to_decimal($1, {N, Acc}) -> {N + 1, Acc + trunc(math:pow(2, N))}. \ No newline at end of file diff --git a/src/common/edit.ts b/src/common/edit.ts new file mode 100644 index 0000000..1ec822c --- /dev/null +++ b/src/common/edit.ts @@ -0,0 +1,126 @@ + +import * as vscode from 'vscode'; +import * as jsDiff from 'diff'; + +export interface FilePatch { + fileName: string; + edits: Edit[]; +} + +export enum EditTypes { EDIT_DELETE, EDIT_INSERT, EDIT_REPLACE }; + +export class Edit { + action: number; + start: vscode.Position; + end: vscode.Position; + text: string; + + constructor(action: number, start: vscode.Position) { + this.action = action; + this.start = start; + this.text = ''; + } + + // Creates TextEdit for current Edit + apply(): vscode.TextEdit { + switch (this.action) { + case EditTypes.EDIT_INSERT: + return vscode.TextEdit.insert(this.start, this.text); + + case EditTypes.EDIT_DELETE: + return vscode.TextEdit.delete(new vscode.Range(this.start, this.end)); + + case EditTypes.EDIT_REPLACE: + return vscode.TextEdit.replace(new vscode.Range(this.start, this.end), this.text); + } + } + + // Applies Edit using given TextEditorEdit + applyUsingTextEditorEdit(editBuilder: vscode.TextEditorEdit): void { + switch (this.action) { + case EditTypes.EDIT_INSERT: + editBuilder.insert(this.start, this.text); + break; + + case EditTypes.EDIT_DELETE: + editBuilder.delete(new vscode.Range(this.start, this.end)); + break; + + case EditTypes.EDIT_REPLACE: + editBuilder.replace(new vscode.Range(this.start, this.end), this.text); + break; + } + } + + // Applies Edits to given WorkspaceEdit + applyUsingWorkspaceEdit(workspaceEdit: vscode.WorkspaceEdit, fileUri: vscode.Uri): void { + switch (this.action) { + case EditTypes.EDIT_INSERT: + workspaceEdit.insert(fileUri, this.start, this.text); + break; + + case EditTypes.EDIT_DELETE: + workspaceEdit.delete(fileUri, new vscode.Range(this.start, this.end)); + break; + + case EditTypes.EDIT_REPLACE: + workspaceEdit.replace(fileUri, new vscode.Range(this.start, this.end), this.text); + break; + } + } +} + + +export function getEdits(fileName: string, oldStr: string, newStr: string): FilePatch { + if (process.platform === 'win32') { + oldStr = oldStr.split('\r\n').join('\n'); + newStr = newStr.split('\r\n').join('\n'); + } + let unifiedDiffs: jsDiff.IUniDiff = jsDiff.structuredPatch(fileName, fileName, oldStr, newStr, '', ''); + let filePatches: FilePatch[] = parseUniDiffs([unifiedDiffs]); + return filePatches[0]; +} + +function parseUniDiffs(diffOutput: jsDiff.IUniDiff[]): FilePatch[] { + let filePatches: FilePatch[] = []; + diffOutput.forEach((uniDiff: jsDiff.IUniDiff) => { + let edit: Edit = null; + let edits: Edit[] = []; + uniDiff.hunks.forEach((hunk: jsDiff.IHunk) => { + let startLine = hunk.oldStart; + hunk.lines.forEach((line) => { + switch (line.substr(0, 1)) { + case '-': + if (edit == null) { + edit = new Edit(EditTypes.EDIT_DELETE, new vscode.Position(startLine - 1, 0)); + } + edit.end = new vscode.Position(startLine, 0); + startLine++; + break; + case '+': + if (edit == null) { + edit = new Edit(EditTypes.EDIT_INSERT, new vscode.Position(startLine - 1, 0)); + } else if (edit.action === EditTypes.EDIT_DELETE) { + edit.action = EditTypes.EDIT_REPLACE; + } + edit.text += line.substr(1) + '\n'; + break; + case ' ': + startLine++; + if (edit != null) { + edits.push(edit); + } + edit = null; + break; + } + }); + if (edit != null) { + edits.push(edit); + } + }); + filePatches.push({ fileName: uniDiff.oldFileName, edits: edits }); + }); + + return filePatches; + +} \ No newline at end of file diff --git a/src/common/output.ts b/src/common/output.ts new file mode 100644 index 0000000..bbb629e --- /dev/null +++ b/src/common/output.ts @@ -0,0 +1,17 @@ +'use strict'; + +import * as vscode from 'vscode'; + +export class ErlangOutput { + constructor(private outputChannel: vscode.OutputChannel) { + + } + + public info(msg: string): void { + this.outputChannel.appendLine(`INFO: ${msg}`); + } + + public error(msg: string): void { + this.outputChannel.appendLine(`ERROR: ${msg}`); + } +} \ No newline at end of file diff --git a/src/common/setting.ts b/src/common/setting.ts new file mode 100644 index 0000000..666468d --- /dev/null +++ b/src/common/setting.ts @@ -0,0 +1,34 @@ +'use strict'; + +import * as vscode from 'vscode'; + +export class EralngSettings{ + public erlangPath: string; + public escriptPath: string; + public rebar3Path:string; + public enableExperimentalAutoComplete: boolean; + + private static erlangSettings: EralngSettings = new EralngSettings(); + constructor() { + if (EralngSettings.erlangSettings) { + throw new Error('Singleton class, Use getInstance method'); + } + vscode.workspace.onDidChangeConfiguration(() => { + this.initializeSettings(); + }); + + this.initializeSettings(); + } + + initializeSettings(){ + let config = vscode.workspace.getConfiguration('erlang'); + this.enableExperimentalAutoComplete = config.get('enableExperimentalAutoComplete'); + this.erlangPath = config.get('erlangPath'); + this.escriptPath = config.get('escriptPath'); + this.rebar3Path = config.get('rebar3Path'); + } + + public static getInstance(): EralngSettings { + return EralngSettings.erlangSettings; + } +} \ No newline at end of file diff --git a/src/common/utils.ts b/src/common/utils.ts new file mode 100644 index 0000000..13062a7 --- /dev/null +++ b/src/common/utils.ts @@ -0,0 +1,28 @@ +'use strict'; + +import * as vscode from 'vscode'; +import * as child_process from 'child_process'; + +function handleResponse(file: string, includeErrorAsResponse: boolean, error: Error, stdout: string, stderr: string): Promise { + return new Promise((resolve, reject) => { + if (includeErrorAsResponse && (stdout.length > 0 || stderr.length > 0)) { + return resolve(stdout + '\n' + stderr); + } + + let hasErrors = (error && error.message.length > 0) || (stderr && stderr.length > 0); + if (hasErrors && (typeof stdout !== 'string' || stdout.length === 0)) { + let errorMsg = (error && error.message) ? error.message : (stderr && stderr.length > 0 ? stderr + '' : ''); + return reject(errorMsg); + } + + resolve(stdout + ''); + }); +} + +export function execFileInternal(file: string, args: string[], options: child_process.ExecFileOptions, includeErrorAsResponse: boolean): Promise { + return new Promise((resolve, reject) => { + child_process.execFile(file, args, options, (error, stdout, stderr) => { + handleResponse(file, includeErrorAsResponse, error, stdout, stderr).then(resolve, reject); + }); + }); +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 4f9c215..fdbca61 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -29,13 +29,19 @@ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 'use strict'; -import {ExtensionContext, Disposable, workspace, window, languages, - Hover} from 'vscode'; -import {ErlangCompletionProvider} from './completion_provider'; -// import {range, debounce} from 'lodash'; +import * as vscode from 'vscode'; +import {ErlangCompletionProvider} from './provider/completion'; +import {EralngFormattingEditProvider} from './provider/formatter'; +import {EralngSettings} from './common/setting'; +import {ErlangOutput} from './common/output'; -export function activate(ctx: ExtensionContext) { - languages.setLanguageConfiguration('erlang', { +const ERLANG: vscode.DocumentFilter = { language: 'erlang', scheme: 'file' }; + +export function activate(ctx: vscode.ExtensionContext) { + let erlangOut = new ErlangOutput(vscode.window.createOutputChannel("erlang")); + let erlangSettings = EralngSettings.getInstance(); + + vscode.languages.setLanguageConfiguration(ERLANG.language, { indentationRules: { increaseIndentPattern: /^\s*([^%]*->|receive|if|fun|case\s+.*\s+of|try\s+.*\s+of|catch)\s*$/, decreaseIndentPattern: /^.*(;|\.)\s*$/, @@ -50,25 +56,25 @@ export function activate(ctx: ExtensionContext) { ['<<', '>>'] ], __characterPairSupport: { - autoClosingPairs: [ - { open: '{', close: '}' }, - { open: '[', close: ']' }, - { open: '(', close: ')' }, - { open: '<<', close: '>>', notIn: ['string', 'comment'] }, - { open: '"', close: '"', notIn: ['string'] }, - { open: '\'', close: '\'', notIn: ['string', 'comment'] } - ] - } + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '<<', close: '>>', notIn: ['string', 'comment'] }, + { open: '"', close: '"', notIn: ['string'] }, + { open: '\'', close: '\'', notIn: ['string', 'comment'] } + ] + } }); // enable auto completion - let config = workspace.getConfiguration('erlang'); - if (config['enableExperimentalAutoComplete']) { - let completionJsonPath = ctx.asAbsolutePath("./priv/erlang-libs.json"); - ctx.subscriptions.push(languages.registerCompletionItemProvider({ - language: 'erlang' - }, new ErlangCompletionProvider(completionJsonPath), ':')); + if (erlangSettings.enableExperimentalAutoComplete) { + ctx.subscriptions.push(vscode.languages.registerCompletionItemProvider(ERLANG, new ErlangCompletionProvider(ctx), ':')); } + + //formatting + const formatProvider = new EralngFormattingEditProvider(ctx, erlangOut, erlangSettings, vscode.workspace.rootPath); + ctx.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider(ERLANG, formatProvider)); } export function deactivate() { diff --git a/src/completion_provider.ts b/src/provider/completion.ts similarity index 57% rename from src/completion_provider.ts rename to src/provider/completion.ts index a20c72e..3bf192b 100644 --- a/src/completion_provider.ts +++ b/src/provider/completion.ts @@ -1,10 +1,7 @@ /// - -import {CompletionItemProvider, TextDocument, Position, CancellationToken, - CompletionItem, CompletionItemKind} from 'vscode'; - -let fs = require('fs'); +import * as vscode from 'vscode'; +import * as fs from 'fs'; const RE_MODULE = /(\w+):$/; @@ -13,30 +10,30 @@ interface FunctionCompletionData { // detail: string; } -export class ErlangCompletionProvider implements CompletionItemProvider { - private modules:any = null; +export class ErlangCompletionProvider implements vscode.CompletionItemProvider { + private modules: any = null; private moduleNames: string[] = null; - private genericCompletionItems: CompletionItem[] = null; + private genericCompletionItems: vscode.CompletionItem[] = null; + private completionPath: string; - constructor(private completionPath: string) {} + constructor(context: vscode.ExtensionContext) { + this.completionPath = context.asAbsolutePath("./priv/erlang-libs.json"); + } - public provideCompletionItems(doc: TextDocument, - pos: Position, - token: CancellationToken): Thenable - { - return new Promise((resolve, reject) => { - const line = doc.lineAt(pos.line); + public provideCompletionItems(doc: vscode.TextDocument, pos: vscode.Position, token: vscode.CancellationToken): Thenable { + return new Promise((resolve, reject) => { + const line = doc.lineAt(pos.line); const m = RE_MODULE.exec(line.text.substring(0, pos.character)); if (this.modules === null) { this.readCompletionJson(this.completionPath, modules => { this.modules = modules; - (m === null)? + (m === null) ? this.resolveModuleNames(resolve) : this.resolveFunNames(m[1], resolve); }); } else { - (m === null)? + (m === null) ? this.resolveModuleNames(resolve) : this.resolveFunNames(m[1], resolve); } @@ -44,7 +41,7 @@ export class ErlangCompletionProvider implements CompletionItemProvider { } private resolveFunNames(module, resolve) { - resolve(this.makeModuleFunsCompletion(module)); + resolve(this.makeModuleFunsCompletion(module)); } private resolveModuleNames(resolve) { @@ -54,27 +51,27 @@ export class ErlangCompletionProvider implements CompletionItemProvider { resolve(this.genericCompletionItems); } - private makeFunctionCompletionItem(name: string): CompletionItem { - const item = new CompletionItem(name); + private makeFunctionCompletionItem(name: string): vscode.CompletionItem { + const item = new vscode.CompletionItem(name); // item.documentation = cd.detail; - item.kind = CompletionItemKind.Function; + item.kind = vscode.CompletionItemKind.Function; return item; } - private makeModuleNameCompletionItem(name: string): CompletionItem { - const item = new CompletionItem(name); - item.kind = CompletionItemKind.Module; + private makeModuleNameCompletionItem(name: string): vscode.CompletionItem { + const item = new vscode.CompletionItem(name); + item.kind = vscode.CompletionItemKind.Module; return item; } - private makeModuleFunsCompletion(module: string): CompletionItem[] { + private makeModuleFunsCompletion(module: string): vscode.CompletionItem[] { const moduleFuns = this.modules[module] || []; return moduleFuns.map(name => { return this.makeFunctionCompletionItem(name); }); } - private makeGenericCompletion(): CompletionItem[] { + private makeGenericCompletion(): vscode.CompletionItem[] { const modules = this.modules || {}; const names = []; for (let k in modules) { @@ -93,7 +90,7 @@ export class ErlangCompletionProvider implements CompletionItemProvider { done({}); } else { - let d: any = JSON.parse(data); + let d: any = data.toJSON(); done(d); } }); diff --git a/src/provider/formatter.ts b/src/provider/formatter.ts new file mode 100644 index 0000000..34e9725 --- /dev/null +++ b/src/provider/formatter.ts @@ -0,0 +1,32 @@ +'use strict'; + +import * as vscode from 'vscode'; +import * as path from 'path'; +import {EralngSettings} from '../common/setting'; +import {ErlangOutput} from '../common/output'; +import {execFileInternal} from '../common/utils'; +import {getEdits} from '../common/edit'; + +export class EralngFormattingEditProvider implements vscode.DocumentFormattingEditProvider { + public constructor(private context: vscode.ExtensionContext, private output: ErlangOutput, private config: EralngSettings, protected workspaceRootPath: string) { + } + + public provideDocumentFormattingEdits(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken): Thenable { + let tidy = this.context.asAbsolutePath("./priv/erl_tidy.escript"); + let cwd = this.workspaceRootPath; + this.output.info(document.uri.fsPath); + return execFileInternal(this.config.escriptPath, [tidy, document.uri.fsPath], { cwd }, true).then(data => { + this.output.info(data); + let textEdits: vscode.TextEdit[] = []; + let filePatch = getEdits(document.fileName, document.getText(), data); + + filePatch.edits.forEach((edit) => { + textEdits.push(edit.apply()); + }); + return textEdits; + }).catch(error => { + this.output.error(error); + return []; + }); + } +} \ No newline at end of file diff --git a/typings.json b/typings.json new file mode 100644 index 0000000..dffcdf9 --- /dev/null +++ b/typings.json @@ -0,0 +1,5 @@ +{ + "globalDependencies": { + "diff": "registry:dt/diff#0.0.0+20160809232139" + } +}