├── test ├── resources │ ├── projects │ │ ├── rebar3project │ │ │ └── readme.md │ │ └── singlefile │ │ │ └── single.erl │ └── readme.md ├── test-fixtures │ ├── fixture1 │ │ └── fixture1.erl │ └── fixture-navigation │ │ ├── navigation_target.erl │ │ └── navigation_source.erl └── test-suite │ ├── index.ts │ └── extension.test.ts ├── rebar3 ├── rebar3.bat ├── images ├── icon.png ├── erlIcon.png ├── erlang-fileicon-dark.png ├── erlang-fileicon-light.png ├── lsp_validate_document.png ├── vscode-erlang-build.png ├── vscode-erlang-debug.png ├── vscode-erlang-editing.gif ├── vscode-erlang-hover.png ├── vscode-lsp-migration.png ├── vscode-erlang-codelens.png ├── vscode-erlang-commands.png ├── vscode-erlang-build-args.png ├── vscode-erlang-build-task.png ├── vscode-erlang-debug-args.png ├── vscode-erlang-inlayhints.png ├── vscode-erlang-inlinevalues.png └── vscode-erlang-rebarpath-settings.png ├── rebar3-erl22 └── rebar3 ├── rebar3-latest └── rebar3 ├── .gitmodules ├── apps └── erlangbridge │ ├── test │ ├── lsp_navigation_SUITE_data │ │ ├── mod_test.erl │ │ ├── data_goods.erl │ │ ├── main.erl │ │ ├── gen_msg_test2.erl │ │ └── gen_msg_test1.erl │ ├── testlog.hrl │ ├── lsp_utils_SUITE.erl │ └── lsp_navigation_SUITE.erl │ └── src │ ├── lsp_log.hrl │ ├── erlfmt_README │ ├── vscode_lsp.app.src │ ├── gen_lsp_help_sup.erl │ ├── gen_lsp_config_sup.erl │ ├── gen_lsp_doc_sup.erl │ ├── worker.erl │ ├── vscode_erlfmt_scan.hrl │ ├── gen_lsp_sup.erl │ ├── vscode_lsp_app.erl │ ├── vscode_lsp_entry.erl │ ├── vscode_lsp_app_sup.erl │ ├── lsp_fun_utils.erl │ ├── gen_connection.erl │ ├── hover_doc_layout.erl │ ├── lsp_signature_doc_layout.erl │ ├── gen_lsp_config_server.erl │ ├── lsp_completion.erl │ ├── lsp_inlayhints.erl │ ├── gen_lsp_server.erl │ └── lsp_parse.erl ├── lib ├── erlangDebug.ts ├── utils.ts ├── erlangSettings.ts ├── ErlangShell.ts ├── vscodeAdapter.ts ├── lsp │ ├── lsp-rename.ts │ ├── lsp-context.ts │ ├── ErlangShellLSP.ts │ ├── lsp-inlinevalues.ts │ ├── lspcodelens.ts │ └── lspclientextension.ts ├── ErlangAdapterDescriptorFactory.ts ├── ErlangConfigurationProvider.ts ├── RebarShell.ts ├── erlangDebugConnection.ts ├── erlangConnection.ts ├── GenericShell.ts ├── extension.ts └── RebarRunner.ts ├── syntaxes ├── test │ ├── function_type_spec.erl │ ├── function_call.erl │ ├── implicit_fun_expression.erl │ ├── syntax_test_macros.erl │ ├── record.erl │ └── syntax_test_data_types.erl └── README.md ├── .gitignore ├── .vscode-test.mjs ├── .vscodeignore ├── tsconfig.json ├── HELP.MD ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── rebar.config ├── DevelopersReadme.md ├── erlang.configuration.json ├── LICENSE ├── .github └── workflows │ └── pr-verify.yml ├── webpack.config.js ├── vsc-extension-quickstart.md ├── samples └── rebar.tasks.json ├── README.md └── package.json /test/resources/projects/rebar3project/readme.md: -------------------------------------------------------------------------------- 1 | rebar3 project -------------------------------------------------------------------------------- /test/resources/readme.md: -------------------------------------------------------------------------------- 1 | 2 | Contains different erlang projects 3 | -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/rebar3 -------------------------------------------------------------------------------- /rebar3.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem set DEBUG=1 3 | escript.exe rebar3 %* 4 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/icon.png -------------------------------------------------------------------------------- /images/erlIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/erlIcon.png -------------------------------------------------------------------------------- /rebar3-erl22/rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/rebar3-erl22/rebar3 -------------------------------------------------------------------------------- /test/test-fixtures/fixture1/fixture1.erl: -------------------------------------------------------------------------------- 1 | -module(fixture1). 2 | 3 | 4 | start() -> ok. 5 | 6 | -------------------------------------------------------------------------------- /rebar3-latest/rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/rebar3-latest/rebar3 -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "grammar"] 2 | path = grammar 3 | url = https://github.com/erlang-ls/grammar 4 | -------------------------------------------------------------------------------- /images/erlang-fileicon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/erlang-fileicon-dark.png -------------------------------------------------------------------------------- /images/erlang-fileicon-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/erlang-fileicon-light.png -------------------------------------------------------------------------------- /images/lsp_validate_document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/lsp_validate_document.png -------------------------------------------------------------------------------- /images/vscode-erlang-build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-erlang-build.png -------------------------------------------------------------------------------- /images/vscode-erlang-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-erlang-debug.png -------------------------------------------------------------------------------- /images/vscode-erlang-editing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-erlang-editing.gif -------------------------------------------------------------------------------- /images/vscode-erlang-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-erlang-hover.png -------------------------------------------------------------------------------- /images/vscode-lsp-migration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-lsp-migration.png -------------------------------------------------------------------------------- /images/vscode-erlang-codelens.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-erlang-codelens.png -------------------------------------------------------------------------------- /images/vscode-erlang-commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-erlang-commands.png -------------------------------------------------------------------------------- /test/resources/projects/singlefile/single.erl: -------------------------------------------------------------------------------- 1 | -module(single). 2 | 3 | start() -> 4 | ok. 5 | 6 | stop() -> 7 | ok. -------------------------------------------------------------------------------- /images/vscode-erlang-build-args.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-erlang-build-args.png -------------------------------------------------------------------------------- /images/vscode-erlang-build-task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-erlang-build-task.png -------------------------------------------------------------------------------- /images/vscode-erlang-debug-args.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-erlang-debug-args.png -------------------------------------------------------------------------------- /images/vscode-erlang-inlayhints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-erlang-inlayhints.png -------------------------------------------------------------------------------- /images/vscode-erlang-inlinevalues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-erlang-inlinevalues.png -------------------------------------------------------------------------------- /apps/erlangbridge/test/lsp_navigation_SUITE_data/mod_test.erl: -------------------------------------------------------------------------------- 1 | -module(mod_test). 2 | -compile(export_all). 3 | 4 | run(RoleId) -> 5 | ok. -------------------------------------------------------------------------------- /lib/erlangDebug.ts: -------------------------------------------------------------------------------- 1 | import { ErlangDebugSession } from './erlangDebugSession'; 2 | 3 | ErlangDebugSession.run(ErlangDebugSession); 4 | 5 | -------------------------------------------------------------------------------- /images/vscode-erlang-rebarpath-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pgourlain/vscode_erlang/HEAD/images/vscode-erlang-rebarpath-settings.png -------------------------------------------------------------------------------- /test/test-fixtures/fixture-navigation/navigation_target.erl: -------------------------------------------------------------------------------- 1 | -module(navigation_target). 2 | 3 | 4 | -export([myexportedfilter/1]). 5 | 6 | myexportedfilter(_X) -> 7 | ok. -------------------------------------------------------------------------------- /syntaxes/test/function_type_spec.erl: -------------------------------------------------------------------------------- 1 | -module(function_type_spec). 2 | 3 | -type fun_type() :: fun((term()) -> ok). 4 | 5 | -spec f() -> fun_type(). 6 | f() -> 7 | fun(_) -> ok end. 8 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/lsp_log.hrl: -------------------------------------------------------------------------------- 1 | -define(LOG(S), 2 | begin 3 | gen_lsp_server:lsp_log("~p", [S]) 4 | end). 5 | -define(LOG(Fmt, Args), 6 | begin 7 | gen_lsp_server:lsp_log(Fmt, Args) 8 | end). -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .DS_Store 4 | rebar.lock 5 | *.vsix 6 | *.beam 7 | *.dump 8 | _build/ 9 | /syntaxes/*.yaml 10 | /syntaxes/*.yaml-tmLanguage 11 | apps/erlangbridge/src/vscode_erlfmt_parse.erl 12 | dist 13 | .vscode-test 14 | 15 | -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'out/test/**/*.test.js', 5 | workspaceFolder: './test/test-fixtures', 6 | mocha: { 7 | ui: 'tdd', 8 | timeout: 60000 9 | } 10 | }); -------------------------------------------------------------------------------- /apps/erlangbridge/test/lsp_navigation_SUITE_data/data_goods.erl: -------------------------------------------------------------------------------- 1 | -module(data_goods). 2 | -export([get/1]). 3 | 4 | -define(DEFAULT, 100). 5 | 6 | get(1) -> 1001; 7 | 8 | get(2) -> 1002; 9 | 10 | get(3) -> 1003; 11 | 12 | get(4) -> 1004; 13 | 14 | get(_) -> ?DEFAULT. -------------------------------------------------------------------------------- /syntaxes/test/function_call.erl: -------------------------------------------------------------------------------- 1 | -module(function_call). 2 | 3 | -define(F, f). 4 | 5 | f() -> 6 | ok. 7 | 8 | g(M, F) -> 9 | m:f(), 10 | m:F(), 11 | m:?F(), 12 | M:f(), 13 | ?MODULE:f(), 14 | ok. 15 | 16 | -spec h() -> ok. 17 | h() -> 18 | g(m, f), 19 | ok. 20 | -------------------------------------------------------------------------------- /apps/erlangbridge/test/testlog.hrl: -------------------------------------------------------------------------------- 1 | -define(logMsg(S), 2 | begin 3 | ct:log(default, 50, "~w:~p", [self(), S], []) 4 | end). 5 | 6 | -define(writeConsole(Fmt, Args), 7 | error_logger:info_msg(Msg, Args)). 8 | 9 | -define(writeConsole(S), 10 | error_logger:info_msg("~p\n", [S])). -------------------------------------------------------------------------------- /test/test-fixtures/fixture-navigation/navigation_source.erl: -------------------------------------------------------------------------------- 1 | -module(navigation_source). 2 | 3 | -export([start/0]). 4 | 5 | start() -> 6 | L = [1,2,3,4,5,6,7,8], 7 | lists:filter(fun myfilter1/1, L), 8 | lists:filter(fun navigation_target:myexportedfilter/1, L). 9 | 10 | 11 | myfilter1(_X) -> 12 | true. -------------------------------------------------------------------------------- /apps/erlangbridge/src/erlfmt_README: -------------------------------------------------------------------------------- 1 | The files vscode_erlfmt... have been copied from https://github.com/WhatsApp/erlfmt 2 | in order to keep the extension self contained. The files have been update with new 3 | module names (vscode_erlfmt instead of elrfmt) and functions to provide escript and 4 | rebar plugin functionality have been removed. The license of the original files 5 | is in erlfmt_LICENSE. -------------------------------------------------------------------------------- /syntaxes/test/implicit_fun_expression.erl: -------------------------------------------------------------------------------- 1 | -module(implicit_fun_expression). 2 | 3 | -define(F, f). 4 | 5 | f() -> 6 | ok. 7 | 8 | g(M, F) -> 9 | fun f/0, 10 | fun F/0, 11 | fun ?F/0, 12 | fun m:f/0, 13 | fun m:F/0, 14 | fun m:?F/0, 15 | fun M:f/0, 16 | fun ?MODULE:f/0, 17 | ok. 18 | 19 | -spec h() -> ok. 20 | h() -> 21 | g(m, f), 22 | ok. 23 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | 4 | /// 5 | /// get keys from a dictionary 6 | /// 7 | export function keysFromDictionary(dico : any): string[] { 8 | var keySet: string[] = []; 9 | for (var prop in dico) { 10 | if (dico.hasOwnProperty(prop)) { 11 | keySet.push(prop); 12 | } 13 | } 14 | return keySet; 15 | } 16 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | typings/** 3 | out/test/** 4 | test/** 5 | src/** 6 | lib/** 7 | **/*.map 8 | .gitignore 9 | **/tsconfig.json 10 | vsc-extension-quickstart.md 11 | **/*.vsix 12 | **/sample_forms.txt 13 | syntaxes/test/** 14 | _build/** 15 | apps/erlangbridge/test/** 16 | .vscode 17 | node_modules 18 | src/ 19 | tsconfig.json 20 | webpack.config.js 21 | .vscode-test/** 22 | 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es7" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "." 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | ".vscode-test", 15 | "typings", 16 | "_build" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /HELP.MD: -------------------------------------------------------------------------------- 1 | 2 | # Introduction 3 | This file contains some help about Erlang extension. 4 | 5 | 6 | # Changing Debugger Mode 7 | - Debugger Mode setting is read at extension startup, so when you change this setting, you must restart vscode. 8 | 9 | # How exclude directories from "goto definition" feature 10 | 11 | Add in your local settings.json this section 12 | 13 | ``` 14 | { 15 | ... 16 | "search.exclude" : { 17 | "**/_build": true 18 | } 19 | ... 20 | } 21 | ``` -------------------------------------------------------------------------------- /lib/erlangSettings.ts: -------------------------------------------------------------------------------- 1 | export interface ErlangSettings { 2 | erlangPath : string; 3 | erlangArgs : string[]; 4 | erlangDistributedNode: boolean; 5 | rebarPath : string; 6 | rebarBuildArgs : string[]; 7 | includePaths : string[]; 8 | linting: boolean; 9 | codeLensEnabled : boolean; 10 | cacheManagement: string; 11 | inlayHintsEnabled: boolean; 12 | verbose: boolean; 13 | debuggerRunMode : string; 14 | /// workspace.rootPath, since VSCode 1.78 workspace.workspaceFolders[0].Uri.path 15 | rootPath: string; 16 | } 17 | -------------------------------------------------------------------------------- /syntaxes/test/syntax_test_macros.erl: -------------------------------------------------------------------------------- 1 | -module(syntax_test_macros). 2 | 3 | -compile(export_all). 4 | 5 | -define(ONE, 1). 6 | -define(INC(N), (N+1)). 7 | -define(ADD(N, M), (N+M)). 8 | -define(NAME_VALUE(Param), [??Param, Param]). 9 | 10 | -spec f() -> ok. 11 | f() -> 12 | ?ONE, 13 | ?INC(1), 14 | ?ADD(1, 2), 15 | ?ADD( % parameters 16 | 1, % 1 17 | 2 % 2 18 | ), % end 19 | X = 1, 20 | ?NAME_VALUE(X), 21 | ok. 22 | 23 | -spec g() -> ok. 24 | g() -> 25 | ok. 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "node_modules" : true, 6 | "_build_": true 7 | }, 8 | "search.exclude": { 9 | "out": true, // set this to false to include "out" folder in search results 10 | "_build": true 11 | }, 12 | "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version 13 | } -------------------------------------------------------------------------------- /apps/erlangbridge/src/vscode_lsp.app.src: -------------------------------------------------------------------------------- 1 | {application, vscode_lsp, [ 2 | {description, "Erlang Language Server Protocol for VSCode"}, 3 | {vsn, "0.1.0"}, 4 | {registered, [vscode_lsp_app]}, 5 | {modules, [ 6 | vscode_lsp_app, gen_lsp_sup, gen_lsp_server, 7 | gen_lsp_doc_sup, gen_lsp_doc_server, 8 | gen_lsp_config_sup, gen_lsp_config_server, 9 | gen_lsp_help_sup, gen_lsp_help_server, 10 | lsp_navigation, lsp_syntax, lsp_utils 11 | ]}, 12 | {applications, [kernel, stdlib]}, 13 | {mod, {vscode_lsp_app, []}}, 14 | {env, []} 15 | ]}. 16 | -------------------------------------------------------------------------------- /apps/erlangbridge/test/lsp_navigation_SUITE_data/main.erl: -------------------------------------------------------------------------------- 1 | -module(main). 2 | -compile(export_all). 3 | 4 | start(RoleId) -> 5 | %% You could write it like this 6 | lib_test:apply_cast(mod_test, run, [RoleId]), 7 | %% or like this 8 | {mod_test, run, [RoleId]}, 9 | %% And even that 10 | {mod_test, run}, 11 | ok. 12 | 13 | %% go to definition valid for both `mod_test` and `run` 14 | 15 | start_local() -> 16 | functionA([1]). 17 | 18 | functionA(_)-> 19 | ok. 20 | 21 | -define(GOODS_ID, 3). 22 | 23 | start() -> 24 | data_goods:get(?GOODS_ID), 25 | ok. 26 | -------------------------------------------------------------------------------- /syntaxes/test/record.erl: -------------------------------------------------------------------------------- 1 | -module(record). 2 | 3 | -record(rec, 4 | {a = 1 :: integer(), % a 5 | b = 1, % b 6 | c :: integer(), % c 7 | d % d 8 | %% after last field 9 | } % after fields 10 | ). % end of record definition 11 | 12 | -spec f() -> ok. 13 | f() -> 14 | #rec.a, % a 15 | A = 1, 16 | #rec{}, 17 | B = 2, 18 | #rec{b = "2", % b 19 | c = 1 % c 20 | }, % b 21 | C = 2, 22 | #rec{a = 1, _ = '_'}, 23 | D = #dummy.a, 24 | E = 4, 25 | ok. 26 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/gen_lsp_help_sup.erl: -------------------------------------------------------------------------------- 1 | -module(gen_lsp_help_sup). 2 | -behaviour(supervisor). 3 | 4 | %% API 5 | -export([start_link/0]). 6 | 7 | %% Supervisor callbacks 8 | -export([init/1]). 9 | 10 | start_link() -> 11 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 12 | 13 | init(_Args) -> 14 | UserSpec = #{id => gen_lsp_help_server, 15 | start => {gen_lsp_help_server, start_link, []}, 16 | restart => permanent, 17 | modules => [gen_lsp_help_server], 18 | type => worker}, 19 | StartSpecs = {{one_for_one, 60, 3600}, [UserSpec]}, 20 | {ok, StartSpecs}. 21 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/gen_lsp_config_sup.erl: -------------------------------------------------------------------------------- 1 | -module(gen_lsp_config_sup). 2 | -behaviour(supervisor). 3 | 4 | %% API 5 | -export([start_link/0]). 6 | 7 | %% Supervisor callbacks 8 | -export([init/1]). 9 | 10 | start_link() -> 11 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 12 | 13 | init(_Args) -> 14 | UserSpec = #{id => gen_lsp_config_server, 15 | start => {gen_lsp_config_server, start_link, []}, 16 | restart => permanent, 17 | modules => [gen_lsp_config_server], 18 | type => worker}, 19 | StartSpecs = {{one_for_one, 60, 3600}, [UserSpec]}, 20 | {ok, StartSpecs}. 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "2.0.0", 12 | "tasks": [ 13 | { 14 | "label": "prepareTest", 15 | "dependsOn": [ 16 | "npm: pretest" 17 | ] 18 | }, 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/gen_lsp_doc_sup.erl: -------------------------------------------------------------------------------- 1 | -module(gen_lsp_doc_sup). 2 | -behaviour(supervisor). 3 | 4 | %% API 5 | -export([start_link/0]). 6 | 7 | %% Supervisor callbacks 8 | -export([init/1]). 9 | 10 | start_link() -> 11 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 12 | 13 | init(_Args) -> 14 | % error_logger:info_msg("~p:init()", [?MODULE]), 15 | % because lsp_log use another gen_server we can't use it here 16 | UserSpec = #{id => gen_lsp_doc_server, 17 | start => {gen_lsp_doc_server, start_link, []}, 18 | restart => permanent, 19 | modules => [gen_lsp_doc_server], 20 | type => worker}, 21 | StartSpecs = {{one_for_one, 60, 3600}, [UserSpec]}, 22 | {ok, StartSpecs}. 23 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {require_otp_vsn, "1[8-9].*|[2-9][0-9].*"}. 2 | {lib_dirs, []}. 3 | 4 | {deps, [ 5 | ] 6 | }. 7 | 8 | {erl_opts, [debug_info]}. 9 | 10 | %% where rebar3 operates from; defaults to the current working directory 11 | {root_dir, "."}. 12 | 13 | {edoc_opts,[{todo,true}]}. 14 | 15 | 16 | %% == EUnit == 17 | 18 | %% eunit:test(Tests) 19 | {eunit_tests, [{application, vscode_lsp}]}. 20 | %% Options for eunit:test(Tests, Opts) 21 | {eunit_opts, [verbose]}. 22 | %{eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}. 23 | 24 | 25 | %% Keep only the logs of the last 5 runs 26 | {ct_opts, [{keep_logs, 2}]}. 27 | 28 | 29 | %% runs 'clean' before 'compile' 30 | {provider_hooks, [{pre, [{compile, clean}]}]}. 31 | 32 | %{project_plugins, [erlfmt]}. 33 | -------------------------------------------------------------------------------- /lib/ErlangShell.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as child_process from 'child_process'; 3 | import { GenericShell } from './GenericShell'; 4 | import * as adapt from './vscodeAdapter'; 5 | 6 | 7 | export class ErlangShell extends GenericShell { 8 | constructor(){ 9 | super(adapt.ErlangOutputAdapter());//ErlangShell.ErlangOutput); 10 | } 11 | public Start(startDir : string, args: string[]) : Thenable { 12 | return this.RunProcess("erl", startDir, args); 13 | } 14 | } 15 | 16 | export class ErlangCompilerShell extends GenericShell { 17 | constructor(){ 18 | super(adapt.ErlangOutputAdapter());//ErlangShell.ErlangOutput); 19 | } 20 | 21 | public Start(startDir : string, args: string[]) : Thenable { 22 | return this.RunProcess("erlc", startDir, args); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/worker.erl: -------------------------------------------------------------------------------- 1 | -module(worker). 2 | -export([start/2]). 3 | 4 | -define(KILL_AFTER, 10 * 60000). 5 | 6 | start(Fun, ID) -> 7 | Owner = self(), 8 | spawn_link(fun () -> 9 | {Worker, Monitor} = spawn_monitor(fun () -> 10 | try 11 | erlang:send(Owner, {worker_result, ID, Fun()}) 12 | catch _Class:Exception:Stack -> 13 | logger:error("~p", [Stack]), 14 | erlang:send(Owner, {worker_error, ID, Exception}) 15 | end 16 | end), 17 | receive 18 | {'DOWN', Monitor, process, Worker, _Reason} -> ok 19 | after 20 | ?KILL_AFTER -> 21 | logger:error("Worker ~p killed due to timeout", [Worker]), 22 | exit(Worker, kill), 23 | erlang:send(Owner, {worker_error, ID, timeout}) 24 | end 25 | end). -------------------------------------------------------------------------------- /apps/erlangbridge/src/vscode_erlfmt_scan.hrl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) Facebook, Inc. and its affiliates. 2 | %% 3 | %% Licensed under the Apache License, Version 2.0 (the "License"); 4 | %% you may not use this file except in compliance with the License. 5 | %% You may obtain a copy of the License at 6 | %% 7 | %% http://www.apache.org/licenses/LICENSE-2.0 8 | %% 9 | %% Unless required by applicable law or agreed to in writing, software 10 | %% distributed under the License is distributed on an "AS IS" BASIS, 11 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | %% See the License for the specific language governing permissions and 13 | %% limitations under the License. 14 | 15 | -define(IS_ATOMIC(Kind), 16 | Kind =:= integer orelse 17 | Kind =:= float orelse 18 | Kind =:= char orelse 19 | Kind =:= atom orelse 20 | Kind =:= string orelse 21 | Kind =:= var 22 | ). 23 | -------------------------------------------------------------------------------- /DevelopersReadme.md: -------------------------------------------------------------------------------- 1 | # Developer's Readme 2 | 3 | ## Build 4 | 5 | At the very first time install some packages in project directory: 6 | (make sure [Node.js](https://nodejs.org) is installed on your machine) 7 | 8 | cd /path/to/vscode_erlang 9 | npm install 10 | npm install -g vsce 11 | 12 | Build the extension and create a VSIX package for manual distributing: 13 | 14 | ./rebar3 compile 15 | vsce package 16 | 17 | ## Test 18 | 19 | In _"Run"_ sidbar choose _"Launch Extension"_. 20 | 21 | 22 | ## Run unit tests 23 | 24 | In _"Terminal"_ menu choose _"New Terminal"_. 25 | then 26 | 27 | ```bash 28 | ./rebar3 ct 29 | ``` 30 | 31 | ## build package 32 | 33 | - vsce package : 'vscode:prepublish' is executed 34 | 35 | 36 | ## Language syntax file 37 | 38 | See [syntaxes/README.md](syntaxes/README.md). 39 | 40 | ## References 41 | 42 | 1. Visual Studio Code [Extension API](https://code.visualstudio.com/api) 43 | -------------------------------------------------------------------------------- /lib/vscodeAdapter.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ILogOutput } from './GenericShell'; 3 | 4 | var erlangOutputChannel : vscode.OutputChannel; 5 | 6 | 7 | export function ErlangOutput() : vscode.OutputChannel { 8 | if (!erlangOutputChannel) { 9 | erlangOutputChannel = vscode.window.createOutputChannel('erlang', 'erlang'); 10 | } 11 | return erlangOutputChannel; 12 | } 13 | 14 | export function ErlangOutputAdapter(outputChannel?: vscode.OutputChannel) : ILogOutput { 15 | return new ErlangWrapperOutput(outputChannel || ErlangOutput()); 16 | } 17 | 18 | class ErlangWrapperOutput implements ILogOutput { 19 | constructor (private channel : vscode.OutputChannel) { 20 | } 21 | public appendLine(value: string): void { 22 | this.channel.appendLine(value); 23 | } 24 | 25 | public debug(msg : string) : void { 26 | this.channel.appendLine("debug:" + msg); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /erlang.configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "%" 4 | }, 5 | "brackets": [ 6 | ["{", "}"], 7 | ["[", "]"], 8 | ["(", ")"], 9 | ["<<", ">>"] 10 | ], 11 | "autoClosingPairs": [ 12 | { "open": "{", "close": "}", "notIn": ["string", "comment"] }, 13 | { "open": "[", "close": "]", "notIn": ["string", "comment"] }, 14 | { "open": "(", "close": ")", "notIn": ["string", "comment"] }, 15 | { "open": "<<", "close": ">>", "notIn": ["string", "comment"] }, 16 | { "open": "\"\"\"", "close": "\"\"\"", "notIn": ["string", "comment"] }, 17 | { "open": "'", "close": "'", "notIn": ["string", "comment"] }, 18 | { "open": "\"", "close": "\"" } 19 | ], 20 | "folding": { 21 | "markers": { 22 | "start": "^\\s*\\%\\%*\\s*#?region\\b", 23 | "end": "^\\s*\\%\\%*\\s*#?endregion\\b" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /test/test-suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | useColors: true, 10 | timeout: 1 * 60 * 1000, /* ms*/ 11 | }); 12 | 13 | const testsRoot = path.resolve(__dirname, '..'); 14 | 15 | return new Promise((c, e) => { 16 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 17 | if (err) { 18 | return e(err); 19 | } 20 | 21 | // Add files to the test suite 22 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 23 | 24 | try { 25 | // Run the mocha test 26 | mocha.run(failures => { 27 | if (failures > 0) { 28 | e(new Error(`${failures} tests failed.`)); 29 | } else { 30 | c(); 31 | } 32 | }); 33 | } catch (err) { 34 | console.error(err); 35 | e(err); 36 | } 37 | }); 38 | }); 39 | } -------------------------------------------------------------------------------- /apps/erlangbridge/test/lsp_navigation_SUITE_data/gen_msg_test2.erl: -------------------------------------------------------------------------------- 1 | -module(gen_msg_test2). 2 | 3 | -export([send_info/1, is_open/0]). 4 | 5 | -behaviour(gen_statem). 6 | -export([start/1, stop/1]). 7 | -export([init/1, callback_mode/0, handle_event/4, code_change/4, terminate/3]). 8 | 9 | is_open() -> 10 | gen_statem:call(?MODULE, is_open). 11 | 12 | send_info(RoleId) -> 13 | gen_statem:cast(?MODULE, {send_info, RoleId}). 14 | 15 | callback_mode() -> handle_event_function. 16 | 17 | start(Args) -> 18 | gen_statem:start_link(?MODULE, Args, []). 19 | stop(Pid) -> 20 | gen_statem:stop(Pid). 21 | 22 | init(_Args) -> 23 | {ok, wait, []}. 24 | 25 | handle_event(cast, {send_info, _RoleId}, _State, _Date) -> 26 | keep_state_and_data; 27 | 28 | handle_event({call, _From}, is_open, _State, _Date) -> 29 | keep_state_and_data; 30 | 31 | handle_event(_EventType, _EventContent, _State, _Date) -> 32 | keep_state_and_data. 33 | 34 | code_change(_Vsn, StateName, Data, _Extra) -> 35 | {ok, StateName, Data}. 36 | 37 | terminate(_Reason, _State, _Data) -> 38 | ok. 39 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/gen_lsp_sup.erl: -------------------------------------------------------------------------------- 1 | -module(gen_lsp_sup). 2 | -behaviour(supervisor). 3 | 4 | %% API 5 | -export([start_link/1]). 6 | 7 | %% Supervisor callbacks 8 | -export([init/1]). 9 | 10 | -define(TCP_OPTIONS, [binary, {packet, raw}, {active, once}, {reuseaddr, true}]). 11 | 12 | start_link(Port) -> 13 | case supervisor:start_link({local, ?MODULE}, ?MODULE, Port) of 14 | {ok, Pid} -> 15 | supervisor:start_child(Pid, []), 16 | {ok, Pid}; 17 | _ -> 18 | error_logger:error_msg("~p:start_link failed",[?MODULE]), 19 | {error, not_started} 20 | end. 21 | 22 | init(VsCodePort) -> 23 | {ok, LSock} = gen_tcp:listen(list_to_integer(VsCodePort), ?TCP_OPTIONS), 24 | UserSpec = #{id => gen_lsp_server, 25 | start => {gen_lsp_server, start_link, [list_to_integer(VsCodePort), LSock]}, 26 | restart => temporary, 27 | modules => [gen_lsp_server], 28 | type => worker}, 29 | StartSpecs = {{simple_one_for_one, 60, 3600}, [UserSpec]}, 30 | gen_tcp:send(LSock, <<"{}">>), 31 | {ok, StartSpecs}. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Pierrick Gourlain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /apps/erlangbridge/test/lsp_navigation_SUITE_data/gen_msg_test1.erl: -------------------------------------------------------------------------------- 1 | -module(gen_msg_test1). 2 | 3 | -export([get_val/1, logout/1]). 4 | 5 | -behaviour(gen_server). 6 | -export([stop/1, start_link/1]). 7 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 8 | 9 | get_val(DropId) -> 10 | gen_server:call(?MODULE, {get_val, DropId}). 11 | 12 | logout(Args) -> 13 | gen_server:cast(?MODULE, {logout, Args}). 14 | 15 | stop(_Name) -> 16 | gen_server:call(?MODULE, stop). 17 | 18 | start_link(_Name) -> 19 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 20 | 21 | init(_Args) -> 22 | {ok, []}. 23 | 24 | 25 | handle_call(stop, _From, State) -> 26 | {stop, normal, stopped, State}; 27 | 28 | handle_call({get_val, _DropId}, _From, State) -> 29 | {reply, ok, State}; 30 | 31 | handle_call(_Request, _From, State) -> 32 | {reply, ok, State}. 33 | 34 | handle_cast({logout, _Args}, State) -> 35 | {noreply, State}; 36 | 37 | handle_cast(_Msg, State) -> 38 | {noreply, State}. 39 | 40 | handle_info(_Info, State) -> 41 | {noreply, State}. 42 | 43 | terminate(_Reason, _State) -> 44 | ok. 45 | 46 | code_change(_OldVsn, State, _Extra) -> 47 | {ok, State}. 48 | -------------------------------------------------------------------------------- /.github/workflows/pr-verify.yml: -------------------------------------------------------------------------------- 1 | name: pr-verify 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | pr-verify-job: 13 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | timeout-minutes: 10 16 | strategy: 17 | fail-fast: true 18 | matrix: 19 | os: [ubuntu-latest] 20 | node: 21 | - 20 22 | steps: 23 | - run: | 24 | sudo apt-get update 25 | sudo apt-get install erlang 26 | if: runner.os == 'Linux' 27 | - run: | 28 | brew install erlang@26 29 | if: runner.os =='macOS' 30 | - run: | 31 | VERSION=$(ls /opt/homebrew/Cellar/erlang/) 32 | export PATH=$PATH:/opt/homebrew/Cellar/erlang/$VERSION/bin 33 | erl -version 34 | name: setup path 35 | if: runner.os =='macOS' 36 | - name: Checkout Source 37 | uses: actions/checkout@v4 38 | - name: Install Node ${{ matrix.node }} 39 | uses: actions/setup-node@v4 40 | with: 41 | node-version: ${{ matrix.node }} 42 | - run: npm install -g typescript "vsce" 43 | - run: npm install 44 | - run: xvfb-run -a npm test 45 | if: runner.os == 'Linux' 46 | - run: npm test 47 | if: runner.os != 'Linux' 48 | - run: | 49 | ./rebar3 ct 50 | 51 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Attach to lspserver", 7 | "type": "node", 8 | "request": "attach", 9 | "port": 6011 10 | }, 11 | { 12 | "name": "debugger Server", 13 | "type": "node", 14 | "request": "launch", 15 | "cwd": "${workspaceFolder}", 16 | "program": "${workspaceFolder}/lib/erlangDebug.ts", 17 | "args": [ 18 | "--server=4711" 19 | ], 20 | "outFiles": [ 21 | "${workspaceFolder}/out/**/*.js" 22 | ] 23 | }, 24 | { 25 | "name": "Launch Extension", 26 | "type": "extensionHost", 27 | "request": "launch", 28 | "runtimeExecutable": "${execPath}", 29 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 30 | "sourceMaps": true, 31 | "outFiles": ["${workspaceRoot}/out"], 32 | }, 33 | { 34 | "name": "Launch Tests", 35 | "type": "extensionHost", 36 | "request": "launch", 37 | "runtimeExecutable": "${execPath}", 38 | "args": [ 39 | "${workspaceFolder}/test/test-fixtures/", 40 | "--disable-extensions", 41 | "--extensionDevelopmentPath=${workspaceFolder}", 42 | "--extensionTestsPath=${workspaceRoot}/out/test/test-suite" 43 | ], 44 | "sourceMaps": true, 45 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 46 | "preLaunchTask": "prepareTest", 47 | // "postDebugTask": "cleanTestFolder" 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | 11 | entry: { 12 | extension: './lib/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 13 | erlangDebug: './lib/erlangDebug.ts' 14 | }, 15 | output: { 16 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 17 | path: path.resolve(__dirname, 'out/lib'), 18 | filename: '[name].js', 19 | libraryTarget: 'commonjs2', 20 | devtoolModuleFilenameTemplate: '../[resource-path]' 21 | }, 22 | devtool: 'source-map', 23 | externals: { 24 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 25 | }, 26 | node: { 27 | __filename: true, 28 | __dirname: true 29 | }, 30 | resolve: { 31 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 32 | extensions: ['.ts', '.js'] 33 | }, 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.ts$/, 38 | exclude: /node_modules/, 39 | use: [ 40 | { 41 | loader: 'ts-loader' 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | }; 48 | module.exports = config; -------------------------------------------------------------------------------- /lib/lsp/lsp-rename.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | CancellationToken, Event, ExtensionContext, RenameProvider, 4 | OutputChannel, ProviderResult, TextDocument, languages, 5 | workspace as Workspace, Range, Position, 6 | WorkspaceEdit 7 | } from 'vscode'; 8 | import { clientIsReady, erlangDocumentSelector } from './lsp-context'; 9 | import { RequestType, TextDocumentIdentifier, PrepareRenameSignature } from 'vscode-languageclient'; 10 | import { client, debugLog } from './lspclientextension'; 11 | import * as proto from 'vscode-languageserver-protocol'; 12 | 13 | export function activate(context: ExtensionContext, lspOutputChannel: OutputChannel) { 14 | //inlay should be registered only if config enabled 15 | // const enabledSetting = 'editor.inlayHints.enabled'; 16 | // const inlay = languages.registerRenameProvider(erlangDocumentSelector, 17 | // new ErlangRenameProvider(lspOutputChannel)); 18 | // context.subscriptions.push(inlay); 19 | } 20 | 21 | export class ErlangRenameProvider implements RenameProvider { 22 | 23 | constructor(private lspOutputChannel: OutputChannel) { 24 | } 25 | 26 | provideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken): ProviderResult { 27 | throw new Error('Method provideRenameEdits not implemented.'); 28 | } 29 | prepareRename?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult { 30 | debugLog(`onPrepareRename :`); 31 | throw new Error('Method prepareRename not implemented.'); 32 | //return null; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /apps/erlangbridge/src/vscode_lsp_app.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @doc Erlang Language Server Process (LSP) for Visual Studio Code. 3 | %%% 4 | %%% Application supervision tree: 5 | %%% 6 | %%% ``` 7 | %%% vscode_lsp_entry 8 | %%% | 9 | %%% | vscode_lsp [app.src] 10 | %%% | 11 | %%% vscode_lsp_app [app] 12 | %%% | 13 | %%% vscode_lsp_app_sup [sup] 14 | %%% | 15 | %%% +--gen_lsp_sup [sup] 16 | %%% | | 17 | %%% | +--gen_lsp_server [gen_server] 18 | %%% | 19 | %%% +--gen_lsp_doc_sup [sup] 20 | %%% | | - (D)ETS document_contents 21 | %%% | | - (D)ETS document_inlayhints 22 | %%% | | - (D)ETS dodged_syntax_tree 23 | %%% | | - ETS references 24 | %%% | | - (D)ETS syntax_tree 25 | %%% | | 26 | %%% | +--gen_lsp_doc_server [gen_server] 27 | %%% | 28 | %%% +--gen_lsp_config_sup [sup] 29 | %%% | | 30 | %%% | +--gen_lsp_config_server [gen_server] 31 | %%% | 32 | %%% +--gen_lsp_help_sup [sup] 33 | %%% | 34 | %%% +--gen_lsp_help_server [gen_server] 35 | %%% ''' 36 | %%% 37 | %%% @end 38 | %%%------------------------------------------------------------------- 39 | -module(vscode_lsp_app). 40 | -behavior(application). 41 | 42 | %% Application callbacks 43 | -export([start/2, stop/1]). 44 | 45 | get_port() -> 46 | case init:get_argument(vscode_port) of 47 | {ok, [[P]]} -> P; 48 | _ -> "0" 49 | end. 50 | 51 | start(_Type, _Args) -> 52 | application:start(inets), 53 | %uncomment to monitor erlang processes 54 | %spawn(fun() -> observer:start() end), 55 | gen_lsp_doc_server:persist_cache_mgmt_opts(), 56 | Port = get_port(), 57 | case vscode_lsp_app_sup:start_link(Port) of 58 | {ok, Pid} -> {ok, Pid}; 59 | _Any -> _Any 60 | end. 61 | 62 | stop(_State) -> 63 | ok. 64 | -------------------------------------------------------------------------------- /syntaxes/README.md: -------------------------------------------------------------------------------- 1 | # Work with language syntax files 2 | 3 | According Visual Studio Code Syntax Highlight Guide: 4 | > As a grammar grows more complex, it can become difficult to understand and 5 | > maintain it as json. If you find yourself writing complex regular expressions 6 | > or needing to add comments to explain aspects of the grammar, consider using 7 | > yaml to define your grammar instead. 8 | > 9 | > Yaml grammars have the exact same structure as a json based grammar but allow 10 | > you to use yaml's more concise syntax, along with features such as multi-line 11 | > strings and comments. 12 | > 13 | > VS Code can only load json _(or plist)_ grammars, so yaml based grammars must 14 | > be converted to json _(or plist)_. 15 | 16 | To work easier with TextMate YAML language files install VSCode extension 17 | [TextMate Languages](https://marketplace.visualstudio.com/items?itemName=Togusa09.tmlanguage) 18 | from _Ben Hockley_. 19 | 20 | * Use commands _"Convert to YAML-tmLanguage file"_ and 21 | _"Convert to tmLanguage file"_ provided by extension _TextMate Languages_. 22 | 23 | * Alternatively download 24 | [plist-yaml-plist](https://github.com/grahampugh/plist-yaml-plist) 25 | from Github and use below commands: (On Windows use WSL) 26 | 27 | /path/to/plist_yaml.py erlang.tmLanguage erlang.yaml-tmLanguage 28 | # Edit YAML file here ... then if you are ready convert back to PLIST 29 | /path/to/yaml_plist.py erlang.yaml-tmLanguage erlang.tmLanguage 30 | 31 | ## References 32 | 33 | 1. Visual Studio Code [Syntax Highlight Guide](https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide) 34 | 2. TextMate [Language Grammars](https://macromates.com/manual/en/language_grammars) 35 | 3. [Writing a TextMate Grammar: Some Lessons Learned](https://www.apeth.com/nonblog/stories/textmatebundle.html) 36 | -------------------------------------------------------------------------------- /test/test-suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { after } from 'mocha'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import { env } from 'process'; 6 | import * as vscode from 'vscode'; 7 | import * as vscodeclient from 'vscode-languageclient' 8 | import * as erlExtension from '../../lib/extension'; 9 | 10 | 11 | 12 | function openTextDocument(fileRelativePath: string ) : Thenable 13 | { 14 | const wk = vscode.workspace.workspaceFolders[0]; 15 | if (!fileRelativePath.startsWith("/")) { 16 | fileRelativePath = "/" + fileRelativePath; 17 | } 18 | const filepath = path.join(wk.uri.fsPath, fileRelativePath); 19 | return vscode.workspace.openTextDocument(filepath); 20 | } 21 | 22 | suite('Erlang Language Extension', () => { 23 | after(() => { 24 | vscode.window.showInformationMessage('All tests done!'); 25 | }); 26 | test('Extension should be present', async () => { 27 | const myExtension = vscode.extensions.getExtension('pgourlain.erlang'); 28 | await myExtension.activate(); 29 | assert.ok(myExtension); 30 | }); 31 | 32 | test('Diagnostics should be generated', async () => { 33 | //use console.info('...') to write on output during test 34 | 35 | const document = await openTextDocument("/fixture1/fixture1.erl"); 36 | assert.ok(document != null); 37 | assert.equal('erlang', document.languageId); 38 | 39 | const waitForDiags = new Promise((resolve, reject) => { 40 | const disposeToken = vscode.languages.onDidChangeDiagnostics( 41 | async (ev) => { 42 | disposeToken.dispose(); 43 | resolve(ev.uris); 44 | } 45 | ) 46 | }); 47 | const uris = await waitForDiags; 48 | assert.equal(true, uris.length > 0); 49 | const diags = vscode.languages.getDiagnostics(uris[0]); 50 | assert.equal(1, diags.length); 51 | }); 52 | }); -------------------------------------------------------------------------------- /apps/erlangbridge/src/vscode_lsp_entry.erl: -------------------------------------------------------------------------------- 1 | -module(vscode_lsp_entry). 2 | 3 | 4 | -export([start/0, start/1]). 5 | 6 | 7 | start() -> 8 | init_lsp(). 9 | 10 | start(_Args) -> 11 | init_lsp(). 12 | 13 | init_lsp() -> 14 | case compile_needed_modules() of 15 | ok -> 16 | case application:start(vscode_lsp, permanent) of 17 | {ok, _Started} -> ok; 18 | {error, Reason} -> 19 | error_logger:error_msg("application start error : ~p",[Reason]), 20 | {error, Reason}; 21 | _ -> ok 22 | end; 23 | {error, Reason} -> 24 | error_logger:error_msg("compile_needed_modules failed : ~p",[Reason]), 25 | {error, Reason} 26 | end. 27 | 28 | compile_needed_modules() -> 29 | CompileOptions = [verbose, binary, report], 30 | do_compile(["src/vscode_lsp_app", "src/gen_lsp_server", 31 | "src/gen_lsp_sup", "src/gen_lsp_doc_sup","src/gen_lsp_doc_server", "src/gen_lsp_config_sup","src/gen_lsp_config_server", 32 | "src/gen_lsp_help_sup","src/gen_lsp_help_server", "src/lsp_handlers", "src/lsp_utils", 33 | "src/vscode_lsp_app_sup", "src/lsp_navigation", "src/lsp_signature", "src/lsp_parse", "src/lsp_syntax", "src/lsp_completion", "src/lsp_inlayhints", 34 | "src/gen_connection", "src/vscode_jsone","src/vscode_jsone_decode","src/hover_doc_layout", "src/worker", "src/lsp_rename"], CompileOptions) 35 | . 36 | 37 | do_compile([H|T], CompileOptions) -> 38 | %compile in memory 39 | case compile:file(H, CompileOptions) of 40 | {ok, ModuleName, Binary} -> 41 | case code:load_binary(ModuleName, atom_to_list(ModuleName), Binary) of 42 | {module, _} -> 43 | do_compile(T, CompileOptions); 44 | {error, Reason} -> 45 | {error, Reason} 46 | end; 47 | _Any -> 48 | error_logger:error_msg("compile result of ~p: ~p",[H, _Any]), 49 | {error, _Any} 50 | 51 | end; 52 | 53 | do_compile([], _CompileOptions) -> 54 | ok. 55 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/vscode_lsp_app_sup.erl: -------------------------------------------------------------------------------- 1 | -module(vscode_lsp_app_sup). 2 | -behaviour(supervisor). 3 | 4 | %% API 5 | -export([start_link/1, start_sup_socket/1, start_sup_doc/0, start_sup_config/0, start_sup_help/0, start_child/1]). 6 | 7 | %% Supervisor callbacks 8 | -export([init/1]). 9 | 10 | start_link(VsCodePort) -> 11 | supervisor:start_link({local, ?MODULE}, ?MODULE, VsCodePort). 12 | 13 | init(VsCodePort) -> 14 | SocketSpec = socket_spec(VsCodePort), 15 | DocSpec = doc_spec(), 16 | ConfigSpec = config_spec(), 17 | HelpSpec = help_spec(), 18 | StartSpecs = {{one_for_one, 60, 3600}, [ConfigSpec, DocSpec, SocketSpec, HelpSpec]}, 19 | {ok, StartSpecs}. 20 | 21 | socket_spec(VsCodePort) -> 22 | #{id => gen_lsp_sup, 23 | start => {gen_lsp_sup, start_link, [VsCodePort]}, 24 | restart => permanent, 25 | modules => [gen_lsp_sup], 26 | type => supervisor}. 27 | 28 | doc_spec() -> 29 | #{id => gen_lsp_doc_sup, 30 | start => {gen_lsp_doc_sup, start_link, []}, 31 | restart => permanent, 32 | modules => [gen_lsp_doc_sup], 33 | type => supervisor}. 34 | 35 | config_spec() -> 36 | #{id => gen_lsp_config_sup, 37 | start => {gen_lsp_config_sup, start_link, []}, 38 | restart => permanent, 39 | modules => [gen_lsp_config_sup], 40 | type => supervisor}. 41 | 42 | help_spec() -> 43 | #{id => gen_lsp_help_sup, 44 | start => {gen_lsp_help_sup, start_link, []}, 45 | restart => permanent, 46 | modules => [gen_lsp_help_sup], 47 | type => supervisor}. 48 | 49 | start_sup_socket(VsCodePort) -> 50 | supervisor:start_child({local, ?MODULE}, socket_spec(VsCodePort)). 51 | 52 | start_sup_doc() -> 53 | supervisor:start_child(?MODULE, doc_spec()). 54 | 55 | start_sup_config() -> 56 | supervisor:start_child(?MODULE, config_spec()). 57 | 58 | start_sup_help() -> 59 | supervisor:start_child(?MODULE, help_spec()). 60 | 61 | start_child(Arg) -> 62 | error_logger:error_msg([{vscode_lsp_app_sup, start_child}, {arg, Arg}]), 63 | error. 64 | -------------------------------------------------------------------------------- /lib/lsp/lsp-context.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as vscode from 'vscode'; 3 | import { LanguageClient, LanguageClientOptions, MessageSignature, ResponseError, ServerOptions } from 'vscode-languageclient/node'; 4 | //import { lspOutputChannel } from './lspclientextension'; 5 | 6 | 7 | export const erlangDocumentSelector = [ 8 | { scheme: 'file', language: 'erlang' } 9 | ]; 10 | 11 | export function isErlangDocument(document: vscode.TextDocument) { 12 | return vscode.languages.match(erlangDocumentSelector, document); 13 | } 14 | 15 | export let clientIsReady: boolean = false; 16 | 17 | export class ErlangLanguageClient extends LanguageClient { 18 | // Override the default implementation for failed requests. The default 19 | // behavior is just to log failures in the output panel, however output panel 20 | // is designed for extension debugging purpose, normal users will not open it, 21 | // thus when the failure occurs, normal users doesn't know that. 22 | // 23 | // For user-interactive operations (e.g. applyFixIt, applyTweaks), we will 24 | // prompt up the failure to users. 25 | outChannel?: vscode.OutputChannel; 26 | 27 | handleFailedRequest(type: MessageSignature, token: vscode.CancellationToken | undefined, error: any, defaultValue: T, showNotification?: boolean): T { 28 | 29 | if (error instanceof ResponseError && type.method === 'workspace/executeCommand') { 30 | //show an error popup at lower right in vscode 31 | vscode.window.showErrorMessage(error.message); 32 | } 33 | return super.handleFailedRequest(type, token, error, defaultValue); 34 | } 35 | 36 | constructor(name: string, serverOptions: ServerOptions, 37 | clientOptions: LanguageClientOptions, 38 | lspOutputChannel: vscode.OutputChannel, 39 | forceDebug?: boolean) { 40 | super(name, serverOptions, clientOptions, forceDebug); 41 | this.outChannel = lspOutputChannel; 42 | } 43 | 44 | public onReady(): void { 45 | this.outChannel?.appendLine("LanguageClient is ready"); 46 | clientIsReady = true; 47 | //not more needed 48 | //return super.onReady(); 49 | } 50 | } -------------------------------------------------------------------------------- /lib/lsp/ErlangShellLSP.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | import { GenericShell, ILogOutput } from '../GenericShell'; 3 | import { getElangConfigConfiguration } from '../ErlangConfigurationProvider'; 4 | 5 | export class ErlangShellLSP extends GenericShell { 6 | constructor(whichOutput: ILogOutput) { 7 | super(whichOutput, null, getElangConfigConfiguration()); 8 | } 9 | public Start(erlPath:string, startDir: string, listen_port: number, bridgePath: string, args: string): Promise { 10 | var debugStartArgs = []; 11 | // Start as distributed node to be able to connect to the Erlang VM for investigation 12 | if (this.erlangDistributedNode) { 13 | debugStartArgs.push( 14 | "-sname", "vscode_" + listen_port.toString(), 15 | "-setcookie", "vscode_" + listen_port.toString()); 16 | } 17 | // Set management mode for large caches 18 | switch (this.cacheManagement) { 19 | case 'file': 20 | debugStartArgs.push("-vscode_cache_mgmt", "file", os.userInfo().username, os.tmpdir()); 21 | break; 22 | 23 | case 'compressed memory': 24 | debugStartArgs.push("-vscode_cache_mgmt", "memory", "compressed"); 25 | break; 26 | 27 | case 'memory': 28 | default: 29 | debugStartArgs.push("-vscode_cache_mgmt", "memory"); 30 | break; 31 | } 32 | // Use special command line arguments 33 | if (this.erlangArgs) { 34 | debugStartArgs = debugStartArgs.concat(this.erlangArgs) 35 | } 36 | debugStartArgs.push( 37 | "-noshell", 38 | "-pa", "src", 39 | "-pa", "ebin", 40 | "-s", "int", 41 | "-vscode_port", listen_port.toString(), 42 | "-s", "vscode_lsp_entry", "start", listen_port.toString()); 43 | var processArgs = debugStartArgs.concat([args]); 44 | 45 | var result = this.LaunchProcess("erl", startDir, processArgs); 46 | return result; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your first VS Code Extension 2 | 3 | ## What's in the folder 4 | * This folder contains all of the files necessary for your extension 5 | * `package.json` - this is the manifest file in which you declare your extension and command. 6 | The sample plugin registers a command and defines its title and command name. With this information 7 | VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | The file exports one function, `activate`, which is called the very first time your extension is 10 | activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 11 | We pass the function containing the implementation of the command as the second parameter to 12 | `registerCommand`. 13 | 14 | ## Get up and running straight away 15 | * press `F5` to open a new window with your extension loaded 16 | * run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World` 17 | * set breakpoints in your code inside `src/extension.ts` to debug your extension 18 | * find output from your extension in the debug console 19 | 20 | ## Make changes 21 | * you can relaunch the extension from the debug toolbar after changing code in `src/extension.ts` 22 | * you can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes 23 | 24 | ## Explore the API 25 | * you can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts` 26 | 27 | ## Run tests 28 | * open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests` 29 | * press `F5` to run the tests in a new window with your extension loaded 30 | * see the output of the test result in the debug console 31 | * make changes to `test/extension.test.ts` or create new test files inside the `test` folder 32 | * by convention, the test runner will only consider files matching the name pattern `**.test.ts` 33 | * you can create folders inside the `test` folder to structure your tests any way you want -------------------------------------------------------------------------------- /apps/erlangbridge/src/lsp_fun_utils.erl: -------------------------------------------------------------------------------- 1 | -module(lsp_fun_utils). 2 | 3 | 4 | -export([get_function_range/1, get_type_range/1]). 5 | 6 | -include("lsp_log.hrl"). 7 | 8 | 9 | get_function_range({function, {L, C}, _FnName, _FnArity, Body}) -> 10 | LastClause = lists:last(Body), 11 | case get_latest_lc(LastClause) of 12 | {-1,-1} -> {L,C,L,C}; 13 | {L1,C1} -> {L,C,L1+1,C1} 14 | end. 15 | 16 | get_type_range({attribute, {L, C}, type, {_Type, TypDef,_}}) -> 17 | case get_latest_lc(TypDef) of 18 | {-1,-1} -> {L,1,L,1}; 19 | {L1,C1} -> {L,C,L1+1,C1} 20 | end. 21 | 22 | % basic filter to get the latest line/column 23 | get_latest_lc({clause, {L, C}, _, _, Body}) -> 24 | case Body of 25 | [] -> {L,C}; 26 | _ -> get_latest_lc(lists:last(Body)) 27 | end; 28 | get_latest_lc({T, {L, C}, _, Args}) when T =:= call orelse T =:= 'case' orelse T =:= record -> 29 | case Args of 30 | [] -> {L,C}; 31 | _ -> get_latest_lc(lists:last(Args)) 32 | end; 33 | get_latest_lc({M, {_, _}, _, Args}) when M =:= match orelse M =:= cons orelse M =:= map_field_assoc 34 | orelse M =:= record_field -> 35 | get_latest_lc(Args); 36 | get_latest_lc({'try', {_, _}, A1, A2, A3, A4}) -> 37 | List = A1 ++ A2 ++ A3 ++ A4, 38 | get_latest_lc(lists:last(List)); 39 | get_latest_lc({T, {L, C}, Clauses}=_Other) when T =:= tuple orelse T =:= 'if' orelse T =:= map -> 40 | case Clauses of 41 | [] -> {L,C}; 42 | _ -> get_latest_lc(lists:last(Clauses)) 43 | end; 44 | get_latest_lc({type, {L, C}, _Type, TypeDefList}) when is_list(TypeDefList) -> 45 | case TypeDefList of 46 | [] -> {L,C}; 47 | _ -> get_latest_lc(lists:last(TypeDefList)) 48 | end; 49 | get_latest_lc({_, {L, C}}=_Other) -> 50 | %?LOG("get_latest_lc: 0 token: ~p", [_Other]), 51 | {L,C}; 52 | get_latest_lc({_, {L, C}, _}=_Other) -> 53 | %?LOG("get_latest_lc: 1 token: ~p", [_Other]), 54 | {L,C}; 55 | get_latest_lc({_, {L, C}, _, _}=_Other) -> 56 | %?LOG("get_latest_lc: 2 token: ~p", [_Other]), 57 | {L,C}; 58 | get_latest_lc({_, {L, C}, _, _, _}=_Other) -> 59 | %?LOG("get_latest_lc: 3 token: ~p", [_Other]), 60 | {L,C}; 61 | get_latest_lc(_Other) -> 62 | ?LOG("get_latest_lc: unknown token: ~p", [_Other]), 63 | {-1,-1}. 64 | 65 | -------------------------------------------------------------------------------- /lib/ErlangAdapterDescriptorFactory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DebugAdapterDescriptorFactory, DebugSession, DebugAdapterExecutable, ProviderResult, DebugAdapterDescriptor, 3 | DebugAdapterServer, DebugAdapterInlineImplementation 4 | } from 'vscode'; 5 | 6 | import { AddressInfo, Server, createServer } from 'net'; 7 | import {ErlangDebugSession} from './erlangDebugSession'; 8 | 9 | export class ErlangDebugAdapterDescriptorFactory implements DebugAdapterDescriptorFactory { 10 | private server?: Server; 11 | createDebugAdapterDescriptor(session: DebugSession, executable: DebugAdapterExecutable): ProviderResult { 12 | if (!this.server) { 13 | // start listening on a random port 14 | this.server = createServer(socket => { 15 | const session = new ErlangDebugSession(true); 16 | session.setRunAsServer(true); 17 | session.start(socket, socket); 18 | }).listen(0); 19 | } 20 | 21 | // make VS Code connect to debug server 22 | return new DebugAdapterServer((this.server.address()).port); 23 | } 24 | 25 | dispose() { 26 | if (this.server) { 27 | this.server.close(); 28 | } 29 | } 30 | } 31 | 32 | export class InlineErlangDebugAdapterFactory implements DebugAdapterDescriptorFactory { 33 | createDebugAdapterDescriptor(session: DebugSession, executable: DebugAdapterExecutable): ProviderResult { 34 | return new DebugAdapterInlineImplementation(new ErlangDebugSession(true)); 35 | } 36 | } 37 | 38 | export class ErlangDebugAdapterExecutableFactory implements DebugAdapterDescriptorFactory { 39 | /** 40 | * 41 | */ 42 | private extenstionPath : string; 43 | constructor(extenstionPath : string ) { 44 | this.extenstionPath = extenstionPath; 45 | } 46 | 47 | createDebugAdapterDescriptor(session: DebugSession, executable: DebugAdapterExecutable): ProviderResult { 48 | // param "executable" contains the executable optionally specified in the package.json (if any) 49 | 50 | // use the executable specified in the package.json if it exists or determine it based on some other information (e.g. the session) 51 | if (!executable) { 52 | const command = "node"; 53 | const args = [ 54 | "./out/lib/erlangDebug.js", 55 | ]; 56 | const options = { 57 | cwd: this.extenstionPath, 58 | //env: { "VAR": "some value" } 59 | }; 60 | executable = new DebugAdapterExecutable(command, args, options); 61 | } 62 | 63 | // make VS Code launch the DA executable 64 | return executable; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/gen_connection.erl: -------------------------------------------------------------------------------- 1 | -module(gen_connection). 2 | 3 | -export([behaviour_info/1]). 4 | 5 | -export([send_message_to_vscode/3, start/1]). 6 | 7 | behaviour_info(callbacks) -> 8 | [{get_port, 0}, {decode_request, 1}, {init, 1}]; 9 | behaviour_info(_) -> undefined. 10 | 11 | %%future : use a gen_server 12 | 13 | start(Module) -> 14 | Port = Module:get_port(), 15 | start(Module, to_integer(Port)). 16 | 17 | start(Module, Port) -> 18 | inets:start(), 19 | VsCodePort = Port, 20 | Module:init(Port), 21 | % subcribe to int 22 | % init_subscribe(VsCodePort), 23 | % send that debugger is ready 24 | start_command_server(VsCodePort, Module), 25 | ok. 26 | 27 | to_integer(Port) when is_atom(Port) -> 28 | erlang:list_to_integer(erlang:atom_to_list(Port)); 29 | to_integer(Port) when is_list(Port) -> 30 | erlang:list_to_integer(Port); 31 | to_integer(Port) -> Port. 32 | 33 | send_message_to_vscode(Port, Verb, Data) -> 34 | {ok, Json} = vscode_jsone:encode(Data), 35 | %io:format("Notify ~s ~p~n", [Verb, Json]), 36 | Uri = "http://127.0.0.1:" ++ 37 | erlang:integer_to_list(Port) ++ "/" ++ Verb, 38 | httpc:request(post, {Uri, [], "application/json", Json}, 39 | [], []). 40 | 41 | %------------------------------- 42 | % Command receiver (from nodejs) 43 | %------------------------------- 44 | -define(TCP_OPTIONS, [binary, {active, false}]). 45 | 46 | start_command_server(VsCodePort, Module) -> 47 | spawn(fun () -> 48 | {ok, Sock} = gen_tcp:listen(0, ?TCP_OPTIONS), 49 | % get assigned port 50 | {ok, Port} = inet:port(Sock), 51 | %send to vscode debugger 52 | send_message_to_vscode(VsCodePort, "listen", 53 | #{port => Port}), 54 | loop_command_server(Sock, Module) 55 | end). 56 | 57 | loop_command_server(Sock, Module) -> 58 | {ok, Conn} = gen_tcp:accept(Sock), 59 | Pid = spawn(fun () -> loop_handle_command(Conn, Module) 60 | end), 61 | gen_tcp:controlling_process(Conn, Pid), 62 | loop_command_server(Sock, Module). 63 | 64 | loop_handle_command(Socket, Module) -> 65 | inet:setopts(Socket, [{active, once}]), 66 | receive 67 | {tcp, Socket, Data} -> 68 | Answer = response_json(Module:decode_request(Data)), 69 | gen_tcp:send(Socket, list_to_binary(Answer)), 70 | gen_tcp:close(Socket), 71 | loop_handle_command(Socket, Module); 72 | {tcp_closed, Socket} -> 73 | error_logger:info_msg("Socket ~p closed~n", [Socket]); 74 | {tcp_error, Socket, Reason} -> 75 | error_logger:error_msg("Error on socket ~p reason: ~p~n", 76 | [Socket, Reason]) 77 | end. 78 | 79 | response_json(M) -> 80 | {ok, B} = vscode_jsone:encode(M), 81 | binary_to_list(iolist_to_binary(io_lib:fwrite("HTTP/1.0 200 OK\r\nContent-Type: application/js" 82 | "on\r\nContent-Length: ~p\r\n\r\n~s", 83 | [byte_size(B), B]))). 84 | -------------------------------------------------------------------------------- /apps/erlangbridge/test/lsp_utils_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(lsp_utils_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | 5 | -include_lib("eunit/include/eunit.hrl"). 6 | 7 | -compile([export_all, nowarn_export_all]). 8 | 9 | -include("./testlog.hrl"). 10 | 11 | % Specify a list of all unit test functions 12 | all() -> [do_is_path_excluded]. 13 | 14 | % required, but can just return Config. this is a suite level setup function. 15 | init_per_suite(Config) -> 16 | %tprof:start(#{type => call_memory}), 17 | %tprof:enable_trace(all), tprof:set_pattern('_', '_' , '_'), 18 | % do custom per suite setup here 19 | StartResult = application:start(vscode_lsp, permanent), 20 | ?assertEqual(ok, StartResult), 21 | % to intercept traces, set to true 22 | ErlangSection = #{verbose => false}, 23 | gen_lsp_config_server:update_config( 24 | erlang, 25 | ErlangSection 26 | ), 27 | Config. 28 | 29 | % required, but can just return Config. this is a suite level tear down function. 30 | end_per_suite(Config) -> 31 | % do custom per suite cleanup here 32 | application:stop(vscode_lsp), 33 | %tprof:disable_trace(all), Sample = tprof:collect(), 34 | %Inspected = tprof:inspect(Sample, process, measurement), Shell = maps:get(self(), Inspected), 35 | %tprof:format(Shell), 36 | Config. 37 | 38 | otp_files() -> 39 | % files list extracted from otp/erts/test/erlc_SUITE_data/src/ 40 | % returned by file:list_dir(...) 41 | [ 42 | "otp/erts/test/erlc_SUITE_data/src/f_include_1.erl", 43 | "otp/erts/test/erlc_SUITE_data/src/erl_test_bad.erl", 44 | "otp/erts/test/erlc_SUITE_data/src/start_bad.script", 45 | "otp/erts/test/erlc_SUITE_data/src/yecc_test_bad.yrl", 46 | "otp/erts/test/erlc_SUITE_data/src/start_ok.script", 47 | "otp/erts/test/erlc_SUITE_data/src/macro_enabled.hrl", 48 | "otp/erts/test/erlc_SUITE_data/src/GOOD-MIB.mib", 49 | [11,116,112,47,101,114,116,115,47, 50 | 116,101,115,116,47,101,114,108,99,95,83,85,73,84,69,95,100, 51 | 97,116,97,47,115,114,99,47,128512,47,101,114,108,95,116,101, 52 | 115,116,95,117,110,105,99,111,100,101,46,101,114,108], % unicode directory 53 | "otp/erts/test/erlc_SUITE_data/src/CVS/older.erl", 54 | "otp/erts/test/erlc_SUITE_data/src/older.beam" 55 | ]. 56 | 57 | otp_files_withresult() -> 58 | % ensure that File converted to unicode:characters_to_binary before calling is_path_excludedand it works as expected 59 | { 60 | lists:zipwith(fun(X, Y) -> {unicode:characters_to_binary(X),Y} end, otp_files(), [false,false,false,false,false,false,false,false,true,false]), 61 | exclude_map() 62 | }. 63 | 64 | exclude_map() -> 65 | % #{'**/.git' => true,'**/.svn' => true,'**/.hg' => true, 66 | % '**/CVS' => true,'**/.DS_Store' => true, 67 | % '**/Thumbs.db' => true}. 68 | #{ ".*/CVS(/.*)?$" => true }. 69 | 70 | check_path_excluded({FilesAndResult, Map}) -> 71 | lists:foreach(fun ({F, ExpectedResult}) -> 72 | R = lsp_utils:is_path_excluded(F, Map), 73 | ?assertEqual(ExpectedResult, R) 74 | end, FilesAndResult). 75 | 76 | do_is_path_excluded(_Config) -> 77 | check_path_excluded(otp_files_withresult()), 78 | ok. 79 | -------------------------------------------------------------------------------- /samples/rebar.tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "isShellCommand": true, 4 | "windows": { 5 | //you should have bash.exe, change the path if needed 6 | "command": "C:\\Program Files\\Git\\usr\\bin\\bash.exe", 7 | "suppressTaskName": true, 8 | "options": { 9 | "cwd": "${workspaceRoot}" 10 | }, 11 | "tasks": [ 12 | { 13 | "taskName": "build", 14 | "isBuildCommand": true, 15 | "args": [ "-c", "'./rebar", "get-deps", "compile'"], 16 | //matcher of warnings and errors 17 | "problemMatcher": [ 18 | { 19 | "fileLocation": ["relative", "${workspaceRoot}"], 20 | "severity": "warning", 21 | "pattern": { 22 | "regexp": "^(.*):(\\d+):(.*)Warning:(.*)$", 23 | "file" : 1, 24 | "line" : 2, 25 | "message": 4 26 | } 27 | }, 28 | { 29 | "fileLocation": ["relative", "${workspaceRoot}"], 30 | "severity": "error", 31 | "pattern": { 32 | "regexp": "^(.*):(\\d+):(.*)$", 33 | "file" : 1, 34 | "line" : 2, 35 | "message": 3 36 | } 37 | } 38 | ] 39 | }, 40 | { 41 | "taskName": "test", 42 | "isTestCommand": true, 43 | "args": [ "-c", "'./rebar", "eunit'"], 44 | //matcher of warnings and errors 45 | "problemMatcher": [ 46 | { 47 | "fileLocation": ["absolute"], 48 | "severity": "warning", 49 | "pattern": { 50 | "regexp": "^(.*):(\\d+):(.*)Warning:(.*)$", 51 | "file" : 1, 52 | "line" : 2, 53 | "message": 4 54 | } 55 | }, 56 | { 57 | "fileLocation": ["absolute"], 58 | "severity": "error", 59 | "pattern": { 60 | "regexp": "^(.*):(\\d+):(.*)$", 61 | "file" : 1, 62 | "line" : 2, 63 | "message": 3 64 | } 65 | } 66 | ] 67 | } 68 | ] 69 | }, 70 | "osx": { 71 | "command": "${workspaceRoot}/rebar", 72 | "suppressTaskName": true, 73 | "tasks": [ 74 | { 75 | "taskName": "build", 76 | "isBuildCommand": true, 77 | "args": ["get-deps", "compile"], 78 | //matcher of warnings and errors 79 | "problemMatcher": [ 80 | { 81 | "fileLocation": ["relative", "${workspaceRoot}"], 82 | "severity": "warning", 83 | "pattern": { 84 | "regexp": "^(.*):(\\d+):(.*)Warning:(.*)$", 85 | "file" : 1, 86 | "line" : 2, 87 | "message": 4 88 | } 89 | }, 90 | { 91 | "fileLocation": ["relative", "${workspaceRoot}"], 92 | "severity": "error", 93 | "pattern": { 94 | "regexp": "^(.*):(\\d+):(.*)$", 95 | "file" : 1, 96 | "line" : 2, 97 | "message": 3 98 | } 99 | } 100 | ] 101 | }, 102 | { 103 | "taskName": "test", 104 | "isTestCommand": true, 105 | "args": ["eunit"], 106 | //matcher of warnings and errors 107 | "problemMatcher": [ 108 | { 109 | "fileLocation": ["absolute"], 110 | "severity": "warning", 111 | "pattern": { 112 | "regexp": "^(.*):(\\d+):(.*)Warning:(.*)$", 113 | "file" : 1, 114 | "line" : 2, 115 | "message": 4 116 | } 117 | }, 118 | { 119 | "fileLocation": ["absolute"], 120 | "severity": "error", 121 | "pattern": { 122 | "regexp": "^(.*):(\\d+):(.*)$", 123 | "file" : 1, 124 | "line" : 2, 125 | "message": 3 126 | } 127 | } 128 | ] 129 | } 130 | ] 131 | }, 132 | "showOutput": "always", 133 | "suppressTaskName": true 134 | } -------------------------------------------------------------------------------- /lib/ErlangConfigurationProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | workspace, WorkspaceFolder, DebugConfiguration, DebugConfigurationProvider, CancellationToken, ProviderResult, WorkspaceConfiguration 3 | } from 'vscode'; 4 | import { ErlangSettings } from './erlangSettings'; 5 | import { stringify } from 'querystring'; 6 | //import { ErlangOutput } from './vscodeAdapter'; 7 | 8 | export class ErlangDebugConfigurationProvider implements DebugConfigurationProvider { 9 | provideDebugConfigurations?(folder: WorkspaceFolder | undefined, token?: CancellationToken): ProviderResult { 10 | if (folder) { 11 | return []; 12 | } 13 | return undefined; 14 | } 15 | 16 | resolveDebugConfiguration?(folder: WorkspaceFolder, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { 17 | let cfg = getElangConfigConfiguration(); 18 | debugConfiguration.verbose = cfg.verbose; 19 | debugConfiguration.erlangPath = cfg.erlangPath; 20 | return debugConfiguration; 21 | } 22 | }; 23 | 24 | let currentSettings: ErlangSettings = null; 25 | 26 | export function configurationChanged(): void { 27 | let erlangConf = workspace.getConfiguration("erlang"); 28 | let settings: ErlangSettings = { 29 | erlangPath: resolveVariables(erlangConf.get("erlangPath", null)), 30 | erlangArgs: erlangConf.get("erlangArgs", []), 31 | erlangDistributedNode: erlangConf.get("erlangDistributedNode", false), 32 | rebarPath: resolveVariables(erlangConf.get("rebarPath", null)), 33 | codeLensEnabled: erlangConf.get('codeLensEnabled', false), 34 | cacheManagement: erlangConf.get("cacheManagement", "memory"), 35 | inlayHintsEnabled: erlangConf.get('inlayHintsEnabled', false), 36 | debuggerRunMode: erlangConf.get("debuggerRunMode", "Server"), 37 | includePaths: erlangConf.get("includePaths", []), 38 | linting: erlangConf.get('linting', false), 39 | rebarBuildArgs: erlangConf.get("rebarBuildArgs", ['compile']), 40 | rootPath: extractRootPath(), 41 | verbose: erlangConf.get("verbose", false) 42 | }; 43 | currentSettings = settings; 44 | } 45 | 46 | export function resolveErlangSettings(erlangSection : WorkspaceConfiguration): any { 47 | const erlangconfigAsJson = JSON.stringify(erlangSection); 48 | const erlangConfiguration = JSON.parse(erlangconfigAsJson); 49 | if (erlangConfiguration) { 50 | erlangConfiguration.erlangPath = resolveVariables(erlangConfiguration.erlangPath); 51 | erlangConfiguration.rebarPath = resolveVariables(erlangConfiguration.rebarPath); 52 | } 53 | return erlangConfiguration; 54 | } 55 | 56 | function getFirstWorkspaceFolderPath(): string { 57 | let folders = workspace.workspaceFolders; 58 | if (folders && folders.length > 0) { 59 | return folders[0].uri.fsPath; 60 | } 61 | return ""; 62 | } 63 | 64 | function extractRootPath(): string { 65 | const res = getFirstWorkspaceFolderPath(); 66 | return res != "" ? res : undefined; 67 | } 68 | 69 | function resolveVariables(value: string) : string { 70 | //https://code.visualstudio.com/docs/editor/variables-reference#_predefined-variables 71 | if (!value) return value; 72 | value = value.replace('${workspaceFolder}', getFirstWorkspaceFolderPath); 73 | return value; 74 | } 75 | 76 | 77 | export function getElangConfigConfiguration(): ErlangSettings { 78 | if (!currentSettings) { 79 | configurationChanged(); 80 | // in order to debug 81 | // let output = ErlangOutput(); 82 | // output.appendLine("workspace information"); 83 | // output.appendLine(`rootPath: ${workspace.rootPath}`); 84 | // output.appendLine(`workspaceFolders: ${JSON.stringify(workspace.workspaceFolders)}`); 85 | } 86 | return currentSettings; 87 | } 88 | -------------------------------------------------------------------------------- /lib/lsp/lsp-inlinevalues.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancellationToken, Event, ExtensionContext, InlineValue,InlineValueText, 3 | InlineValueVariableLookup, InlineValueEvaluatableExpression, InlineValuesProvider, 4 | OutputChannel, ProviderResult, TextDocument, languages, 5 | workspace as Workspace, Range, Position, InlineValueContext 6 | } from 'vscode'; 7 | import { clientIsReady, erlangDocumentSelector } from './lsp-context'; 8 | import { RequestType, TextDocumentIdentifier } from 'vscode-languageclient'; 9 | import { client } from './lspclientextension'; 10 | import * as proto from 'vscode-languageserver-protocol'; 11 | 12 | export function activate(context: ExtensionContext, lspOutputChannel: OutputChannel) { 13 | //inlay should be registered only if config enabled 14 | // const enabledSetting = 'editor.inlayHints.enabled'; 15 | const values = languages.registerInlineValuesProvider(erlangDocumentSelector, 16 | new ErlangInlineValueProvider(lspOutputChannel)); 17 | context.subscriptions.push(values); 18 | } 19 | 20 | namespace protocol { 21 | export interface ErlangInlineValue { 22 | range: proto.Range; 23 | position?: proto.Position; 24 | kind: string; 25 | label: string; 26 | } 27 | 28 | export interface InlineValuesParams { 29 | textDocument: TextDocumentIdentifier; 30 | range?: proto.Range; 31 | stoppedLocation?: proto.Range; 32 | frameId?: number; 33 | threadId?: number; 34 | } 35 | export namespace InlineValueRequest { 36 | export const type = 37 | new RequestType( 38 | 'textDocument/inlineValues'); 39 | } 40 | } 41 | 42 | export class ErlangInlineValueProvider implements InlineValuesProvider 43 | { 44 | constructor(private lspOutputChannel: OutputChannel) { 45 | } 46 | 47 | onDidChangeInlineValues?: Event; 48 | provideInlineValues(document: TextDocument, viewPort: Range, context: InlineValueContext, token: CancellationToken): ProviderResult { 49 | 50 | if (!clientIsReady) { 51 | //client not ready => not connected to language server, so return empty 52 | return Promise.resolve([]); 53 | } 54 | var frameId = Math.floor(context.frameId / 100000); 55 | var threadId = (context.frameId - frameId * 100000); 56 | //var processName = this.thread_id_to_pid(threadId); 57 | 58 | const request: protocol.InlineValuesParams = { 59 | textDocument: { uri: document.uri.toString() }, 60 | range: client.code2ProtocolConverter.asRange(viewPort), 61 | frameId: frameId, 62 | threadId: threadId, 63 | stoppedLocation: client.code2ProtocolConverter.asRange(context.stoppedLocation) 64 | }; 65 | return this.sendRequest(request, token); 66 | } 67 | 68 | async sendRequest(request: protocol.InlineValuesParams, token: CancellationToken): Promise { 69 | //send requiest to lsp textDocument/inlineValues 70 | let result = await client.sendRequest(protocol.InlineValueRequest.type, request, token); 71 | //this.lspOutputChannel.appendLine(`inlineValues params => ${JSON.stringify(request)}`); 72 | return result.map(this.decode, this); 73 | } 74 | 75 | decode(value: protocol.ErlangInlineValue): InlineValue { 76 | const P = client.protocol2CodeConverter.asPosition(value.position!); 77 | const R = new Range(P, P); 78 | switch (value.kind) 79 | { 80 | case 'text': 81 | return new InlineValueText(R, value.label); 82 | case 'var': 83 | return new InlineValueVariableLookup(R, value.label, true); 84 | case 'expression': 85 | return new InlineValueEvaluatableExpression(R, value.label); 86 | default: 87 | return new InlineValueText(R, "unknown kind of inlinevalue"); 88 | } 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /lib/lsp/lspcodelens.ts: -------------------------------------------------------------------------------- 1 | import { 2 | workspace as Workspace, 3 | Uri, Disposable, WorkspaceConfiguration, CodeLens as VSCodeLens, Command as VSCommand, 4 | ProviderResult, TextDocument, CancellationToken, Range as VSCodeRange, Position as VSCodePosition 5 | } from 'vscode'; 6 | 7 | import { 8 | CodeLensRequest, CodeLensRegistrationOptions, CodeLensParams, CodeLens, 9 | TextDocumentIdentifier, Command, Range, Position 10 | } from 'vscode-languageclient'; 11 | 12 | import { debugLog, client } from './lspclientextension'; 13 | import URI from 'vscode-uri'; 14 | import { EventEmitter } from 'events'; 15 | 16 | class DocumentValidatedEvent extends EventEmitter { 17 | 18 | public Fire() { 19 | this.emit("documentValidated"); 20 | } 21 | } 22 | 23 | let codeLensEnabled = false; 24 | let autosave = Workspace.getConfiguration("files").get("autoSave")=="afterDelay"; 25 | let documentValidatedEvent = new DocumentValidatedEvent(); 26 | 27 | export function configurationChanged() : void { 28 | codeLensEnabled = Workspace.getConfiguration("erlang").get("codeLensEnabled"); 29 | autosave = Workspace.getConfiguration("files").get("autoSave")=="afterDelay"; 30 | } 31 | export async function onProvideCodeLenses(document: TextDocument, token: CancellationToken): Promise> { 32 | if (!codeLensEnabled) { 33 | return Promise.resolve([]); 34 | } 35 | if (autosave && document.isDirty) { 36 | //codeLens event is fire after didChange and before DidSave 37 | //So, when autoSave is on, Erlang document is validated on didSaved 38 | return await new Promise>(a => { 39 | let fn = () => 40 | { 41 | documentValidatedEvent.removeListener("documentValidated", fn); 42 | a(internalProvideCodeLenses(document, token)); 43 | }; 44 | documentValidatedEvent.addListener("documentValidated", fn); 45 | }); 46 | } 47 | return await internalProvideCodeLenses(document, token); 48 | } 49 | 50 | async function internalProvideCodeLenses(document: TextDocument, token: CancellationToken): Promise> { 51 | //Send request for codeLens 52 | return await client.sendRequest(CodeLensRequest.type, 53 | { 54 | textDocument: { uri: document.uri.toString() } 55 | }, 56 | token).then(async (codelenses) => await codeLensToVSCodeLens(codelenses)); 57 | } 58 | 59 | export function onResolveCodeLenses(codeLens: VSCodeLens): ProviderResult { 60 | debugLog("onResolveCodeLenses"); 61 | return codeLens; 62 | } 63 | 64 | export function onDocumentDidSave() : void { 65 | documentValidatedEvent.Fire(); 66 | } 67 | 68 | function delay(ms: number) { 69 | return new Promise(resolve => setTimeout(resolve, ms)); 70 | } 71 | 72 | 73 | 74 | async function codeLensToVSCodeLens(codelenses: CodeLens[]): Promise { 75 | //debugLog(`convert codelens : ${JSON.stringify(codelenses)}`); 76 | return Promise.resolve(codelenses.map(V => asCodeLens(V))); 77 | } 78 | 79 | function asCommand(item: Command): VSCommand { 80 | let result: VSCommand = { 81 | title: item.title, 82 | command: item.command 83 | } 84 | 85 | if (item.arguments) { result.arguments = item.arguments.map(function (arg:any): any { 86 | if (arg.indexOf && arg.indexOf("file:") === 0) 87 | return URI.parse(arg); 88 | else 89 | return arg; 90 | }); } 91 | return result; 92 | } 93 | 94 | function asCodeLens(item: CodeLens): VSCodeLens { 95 | let result = new VSCodeLens(asRange(item.range)); 96 | if (item.command) { result.command = asCommand(item.command); } 97 | return result; 98 | } 99 | 100 | function asRange(item: Range): VSCodeRange { 101 | return new VSCodeRange(asPosition(item.start), asPosition(item.end)); 102 | } 103 | 104 | function asPosition(item: Position): VSCodePosition { 105 | return new VSCodePosition(item.line, item.character); 106 | } -------------------------------------------------------------------------------- /apps/erlangbridge/src/hover_doc_layout.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% Inpired from 'https://github.com/erlang/otp/blob/master/lib/edoc/src/edoc_layout.erl' 3 | %% 4 | -module(hover_doc_layout). 5 | 6 | 7 | -export([module/2]). 8 | 9 | -include_lib("xmerl/include/xmerl.hrl"). 10 | 11 | %% @doc return a string 12 | module(Element, Options) -> 13 | Functions = layout_module(Element, init_opts(Element,Options)), 14 | %return only description 15 | Ret = [FDesc || {_FName, FDesc} <- Functions], 16 | lists:flatten(join_strings(Ret, " \n")). 17 | 18 | init_opts(_Element, Options) -> 19 | Options. 20 | 21 | layout_module({_, Xml}, Opts) -> 22 | layout_module(Xml,Opts); 23 | 24 | layout_module(#xmlElement{name = module, content = Es}, Opts) -> 25 | % Filter is like this : {filter, [{function, load_xy, 1}]} 26 | FnFilter = case proplists:get_value(filter, Opts) of 27 | undefined -> fun (_) -> true end; 28 | Array -> 29 | case proplists:get_value(function, Array) of 30 | {FName, Arity} -> fun (X) -> are_function_equal(FName, Arity, X) end; 31 | _ -> fun (_) -> true end 32 | end 33 | end, 34 | % filter the functions according to given options parameters 35 | Functions = [{function_name(E, Opts), function_description(E, Opts)} || 36 | E <- get_content(functions, Es), FnFilter(E)], 37 | Functions. 38 | 39 | are_function_equal(FName, Arity, E) -> 40 | N = list_to_atom(get_attrval(name, E)), 41 | A = get_attrval(arity, E), 42 | Result = N == FName andalso length(A) >0 andalso Arity == list_to_integer(A), 43 | Result. 44 | 45 | function_description(E, _Opts) -> 46 | Content = E#xmlElement.content, 47 | Desc = get_content(description, Content), 48 | %io:format("description : ~p~n", [Desc]), 49 | FullDesc = get_text(fullDescription, Desc), 50 | %replace all '\n' by ' \n' (two spaces) for markdown rendering 51 | lists:flatten(add_spaces(FullDesc)). 52 | 53 | add_spaces(Str) -> 54 | lists:reverse(add_spaces(Str, "")). 55 | add_spaces("", Acc) -> 56 | Acc; 57 | add_spaces([$\n | T], Acc) -> 58 | add_spaces(T, [$\n, $ ,$ | Acc ]); 59 | add_spaces([H | T], Acc) -> 60 | add_spaces(T, [H | Acc]). 61 | 62 | function_name(E, _Opts) -> 63 | Children = E#xmlElement.content, 64 | Name = get_attrval(name, E), 65 | lists:flatten(Name ++ "(" ++ function_args(get_content(args, Children)) ++ ")"). 66 | 67 | function_args(Es) -> 68 | Args = [get_text(argName, Arg#xmlElement.content) || Arg <- get_elem(arg, Es)], 69 | join_strings(Args, ","). 70 | 71 | get_elem(Name, [#xmlElement{name = Name} = E | Es]) -> 72 | [E | get_elem(Name, Es)]; 73 | get_elem(Name, [_ | Es]) -> 74 | get_elem(Name, Es); 75 | get_elem(_, []) -> 76 | []. 77 | 78 | get_content(Name, Es) -> 79 | case get_elem(Name, Es) of 80 | [#xmlElement{content = Es1}] -> 81 | Es1; 82 | [] -> [] 83 | end. 84 | 85 | get_text(Name, Es) -> 86 | case get_content(Name, Es) of 87 | [#xmlText{value = Text}] -> 88 | Text; 89 | [] -> ""; 90 | [#xmlElement{name=p, content= Es1}|_OtherXmlText] -> 91 | lists:flatten([T || T <- get_text_value(Es1 ++ _OtherXmlText)]); 92 | _Other -> 93 | error_logger:warning_msg("~p:get_text unknown xml content : ~p~n ", [?MODULE, _Other]), 94 | "" 95 | end. 96 | 97 | get_text_value([#xmlText{value = Text}|T]) -> 98 | Text ++ get_text_value(T); 99 | get_text_value([]) -> 100 | ""; 101 | get_text_value([#xmlElement{name=p, content=Es}|T]) -> 102 | get_text_value(Es) ++ get_text_value(T); 103 | get_text_value([_H|T]) -> 104 | %%ignore _H, it's not an XmlText or 'p' element 105 | get_text_value(T). 106 | 107 | 108 | get_attr(Name, [#xmlAttribute{name = Name} = A | As]) -> 109 | [A | get_attr(Name, As)]; 110 | get_attr(Name, [_ | As]) -> 111 | get_attr(Name, As); 112 | get_attr(_, []) -> 113 | []. 114 | 115 | get_attrval(Name, #xmlElement{attributes = As}) -> 116 | case get_attr(Name, As) of 117 | [#xmlAttribute{value = V}] -> 118 | V; 119 | [] -> "" 120 | end. 121 | 122 | join_strings([], _) -> 123 | []; 124 | join_strings([String], _) -> 125 | String; 126 | join_strings([String|Rest], Joiner) -> 127 | String ++ Joiner ++ join_strings(Rest, Joiner). -------------------------------------------------------------------------------- /lib/RebarShell.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { GenericShell, ILogOutput, IShellOutput } from './GenericShell'; 4 | import { getElangConfigConfiguration } from './ErlangConfigurationProvider'; 5 | 6 | /** 7 | * Provides rebar shell commands. Locates appropriate rebar executable based on provided settings. 8 | * The exit codes and stdout/stderr outputs are returned. 9 | */ 10 | export default class RebarShell extends GenericShell { 11 | protected shellOutput: RebarShellOutput; 12 | 13 | constructor(private rebarSearchPaths: string[], private defaultRebarSearchPath: string, outputChannel: ILogOutput) { 14 | super(outputChannel, new RebarShellOutput(), getElangConfigConfiguration()); 15 | } 16 | 17 | /** 18 | * Compile the Erlang apps located at `cwd`. 19 | * 20 | * @param cwd - The working directory where compilation will take place 21 | * @returns Promise resolved or rejected when rebar exits 22 | */ 23 | public compile(cwd: string) : Promise { 24 | return this.runScript(cwd, ['compile']); 25 | } 26 | 27 | /** 28 | * Execute rebar with supplied arguments. 29 | * 30 | * @param cwd - The working directory where rebar will be executed 31 | * @param commands - Arguments to rebar 32 | * @returns Promise resolved or rejected when rebar exits 33 | */ 34 | public async runScript(cwd: string, commands: string[]): Promise { 35 | // Rebar may not have execution permission (e.g. if extension is built 36 | // on Windows but installed on Linux). Let's always run rebar by escript. 37 | let escript = (process.platform == 'win32' ? 'escript.exe' : 'escript'); 38 | let rebarFileName = this.getRebarFullPath(); 39 | if (rebarFileName.search(' ') > -1) { 40 | // There is at least one space in rebarPath. Use double quotes 41 | // instead of single quotes for cross-operability between 42 | // Unix shells (e.g. bash) and the Windows shell. 43 | rebarFileName = ('"' + rebarFileName + '"'); 44 | } 45 | let args = [rebarFileName].concat(commands); 46 | 47 | this.shellOutput.clear(); 48 | 49 | let result: number; 50 | try { 51 | result = await this.RunProcess(escript, cwd, args); 52 | } catch (exitCode) { 53 | result = exitCode; 54 | } 55 | return wrapProcessExit(result, this.shellOutput.output); 56 | } 57 | 58 | /** 59 | * Get the full path to the rebar executable that will be used. 60 | * 61 | * @returns Full path to rebar executable 62 | */ 63 | private getRebarFullPath(): string { 64 | const rebarSearchPaths = this.rebarSearchPaths.slice(); 65 | if (!rebarSearchPaths.includes(this.defaultRebarSearchPath)) { 66 | rebarSearchPaths.push(this.defaultRebarSearchPath); 67 | } 68 | return this.findBestFile(rebarSearchPaths, ['rebar3', 'rebar'], 'rebar3'); 69 | } 70 | 71 | /** 72 | * Find the rebar executable to be used based on the order of `dirs` and `filenames` provided. 73 | * The order defines the priority. `defaultResult` will be used if no executable could be found. 74 | * 75 | * @param dirs - Directories to search for one of `fileNames`, in order of priority 76 | * @param fileNames - Filenames to search in each directory, in order of priority 77 | * @param defaultResult - Fallback executable path or command if rebar not found 78 | * @returns Preferred rebar executable path or command 79 | */ 80 | private findBestFile(dirs : string[], fileNames : string[], defaultResult : string) : string 81 | { 82 | var result = defaultResult; 83 | for (var i=0; i < dirs.length; i++) 84 | { 85 | for (var j=0; j < fileNames.length; j++) 86 | { 87 | var fullPath = path.normalize(path.join(dirs[i], fileNames[j])); 88 | if (fs.existsSync(fullPath)) { 89 | return fullPath; 90 | } 91 | } 92 | } 93 | return result; 94 | } 95 | } 96 | 97 | export interface RebarShellResult { 98 | exitCode: number, 99 | output: string 100 | } 101 | 102 | /** 103 | * Accumulates shell output from processes executed by GenericShell. 104 | */ 105 | class RebarShellOutput implements IShellOutput { 106 | public output: string = ''; 107 | 108 | append(value: string) { 109 | this.output += value; 110 | } 111 | 112 | clear() { 113 | this.output = ''; 114 | } 115 | } 116 | 117 | const wrapProcessExit = (exitCode: number, output: string) => ({ 118 | exitCode, 119 | output 120 | }); 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Erlang for Visual Studio Code 2 | 3 | [![Visual Studio Marketplace](https://img.shields.io/visual-studio-marketplace/v/pgourlain.erlang?style=for-the-badge&label=VS%20Marketplace&logo=visual-studio-code)](https://marketplace.visualstudio.com/items?itemName=pgourlain.erlang) 4 | [![Installs](https://img.shields.io/visual-studio-marketplace/i/pgourlain.erlang?style=for-the-badge)](https://marketplace.visualstudio.com/items?itemName=pgourlain.erlang) 5 | [![Build Status](https://img.shields.io/github/actions/workflow/status/pgourlain/vscode_erlang/pr-verify.yml?branch=master&style=for-the-badge&logo=github)](https://github.com/pgourlain/vscode_erlang/actions?query=workflow:pr-verify) 6 | [![License](https://img.shields.io/github/license/pgourlain/vscode_erlang?style=for-the-badge&logo=erlang)](https://github.com/pgourlain/vscode_erlang/blob/master/LICENSE) 7 | 8 | This extension adds support for the Erlang language to Visual Studio Code, including editing, building and debugging. 9 | 10 | 11 | ## Editing support 12 | 13 | - Syntax highlighting 14 | - Automatic indentation 15 | - Erlang IntelliSense 16 | - Shows errors and warnings dynamically while you type 17 | - Go To Definition/Peek Definition 18 | - Hover help for standard functions 19 | - Hover for project functions showing head clauses 20 | - CodeLens showing exported functions and references 21 | - InlayHints showing parameters name in function calls 22 | - disable by default : enable in configuration settings 23 | - limits : only works with locals calls 24 | 25 | ![editing](images/vscode-erlang-editing.gif) 26 | 27 | InlayHints in function calls 28 | 29 | ![inlayHints](images/vscode-erlang-inlayhints.png) 30 | - showing parameter name when it doesn't match with caller var name 31 | 32 | ## Build 33 | 34 | ![build](images/vscode-erlang-build.png) 35 | 36 | - Standard rebar3 is the default build tool, also rebar is supported. The rebar.config file should be placed in the root directory. 37 | - Build arguments are configurable, by default "compile" is used 38 | - You can override the default in configuration file (i.e. workspace settings) 39 | 40 | ![build](images/vscode-erlang-build-args.png) 41 | 42 | ## Debugger 43 | 44 | - Variables List shows variables from the current scope 45 | - Call Stack shows Erlang processes and allows to control them with e.g. Pause and Continue 46 | - Standard commands Step Over, Step Into, Step Out supported 47 | - Full breakpoints support: 48 | - Regular breakpoints 49 | - Function Breakpoints: use format module:function/arity 50 | - Conditional Breakpoints 51 | - Hit-Count Breakpoints 52 | 53 | ![debug-inlinevalues](images/vscode-erlang-inlinevalues.png) 54 | 55 | ## Running debugger 56 | 57 | You can provide a specific command line to 'erl' in launch.json configuration file in "arguments" entry. 58 | 59 | ![debug1](images/vscode-erlang-debug-args.png) 60 | 61 | The modified code may be automatically build before debugger is started. To set automatic build up you need to: 62 | 63 | 1. Add to launch.json file the entry "preLaunchTask": "rebar3 compile" 64 | 1. Select **Configure Task** in the alert, choose **Create tasks.json file from template** and then **Others: Example to run an arbitrary command** 65 | 1. This will create tasks.json for you. Change both label and command to "rebar3 compile". 66 | 1. Add entry "problemMatcher": "$erlang" 67 | 68 | ![debug](images/vscode-erlang-build-task.png) 69 | 70 | Then, before debugging is started, modified files will be recompiled automatically. 71 | 72 | ## Using this extension in Erlang Docker instance 73 | 74 | Clone this repo, and try it : 75 | - [vscode-remote-try-erlang](https://github.com/pgourlain/vscode-remote-try-erlang) 76 | 77 | For more information about vscode and containers : 78 | - [setup vscode for containers](https://code.visualstudio.com/docs/containers/overview) and [remote container documentation](https://code.visualstudio.com/docs/remote/containers) 79 | 80 | ## Available commands 81 | 82 | Support for Erlang tools, including rebar3, EUnit and Dialyzer 83 | 84 | ![commands](images/vscode-erlang-commands.png) 85 | 86 | - Dialyzer warnings displayed in Problems tab for easy navigation 87 | 88 | ## Settings 89 | 90 | - `erlang.erlangPath` - Directory location of erl/escript 91 | - `erlang.erlangArgs` - Arguments passed to Erlang backend 92 | - `erlang.erlangDistributedNode` - Start the Erlang backend in a distributed Erlang node for extension development 93 | - `erlang.rebarPath` - Directory location of rebar/rebar3 94 | - `erlang.rebarBuildArgs` - Arguments to provide to rebar/rebar3 build command 95 | - `erlang.includePaths` - Include paths are read from rebar.config, and also standard set of paths is used. This setting is for special cases when the default behaviour is not enough 96 | - `erlang.linting` - Enable/disable dynamic validation of opened Erlang source files 97 | - `erlang.codeLensEnabled` - Enable/Disable CodeLens 98 | - `erlang.cacheManagement` - Specify where and how to store large cache tables 99 | - `erlang.inlayHintsEnabled` - Enable/Disable InlayHints 100 | - `erlang.verbose` - Activate technical traces for use in the extension development 101 | 102 | ## Help 103 | 104 | [Some configuration tricks](./HELP.MD) 105 | 106 | ## Credits 107 | 108 | File 'Erlang.tmLanguage' is inspired from 109 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/lsp_signature_doc_layout.erl: -------------------------------------------------------------------------------- 1 | -module(lsp_signature_doc_layout). 2 | 3 | -export([module/2]). 4 | 5 | -include_lib("xmerl/include/xmerl.hrl"). 6 | 7 | 8 | %% @doc return a signature compatible to vscode model 9 | module(Element, Options) -> 10 | Functions = layout_module(Element, init_opts(Element,Options)), 11 | Functions. 12 | 13 | init_opts(_Element, Options) -> 14 | Options. 15 | 16 | layout_module({_, Xml}, Opts) -> 17 | layout_module(Xml,Opts); 18 | layout_module(#xmlElement{name = module, content = Es}, Opts) -> 19 | % Filter is like this : {filter, [{function, load_xy, 1}]} 20 | FnFilter = case proplists:get_value(filter, Opts) of 21 | undefined -> fun (_) -> true end; 22 | Array -> 23 | case proplists:get_value(function, Array) of 24 | {FName, Arity} -> fun (X) -> are_function_equal(FName, Arity, X) end; 25 | _ -> fun (_) -> true end 26 | end 27 | end, 28 | % filter the functions according to given options parameters 29 | Functions = [{function_name(E, Opts), function_description(E, Opts)} || 30 | E <- get_content(functions, Es), FnFilter(E)], 31 | Functions. 32 | 33 | are_function_equal(FName, Arity, E) -> 34 | N = list_to_atom(get_attrval(name, E)), 35 | A = get_attrval(arity, E), 36 | Result = N == FName andalso length(A) >0 andalso (Arity == list_to_integer(A) orelse (Arity == any)), 37 | Result. 38 | 39 | function_description(E, _Opts) -> 40 | Content = E#xmlElement.content, 41 | Desc = get_content(description, Content), 42 | %io:format("description : ~p~n", [Desc]), 43 | FullDesc = get_text(fullDescription, Desc), 44 | %replace all '\n' by ' \n' (two spaces) for markdown rendering 45 | lists:flatten(add_spaces(FullDesc)). 46 | 47 | add_spaces(Str) -> 48 | lists:reverse(add_spaces(Str, "")). 49 | add_spaces("", Acc) -> 50 | Acc; 51 | add_spaces([$\n | T], Acc) -> 52 | add_spaces(T, [$\n, $ ,$ | Acc ]); 53 | add_spaces([H | T], Acc) -> 54 | add_spaces(T, [H | Acc]). 55 | 56 | function_name(E, _Opts) -> 57 | Children = E#xmlElement.content, 58 | Name = get_attrval(name, E), 59 | lists:flatten(Name ++ "(" ++ function_args(get_content(args, Children)) ++ ")"). 60 | 61 | function_args(Es) -> 62 | Args = [get_text(argName, Arg#xmlElement.content) || Arg <- get_elem(arg, Es)], 63 | join_strings(Args, ","). 64 | 65 | get_elem(Name, [#xmlElement{name = Name} = E | Es]) -> 66 | [E | get_elem(Name, Es)]; 67 | get_elem(Name, [_ | Es]) -> 68 | get_elem(Name, Es); 69 | get_elem(_, []) -> 70 | []. 71 | 72 | get_content(Name, Es) -> 73 | case get_elem(Name, Es) of 74 | [#xmlElement{content = Es1}] -> 75 | Es1; 76 | [] -> [] 77 | end. 78 | 79 | get_text(Name, Es) -> 80 | case get_content(Name, Es) of 81 | [#xmlText{value = Text}] -> 82 | Text; 83 | [] -> ""; 84 | [#xmlElement{name=p, content= Es1}|_OtherXmlText] -> 85 | lists:flatten([T || T <- get_text_value(Es1 ++ _OtherXmlText)]); 86 | _Other -> 87 | error_logger:warning_msg("~p:get_text unknown xml content : ~p~n ", [?MODULE, _Other]), 88 | "" 89 | end. 90 | 91 | get_text_value([#xmlText{value = Text}|T]) -> 92 | Text ++ get_text_value(T); 93 | get_text_value([]) -> 94 | ""; 95 | get_text_value([#xmlElement{name=p, content=Es}|T]) -> 96 | get_text_value(Es) ++ get_text_value(T); 97 | get_text_value([_H|T]) -> 98 | %%ignore _H, it's not an XmlText or 'p' element 99 | get_text_value(T). 100 | 101 | 102 | get_attr(Name, [#xmlAttribute{name = Name} = A | As]) -> 103 | [A | get_attr(Name, As)]; 104 | get_attr(Name, [_ | As]) -> 105 | get_attr(Name, As); 106 | get_attr(_, []) -> 107 | []. 108 | 109 | get_attrval(Name, #xmlElement{attributes = As}) -> 110 | case get_attr(Name, As) of 111 | [#xmlAttribute{value = V}] -> 112 | V; 113 | [] -> "" 114 | end. 115 | 116 | join_strings([], _) -> 117 | []; 118 | join_strings([String], _) -> 119 | String; 120 | join_strings([String|Rest], Joiner) -> 121 | String ++ Joiner ++ join_strings(Rest, Joiner). 122 | 123 | 124 | signatures_sample() -> 125 | [ 126 | #{ 127 | label => <<"signature 1 from signature_doc_layout">>, 128 | documentation => #{ 129 | kind => <<"markdown">>, 130 | value => <<"">> 131 | }, 132 | parameters => [ 133 | #{ 134 | label => <<"parameter 1 label">>, 135 | documentation => <<"doc of parameter 1">> 136 | } 137 | ] 138 | }, 139 | #{ 140 | label => <<"signature 2 from signature_doc_layout">>, 141 | documentation => #{ 142 | kind => <<"markdown">>, 143 | value => <<"# Header \n Some text \n ```typescript\nsomecode();\n```">> 144 | }, 145 | parameters => [ 146 | #{ 147 | label => <<"parameter 1 label">>, 148 | documentation => <<"doc of parameter 1">> 149 | }, 150 | #{ 151 | label => <<"parameter 2 label">>, 152 | documentation => <<"doc of parameter 2">> 153 | } 154 | ] 155 | } 156 | ]. 157 | -------------------------------------------------------------------------------- /apps/erlangbridge/test/lsp_navigation_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(lsp_navigation_SUITE). 2 | 3 | -include_lib("common_test/include/ct.hrl"). 4 | 5 | -include_lib("eunit/include/eunit.hrl"). 6 | 7 | -compile([export_all, nowarn_export_all]). 8 | 9 | -include("./testlog.hrl"). 10 | 11 | % Specify a list of all unit test functions 12 | all() -> [testnavigation, test_macros]. 13 | 14 | % required, but can just return Config. this is a suite level setup function. 15 | init_per_suite(Config) -> 16 | % do custom per suite setup here 17 | StartResult = application:start(vscode_lsp, permanent), 18 | ?assertEqual(ok, StartResult), 19 | % to intercept traces, set to true 20 | ErlangSection = #{verbose => false}, 21 | gen_lsp_config_server:update_config(erlang, 22 | ErlangSection), 23 | Config. 24 | 25 | % required, but can just return Config. this is a suite level tear down function. 26 | end_per_suite(Config) -> 27 | % do custom per suite cleanup here 28 | application:stop(vscode_lsp), 29 | Config. 30 | 31 | % optional, can do function level setup for all functions, 32 | % or for individual functions by matching on TestCase. 33 | init_per_testcase(_TestCase, Config) -> 34 | % do custom test case setup here 35 | Config. 36 | 37 | % optional, can do function level tear down for all functions, 38 | % or for individual functions by matching on TestCase. 39 | end_per_testcase(_TestCase, Config) -> 40 | % do custom test case cleanup here 41 | Config. 42 | 43 | %%%%%%%%%%%%%%%% 44 | %% test cases %% 45 | %%%%%%%%%%%%%%%% 46 | 47 | check_result(Result, ExpectedStart, ExpectedEnd, ExpectedModuleName) when is_tuple(Result) -> 48 | error_logger:info_msg("check_result: ~p ~p ~p ~p~n", [Result, ExpectedStart, ExpectedEnd, ExpectedModuleName]), 49 | {FilePath, Line, _StartColumn, _EndColumn} = Result, 50 | ?assertEqual(ExpectedStart, Line - 1), 51 | ?assertEqual(ExpectedEnd, Line - 1), 52 | BaseName = filename:basename(FilePath), 53 | ?assertEqual(ExpectedModuleName, BaseName); 54 | check_result([Result], ExpectedStart, ExpectedEnd, ExpectedModuleName) when is_tuple(Result) -> 55 | check_result(Result, ExpectedStart, ExpectedEnd, ExpectedModuleName). 56 | 57 | check_result(Result, ExpectedStart, ExpectedEnd) when is_tuple(Result) -> 58 | error_logger:info_msg("check_result/3: ~p ~p ~p~n", [Result, ExpectedStart, ExpectedEnd]), 59 | {_File, Line, _StartColumn, _EndColumn} = Result, 60 | 61 | ?assertEqual(ExpectedStart, Line - 1), 62 | ?assertEqual(ExpectedEnd, Line - 1), 63 | ok; 64 | check_result([Result], ExpectedStart, ExpectedEnd) when is_tuple(Result) -> 65 | check_result(Result, ExpectedStart, ExpectedEnd). 66 | 67 | dotestfiles(AppDir, [{FileName, LocationTests}|T]) -> 68 | dotestfile(filename:join(AppDir,FileName), LocationTests), 69 | dotestfiles(AppDir, T); 70 | dotestfiles(_AppDir, []) -> 71 | ok. 72 | 73 | dotestfile(FilePath, [{Line,Column, ExpectedLine, _ExpectedColumn, ExpectedModuleName}|T]) -> 74 | GoTo = lsp_navigation:definition(FilePath, Line, Column), 75 | ?writeConsole(GoTo), 76 | check_result(GoTo, ExpectedLine, ExpectedLine, ExpectedModuleName), 77 | dotestfile(FilePath, T); 78 | dotestfile(FilePath, [{Line,Column, ExpectedLine, _ExpectedColumn}|T]) -> 79 | GoTo = lsp_navigation:definition(FilePath, Line, Column), 80 | ?writeConsole(GoTo), 81 | check_result(GoTo, ExpectedLine, ExpectedLine), 82 | dotestfile(FilePath, T); 83 | dotestfile(FilePath, [{Line,Column, ExpectedLine}|T]) -> 84 | GoTo = lsp_navigation:definition(FilePath, Line,Column), 85 | ?writeConsole(GoTo), 86 | check_result(GoTo, ExpectedLine, ExpectedLine), 87 | dotestfile(FilePath, T); 88 | 89 | dotestfile(_FilePath, []) -> 90 | ok. 91 | 92 | navigation_datatests() -> 93 | % Format : [{InputFile, [{Line, Column, ResultLine, ResultColumn, ResultModule} ,...]} ,...] 94 | % Line and Column are should be one index based 95 | % ResultLine and ResultColumn should be zero index based 96 | [{"main.erl",[ 97 | {16, 10, 17, 0}, 98 | {8, 28, 3, 6}, 99 | {6, 37, 3, 0, "mod_test.erl"}, 100 | {24, 18, 9, 0, "data_goods.erl"}, 101 | {24, 24, 20, 0} 102 | ] 103 | }, 104 | {"gen_msg_test1.erl",[ 105 | {10, 36, 27, 0}, 106 | {16, 32, 24, 0}, 107 | {13, 34, 33, 0} 108 | ] 109 | }, 110 | {"gen_msg_test2.erl",[ 111 | {10, 34, 27, 0}, 112 | {13, 38, 24, 0} 113 | ] 114 | } 115 | ]. 116 | 117 | testnavigation(Config) -> 118 | % write standard erlang code to test whatever you want 119 | % use pattern matching to specify expected return values 120 | AppDir = (?config(data_dir, Config)), 121 | % set root config, induce readline all filename from AppDir 122 | gen_lsp_config_server:update_config(root, AppDir), 123 | % add all documents from root dir into documents server 124 | gen_lsp_doc_server:root_available(), 125 | gen_lsp_doc_server:config_change(), 126 | dotestfiles(AppDir, navigation_datatests()), 127 | ok. 128 | 129 | test_macros(Config) -> 130 | % write standard erlang code to test whatever you want 131 | % use pattern matching to specify expected return values 132 | AppDir = (?config(data_dir, Config)), 133 | % set root config, induce readline all filename from AppDir 134 | gen_lsp_config_server:update_config(root, AppDir), 135 | % add all documents from root dir into documents server 136 | gen_lsp_doc_server:root_available(), 137 | gen_lsp_doc_server:config_change(), 138 | 139 | % test macros 140 | SyntaxTree = gen_lsp_doc_server:get_dodged_syntax_tree(filename:join(AppDir,"data_goods.erl")), 141 | Macros = lsp_syntax:get_macros(SyntaxTree), 142 | ?assertEqual(true, is_list(Macros)), 143 | ?assertEqual([{{14,11},'DEFAULT',7}], Macros) 144 | . 145 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/gen_lsp_config_server.erl: -------------------------------------------------------------------------------- 1 | -module(gen_lsp_config_server). 2 | -behavior(gen_server). 3 | 4 | %% API 5 | -export([start_link/0]). 6 | -export([standard_modules/0, bifs/0]). 7 | -export([update_config/2, root/0, tmpdir/0, username/0, codeLensEnabled/0, includePaths/0, linting/0, 8 | verbose/0, autosave/0, proxy/0, search_files_exclude/0, search_exclude/0, 9 | formatting_line_length/0, inlayHintsEnabled/0, verbose_is_include/1]). 10 | 11 | %% gen_server callbacks 12 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 13 | 14 | -include("lsp_log.hrl"). 15 | -define(SERVER, ?MODULE). 16 | 17 | -record(state, {config, standard_modules, bifs}). 18 | 19 | start_link() -> 20 | gen_server:start_link({local, ?SERVER}, ?MODULE, [],[]). 21 | 22 | standard_modules() -> 23 | gen_server:call(?SERVER, {standard_modules}). 24 | 25 | bifs() -> 26 | gen_server:call(?SERVER, {bifs}). 27 | 28 | update_config(Key, Value) -> 29 | Res = gen_server:call(?SERVER, {update_config, Key, Value}), 30 | compute_erlang_section(Key), 31 | Res. 32 | 33 | compute_erlang_section(Key) -> 34 | if 35 | Key =:= erlang -> 36 | Excludes = get_config_entry(erlang, verboseExcludeFilter, ""), 37 | Splitted = lists:map(fun(X) -> {lsp_utils:to_binary(X), false} end, lists:flatmap( fun(X) -> string:split(X, ",") end, string:split(Excludes, ";"))), 38 | gen_server:call(?SERVER, {update_config, erlang_computed, #{ verboseExcludeFilter => maps:from_list(Splitted)}}); 39 | true -> ok 40 | end. 41 | 42 | 43 | get_config() -> 44 | gen_server:call(?SERVER, get_config). 45 | 46 | get_config_entry(Section, Entry, Default) -> 47 | SectionMap = maps:get(Section, get_config(), #{}), 48 | maps:get(Entry, SectionMap, Default). 49 | 50 | root() -> 51 | maps:get(root, get_config(), ""). 52 | 53 | codeLensEnabled() -> 54 | get_config_entry(erlang, codeLensEnabled, false). 55 | 56 | inlayHintsEnabled() -> 57 | get_config_entry(erlang, inlayHintsEnabled, false). 58 | 59 | includePaths() -> 60 | get_config_entry(erlang, includePaths, []). 61 | 62 | linting() -> 63 | get_config_entry(erlang, linting, true). 64 | 65 | verbose() -> 66 | get_config_entry(erlang, verbose, false). 67 | 68 | verbose_is_include(Method) -> 69 | M = get_config_entry(erlang_computed, verboseExcludeFilter, #{}), 70 | lsp_utils:try_get(Method, M, true). 71 | 72 | autosave() -> 73 | get_config_entry(computed, autosave, true). 74 | 75 | proxy() -> 76 | get_config_entry(http, proxy, ""). 77 | 78 | tmpdir() -> 79 | get_config_entry(computed, tmpdir, ""). 80 | 81 | username() -> 82 | get_config_entry(computed, username, ""). 83 | 84 | %%-------------------------------------------------------------------- 85 | %% @doc Exclude filters for search in workspace. 86 | %% 87 | %% It is a combination of Visual Studio Code settings `files.exclude' and 88 | %% `search.exclude' as Visual Studio Code GUI does merge these for searching 89 | %% but extensions get the pure settings and we have to merge them explicitly. 90 | %% @end 91 | %%-------------------------------------------------------------------- 92 | search_exclude() -> 93 | %% From https://code.visualstudio.com/docs/getstarted/settings 94 | %% files.exclude: 95 | %% Configure glob patterns for excluding files and folders. For example, 96 | %% the File Explorer decides which files and folders to show or hide based 97 | %% on this setting. Refer to the `search.exclude` setting to define 98 | %% search-specific excludes. 99 | %% search.exclude: 100 | %% Configure glob patterns for excluding files and folders in fulltext 101 | %% searches and quick open. Inherits all glob patterns from the 102 | %% `files.exclude` setting. 103 | maps:merge(get_config_entry(files, exclude, #{}), 104 | get_config_entry(search, exclude, #{})). 105 | 106 | %%-------------------------------------------------------------------- 107 | %% @doc Exclude filters for searching project files. 108 | %% 109 | %% It is a combination of Visual Studio Code settings `files.exclude', 110 | %% `files.watcherExclude' and `search.exclude'. 111 | %% @end 112 | %%-------------------------------------------------------------------- 113 | search_files_exclude() -> 114 | %% From https://code.visualstudio.com/docs/getstarted/settings 115 | %% files.watcherExclude: 116 | %% Configure paths or glob patterns to exclude from file watching. 117 | maps:merge(get_config_entry(files, watcherExclude, #{}), search_exclude()). 118 | 119 | formatting_line_length() -> 120 | get_config_entry(erlang, formattingLineLength, 100). 121 | 122 | init(_Args) -> 123 | StandardModules = lists:foldl(fun (Dir, Acc) -> 124 | lists:foldl(fun (File, AccF) -> 125 | [filename:rootname(File) | AccF] 126 | end, Acc, filelib:wildcard("*.beam", Dir)) 127 | end, [], code:get_path()), 128 | BIFs = sets:to_list(lists:foldl(fun ({Name, _Arity}, Acc) -> 129 | sets:add_element(atom_to_list(Name), Acc) 130 | end, sets:new(), erlang:module_info(exports))), 131 | process_flag(trap_exit, true), % to terminate/2 be called at exit 132 | {ok, #state{config = #{}, standard_modules = StandardModules, bifs = BIFs}}. 133 | 134 | handle_call({standard_modules}, _From, State) -> 135 | {reply, State#state.standard_modules, State}; 136 | handle_call({bifs}, _From, State) -> 137 | {reply, State#state.bifs, State}; 138 | handle_call({update_config, Key, Value}, _From, State) -> 139 | {reply, #{}, State#state{config = (State#state.config)#{Key => Value}}}; 140 | handle_call(get_config, _From, State) -> 141 | {reply, State#state.config, State}; 142 | handle_call(_Request, _From, State) -> 143 | {reply, ok, State}. 144 | 145 | handle_cast(stop, State) -> 146 | {stop, normal, State}. 147 | 148 | handle_info(_Info, State) -> 149 | {noreply, State}. 150 | 151 | terminate(_Reason, #state{config = #{computed := #{tmpdir := TmpDir, username := UserName}}}) -> 152 | %% Delete old caches left there by brutally killed extension instances 153 | gen_lsp_doc_server:delete_unused_caches(TmpDir, UserName), 154 | ok; 155 | terminate(_Reason, _State) -> 156 | ok. 157 | 158 | code_change(_OldVersion, State, _Extra) -> 159 | {ok, State}. 160 | -------------------------------------------------------------------------------- /lib/erlangDebugConnection.ts: -------------------------------------------------------------------------------- 1 | import {ErlangConnection} from './erlangConnection'; 2 | import { DebugProtocol } from '@vscode/debugprotocol'; 3 | import { FunctionBreakpoint } from './ErlangShellDebugger'; 4 | 5 | 6 | export class ErlangDebugConnection extends ErlangConnection { 7 | protected get_ErlangFiles(): string[] { 8 | return ["gen_connection.erl", "vscode_connection.erl", "vscode_jsone.erl"]; 9 | } 10 | 11 | protected handle_erlang_event(url: string, body : any) : void { 12 | //this method handle every event receiver from erlang 13 | switch(url) { 14 | case "/listen": 15 | this.erlangbridgePort = body.port; 16 | this.emit("listen", "erlang bridge listen on port :" + this.erlangbridgePort.toString()); 17 | break; 18 | case "/interpret": 19 | this.emit("new_module", body.module); 20 | break; 21 | case "/new_process": 22 | this.emit("new_process", body.process); 23 | break; 24 | case "/new_status": 25 | this.emit("new_status", body.process, body.status, body.reason, body.module, body.line); 26 | break; 27 | case "/new_break": 28 | this.emit("new_break", body.module, body.line); 29 | break; 30 | case "/on_break": 31 | this.emit("on_break", body.process, body.module, body.line, body.stacktrace); 32 | break; 33 | case "/delete_break": 34 | break; 35 | case "/fbp_verified": 36 | this.emit("fbp_verified", body.module, body.name, body.arity); 37 | break; 38 | default: 39 | this.debug("receive from erlangbridge :" + url + ", body :" + JSON.stringify(body)); 40 | break; 41 | } 42 | } 43 | 44 | public setBreakPointsRequest(moduleName : string, breakPoints : DebugProtocol.Breakpoint[], functionBreakpoints: FunctionBreakpoint[]) : Promise { 45 | if (this.erlangbridgePort > 0) { 46 | let bps = moduleName + "\r\n"; 47 | breakPoints.forEach(bp => { 48 | bps += `line ${bp.line}\r\n`; 49 | }); 50 | functionBreakpoints.forEach(bp => { 51 | bps += `function ${bp.functionName} ${bp.arity}\r\n`; 52 | }); 53 | return this.post("set_bp", bps).then(res => { 54 | return true; 55 | }, err => { 56 | return false; 57 | }); 58 | } else { 59 | return new Promise(() => false); 60 | } 61 | } 62 | 63 | public debuggerContinue(pid : string) : Promise { 64 | if (this.erlangbridgePort > 0) { 65 | return this.post("debugger_continue", pid).then(res => { 66 | return true; 67 | }, err => { 68 | return false; 69 | }); 70 | } else { 71 | return new Promise(() => false); 72 | } 73 | 74 | } 75 | 76 | public debuggerNext(pid : string) : Promise { 77 | if (this.erlangbridgePort > 0) { 78 | return this.post("debugger_next", pid).then(res => { 79 | return true; 80 | }, err => { 81 | return false; 82 | }); 83 | } else { 84 | return new Promise(() => false); 85 | } 86 | } 87 | 88 | public debuggerStepIn(pid : string) : Promise { 89 | if (this.erlangbridgePort > 0) { 90 | return this.post("debugger_stepin", pid).then(res => { 91 | return true; 92 | }, err => { 93 | return false; 94 | }); 95 | } else { 96 | return new Promise(() => false); 97 | } 98 | } 99 | 100 | public debuggerStepOut(pid : string) : Promise { 101 | if (this.erlangbridgePort > 0) { 102 | return this.post("debugger_stepout", pid).then(res => { 103 | return true; 104 | }, err => { 105 | return false; 106 | }); 107 | } else { 108 | return new Promise(() => false); 109 | } 110 | } 111 | 112 | public debuggerPause(pid: string): Promise { 113 | if (this.erlangbridgePort > 0) { 114 | return this.post("debugger_pause", pid).then(res => { 115 | return true; 116 | }, err => { 117 | return false; 118 | }); 119 | } else { 120 | return new Promise(() => false); 121 | } 122 | } 123 | 124 | public debuggerBindings(pid: string, frameId: string): Promise { 125 | if (this.erlangbridgePort > 0) { 126 | return this.post("debugger_bindings", pid + "\r\n" + frameId).then(res => { 127 | //this.debug(`result of bindings : ${JSON.stringify(res)}`); 128 | return (>res); 129 | }, err => { 130 | this.debug(`debugger_bindings error : ${err}`); 131 | return []; 132 | }); 133 | } else { 134 | return new Promise(() => []); 135 | } 136 | } 137 | 138 | public debuggerEval(pid: string, frameId : string, expression: string): Promise { 139 | if (this.erlangbridgePort > 0) { 140 | return this.post("debugger_eval", pid + "\r\n" + frameId + "\r\n" + expression).then(res => { 141 | return (res); 142 | }, err => { 143 | this.debug(`debugger_eval error : ${err}`); 144 | return []; 145 | }); 146 | } else { 147 | return new Promise(() => []); 148 | } 149 | } 150 | public debuggerExit(): Promise { 151 | if (this.erlangbridgePort > 0) { 152 | //this.debug('exit') 153 | return this.post("debugger_exit", "").then(res => { 154 | this.debug('exit yes') 155 | return (res); 156 | }, err => { 157 | this.debug('exit no') 158 | this.debug(`debugger_exit error : ${err}`); 159 | return []; 160 | }); 161 | } else { 162 | return new Promise(() => []); 163 | } 164 | } 165 | 166 | Quit() : void { 167 | this.debuggerExit().then(() => { 168 | this.events_receiver.close(); 169 | }); 170 | } 171 | } -------------------------------------------------------------------------------- /apps/erlangbridge/src/lsp_completion.erl: -------------------------------------------------------------------------------- 1 | -module(lsp_completion). 2 | 3 | -export([disable_completion/0, module_function/2, record/2, field/3, variable/3, atom/2, attribute/1]). 4 | 5 | disable_completion() -> 6 | [#{ 7 | label => <<>> 8 | }]. 9 | 10 | module_function(Module, Prefix) -> 11 | File = gen_lsp_doc_server:get_module_file(Module), 12 | ExportsResult = case gen_lsp_doc_server:get_syntax_tree(File) of 13 | undefined -> 14 | standard_module_exports(Module); 15 | SyntaxTree -> 16 | syntax_tree_exports(SyntaxTree) 17 | end, 18 | case ExportsResult of 19 | {ok, Exports} -> 20 | NamesOnly = [atom_to_list(element(1, Export)) || Export <- Exports], 21 | Unique = sets:to_list(sets:from_list(NamesOnly)), 22 | lists:filtermap(fun (Name) -> 23 | case lists:prefix(Prefix, Name) of 24 | true -> {true, module_function_item(Module, Name)}; 25 | _ -> false 26 | end 27 | end, Unique); 28 | {error, _Error} -> 29 | [] 30 | end. 31 | 32 | module_function_item(Module, Name) -> 33 | Description = lsp_navigation:function_description(Module, list_to_atom(Name)), 34 | case Description of 35 | <<>> -> 36 | #{ 37 | label => list_to_binary(Name), 38 | kind => 3 % Function 39 | }; 40 | _ -> 41 | #{ 42 | label => list_to_binary(Name), 43 | kind => 3, % Function 44 | documentation => #{ 45 | value => Description, 46 | kind => <<"markdown">> 47 | } 48 | } 49 | end. 50 | 51 | standard_module_exports(Module) -> 52 | case code:ensure_loaded(Module) of 53 | {module, _} -> {ok, proplists:get_value(exports, Module:module_info())}; 54 | _ -> {error, "No such module"} 55 | end. 56 | 57 | syntax_tree_exports(SyntaxTree) -> 58 | {ok, lists:foldl(fun 59 | ({attribute, _, export, Exports}, Acc) -> 60 | lists:append(Exports, Acc); 61 | (_, Acc) -> 62 | Acc 63 | end, [], SyntaxTree)}. 64 | 65 | record(File, Prefix) -> 66 | lists:filtermap(fun 67 | ({attribute, _, record, {Name, _}}) -> 68 | case lists:prefix(Prefix, atom_to_list(Name)) of 69 | true -> {true, #{ 70 | label => list_to_binary(atom_to_list(Name)), 71 | kind => 22 % Struct 72 | }}; 73 | _ -> false 74 | end; 75 | (_) -> 76 | false 77 | end, gen_lsp_doc_server:get_syntax_tree(File)). 78 | 79 | field(File, Record, Prefix) -> 80 | lists:filtermap(fun (Field) -> 81 | case lists:prefix(Prefix, atom_to_list(Field)) of 82 | true -> {true, #{label => list_to_binary(atom_to_list(Field)), kind => 5}}; 83 | _ -> false 84 | end 85 | end, lsp_navigation:record_fields(File, Record)). 86 | 87 | variable(File, Line, Prefix) -> 88 | FileSyntaxTree = gen_lsp_doc_server:get_syntax_tree(File), 89 | Function = lsp_navigation:find_function_with_line(FileSyntaxTree, Line), 90 | case Function of 91 | undefined -> 92 | []; 93 | _ -> 94 | Names = erl_syntax_lib:fold(fun (SyntaxTree, Acc) -> 95 | case SyntaxTree of 96 | {var, _, Name} -> 97 | [Name | Acc]; 98 | _ -> 99 | Acc 100 | end 101 | end, [], Function), 102 | Unique = sets:to_list(sets:from_list(Names)), 103 | lists:filtermap(fun (Name) -> 104 | case lists:prefix(Prefix, atom_to_list(Name)) of 105 | true -> {true, #{ 106 | label => list_to_binary(atom_to_list(Name)), 107 | kind => 6 % Variable 108 | }}; 109 | _ -> false 110 | end 111 | end, Unique) 112 | end. 113 | 114 | atom(File, Prefix) -> 115 | LocalAtoms = lists:filtermap(fun (#{label := Name} = Item) -> 116 | case lists:prefix(Prefix, atom_to_list(Name)) of 117 | true -> {true, Item}; 118 | _ -> false 119 | end 120 | end, sets:to_list(sets:from_list(local_atoms(File)))), 121 | StandardModules = lists:filtermap(fun (Module) -> 122 | case lists:prefix(Prefix, Module) of 123 | true -> {true, #{ 124 | label => list_to_binary(Module), 125 | kind => 9 % Module 126 | }}; 127 | _ -> false 128 | end 129 | end, gen_lsp_config_server:standard_modules()), 130 | ProjectModules = lists:filtermap(fun (Module) -> 131 | case lists:prefix(Prefix, Module) of 132 | true -> {true, #{ 133 | label => list_to_binary(Module), 134 | kind => 9 % Module 135 | }}; 136 | _ -> false 137 | end 138 | end, gen_lsp_doc_server:project_modules()), 139 | BIFs = lists:filtermap(fun (Function) -> 140 | case lists:prefix(Prefix, Function) of 141 | true -> {true, module_function_item(erlang, Function)}; 142 | _ -> false 143 | end 144 | end, gen_lsp_config_server:bifs()), 145 | LocalAtoms ++ StandardModules ++ ProjectModules ++ BIFs. 146 | 147 | local_atoms(File) -> 148 | FileSyntaxTree = gen_lsp_doc_server:get_syntax_tree(File), 149 | AtomTypes = lists:foldl(fun (TopLevelSyntaxTree, Acc) -> 150 | erl_syntax_lib:fold(fun (SyntaxTree, AccS) -> 151 | case SyntaxTree of 152 | {function, Position, Name, _Arity, _Clauses} -> 153 | AccS#{{Position, Name} => 3}; % Function 154 | {remote, _, {atom, ModulePosition, Module}, {atom, FunctionPosition, Function}} -> 155 | AccS#{{ModulePosition, Module} => 0, {FunctionPosition, Function} => 0}; 156 | {call, _, {atom, Position, Name}, _} -> 157 | AccS#{{Position, Name} => 0}; 158 | {atom, Position, Name} -> 159 | AccS#{{Position, Name} => 13}; % Enum 160 | _ -> 161 | AccS 162 | end 163 | end, Acc, TopLevelSyntaxTree) 164 | end, #{}, FileSyntaxTree), 165 | maps:fold(fun 166 | ({_, _Name}, 0, Acc) -> 167 | Acc; 168 | ({_, Name}, 3, Acc) -> 169 | Module = list_to_atom(filename:rootname(filename:basename(File))), 170 | [#{label => Name, kind => 3, documentation => 171 | #{value => lsp_navigation:function_description(Module, Name), kind => <<"markdown">> } 172 | } | Acc]; 173 | ({_, Name}, Type, Acc) -> 174 | [#{label => Name, kind => Type} | Acc] 175 | end, [], AtomTypes). 176 | 177 | attribute(Prefix) -> 178 | Attributes = ["module", "export", "include", "include_lib", "record", "behaviour", "import", 179 | "compile", "vsn", "on_load", "callback", "define", "file", "type", "spec"], 180 | lists:filtermap(fun (Attribute) -> 181 | case lists:prefix(Prefix, Attribute) of 182 | true -> {true, #{ 183 | label => list_to_binary(Attribute), 184 | kind => 11 % Unit 185 | }}; 186 | _ -> false 187 | end 188 | end, Attributes). 189 | -------------------------------------------------------------------------------- /lib/erlangConnection.ts: -------------------------------------------------------------------------------- 1 | 2 | import { EventEmitter } from 'events' 3 | import * as http from 'http'; 4 | import * as path from 'path'; 5 | import { ErlangShellForDebugging } from './ErlangShellDebugger'; 6 | import { ILogOutput } from './GenericShell'; 7 | import * as fs from 'fs'; 8 | import { AddressInfo } from 'net'; 9 | 10 | const rebar3BuildPath = path.join('_build', 'default', 'lib', 'vscode_lsp'); 11 | // Based on JS output path, not TS path 12 | export let erlangBridgePath = path.join(__dirname, "..", "..", rebar3BuildPath); 13 | 14 | /** 15 | * Update path to erlangbridge Erlang app based on extension path. 16 | * 17 | * @param currentExtensionPath - The current extension path 18 | */ 19 | export function setExtensionPath(currentExtensionPath: string): void { 20 | erlangBridgePath = path.join(currentExtensionPath, rebar3BuildPath); 21 | } 22 | /** this class is responsible to send/receive debug command to erlang bridge */ 23 | export abstract class ErlangConnection extends EventEmitter { 24 | erlangbridgePort: number; 25 | protected events_receiver: http.Server; 26 | _output: ILogOutput; 27 | verbose: boolean; 28 | 29 | 30 | public get isConnected(): boolean { 31 | return this.erlangbridgePort > 0; 32 | } 33 | 34 | public constructor(output: ILogOutput) { 35 | super(); 36 | this._output = output; 37 | this.erlangbridgePort = -1; 38 | this.verbose = true; 39 | } 40 | 41 | protected log(msg: string): void { 42 | if (this._output) { 43 | this._output.appendLine(msg); 44 | } 45 | } 46 | 47 | protected logAppend(msg: string): void { 48 | if (this._output) { 49 | //this._output.append(msg); 50 | } 51 | } 52 | 53 | protected debug(msg: string): void { 54 | if (this._output) { 55 | this._output.appendLine("debug:" + msg); 56 | } 57 | } 58 | 59 | protected error(msg: string): void { 60 | if (this._output) { 61 | //this._output.error(msg); 62 | } 63 | } 64 | 65 | public async Start(verbose: boolean): Promise { 66 | this.verbose = verbose; 67 | return new Promise((a, r) => { 68 | //this.debug("erlangConnection.Start"); 69 | this.compile_erlang_connection().then(() => { 70 | return this.start_events_receiver().then(res => { 71 | a(res); 72 | }, exitCode => { 73 | //this.log("reject"); 74 | r(exitCode); 75 | }); 76 | }, exiCode => { 77 | r(`Erlang compile failed : ${exiCode}`); 78 | }); 79 | }); 80 | } 81 | 82 | public abstract Quit() : void; 83 | 84 | private compile_erlang_connection(): Promise { 85 | return new Promise((a, r) => { 86 | const ebinDir = path.join(erlangBridgePath, '..', 'ebin'); 87 | this.log(`compiling erlang bridge to '${ebinDir}'`); 88 | 89 | var compiler = new ErlangShellForDebugging(this.verbose ? this._output : null); 90 | var erlFiles = this.get_ErlangFiles(); 91 | //create dir if not exists 92 | //compile erlang_connection in specifc diretory to avoid that the target can access to lspxxx.beam at debug time 93 | if (!fs.existsSync(ebinDir)) { 94 | fs.mkdirSync(ebinDir); 95 | } 96 | 97 | let args = ["-o", path.normalize(ebinDir)].concat(erlFiles); 98 | return compiler.Compile(path.join(erlangBridgePath,'src'), args).then(res => { 99 | //this.debug("Compilation of erlang bridge...ok"); 100 | a(res); 101 | }, exitCode => { 102 | this.error("Compilation of erlang bridge...ko"); 103 | r(exitCode); 104 | }); 105 | }); 106 | } 107 | 108 | private start_events_receiver(): Promise { 109 | if (this.verbose) 110 | this.debug("Starting http listener..."); 111 | return new Promise((accept, reject) => { 112 | this.events_receiver = http.createServer((req, res) => { 113 | var url = req.url; 114 | var body = []; 115 | var jsonBody = null; 116 | req.on('error', err => { 117 | this.error("request error"); 118 | }).on('data', chunk => { 119 | body.push(chunk); 120 | }).on('end', () => { 121 | //here : receive all events from erlangBridge 122 | var sbody = Buffer.concat(body).toString(); 123 | try { 124 | //this.log("body:" + sbody); 125 | jsonBody = JSON.parse(sbody); 126 | this.handle_erlang_event(url, jsonBody); 127 | } 128 | catch (err) { 129 | this.error("error while receving command :" + err + "\r\n" + sbody); 130 | } 131 | res.statusCode = 200; 132 | res.setHeader('Content-Type', 'text/plain'); 133 | res.end('ok'); 134 | }); 135 | }); 136 | this.events_receiver.listen(0, '127.0.0.1', () => { 137 | var p = (this.events_receiver.address()).port; 138 | if (this.verbose) 139 | this.debug(` on http://127.0.0.1:${p}\n`); 140 | accept(p); 141 | }); 142 | 143 | }); 144 | } 145 | 146 | protected abstract handle_erlang_event(url: string, body: any); 147 | 148 | protected abstract get_ErlangFiles(): string[]; 149 | 150 | protected post(verb: string, body?: string, multilineBody?: boolean): Promise { 151 | return new Promise((a, r) => { 152 | if (!body) { 153 | body = ""; 154 | } 155 | var options: http.RequestOptions = { 156 | host: "127.0.0.1", 157 | path: verb, 158 | port: this.erlangbridgePort, 159 | method: "POST", 160 | headers: { 161 | 'Content-Type': 'plain/text', 162 | 'Content-Length': Buffer.byteLength(body) 163 | } 164 | } 165 | if (multilineBody) { 166 | options.headers['X-Multiline-Body'] = 'true'; 167 | } 168 | var postReq = http.request(options, response => { 169 | var body = ''; 170 | response.on('data', buf => { 171 | body += buf; 172 | }); 173 | 174 | response.on('end', () => { 175 | try { 176 | //this.log("command response : " + body); 177 | var parsed = JSON.parse(body); 178 | a(parsed); 179 | } catch (err) { 180 | this.log("unable to parse response as JSON:" + err); 181 | //console.error('Unable to parse response as JSON', err); 182 | r(err); 183 | } 184 | }); 185 | response.on("error", err => { 186 | this.log("error while sending command to erlang :" + err); 187 | }); 188 | }); 189 | postReq.write(body); 190 | postReq.end(); 191 | }); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/lsp_inlayhints.erl: -------------------------------------------------------------------------------- 1 | -module(lsp_inlayhints). 2 | 3 | -export([inlayhint_analyze/3, generate_inlayhints/3]). 4 | 5 | -include("lsp_log.hrl"). 6 | 7 | -ifdef(OTP_RELEASE). 8 | inlayhint_analyze(SyntaxTree, _CurrentFile, Dict) -> 9 | %do not crash on error, just log, in order to avoid exception while parsing file 10 | try 11 | internal_inlayhint_analyze(SyntaxTree, _CurrentFile, Dict) 12 | catch Error:Exception:StackStrace -> 13 | ?LOG("inlayhint_analyze error ~p:~p, stacktrace:~p", [Error, Exception, StackStrace]), 14 | Dict 15 | end. 16 | -else. 17 | inlayhint_analyze(SyntaxTree, _CurrentFile, Dict) -> 18 | try 19 | internal_inlayhint_analyze(SyntaxTree, _CurrentFile, Dict) 20 | catch Error:Exception -> 21 | ?LOG("inlayhint_analyze error ~p:~p, stacktrace:~p", [Error, Exception, erlang:get_stacktrace()]), 22 | Dict 23 | end. 24 | -endif. 25 | 26 | internal_inlayhint_analyze(SyntaxTree, _CurrentFile, #{defs := Defs, calls := Calls} = Dict) -> 27 | case SyntaxTree of 28 | %TODO, get args from spec if exists 29 | {function, _Location, FuncName, Arity, Content} when Arity > 0 -> 30 | F = #{ 31 | func_name => FuncName, 32 | arity => Arity, 33 | args => extract_function_args(Content) 34 | }, 35 | maps:put(defs, Defs ++ [F], Dict); 36 | {call, _LocationCall, {atom, _, FName}, Args} when length(Args) > 0 -> 37 | %sample Args 38 | %functionName:test_literal_guard, args: [{integer,{11,24},5}] 39 | %functionName:test_literal_guard), args: [{var,{12,24},'B'}] 40 | F = #{ 41 | func_name => FName, 42 | args => index_args(Args) 43 | }, 44 | maps:put(calls, Calls ++ [F], Dict); 45 | % {remote,{32,15},{atom,{32,5},sample_lib},{atom,{32,16},fn_utils1}} 46 | %{call,{32,5},{remote,{32,15},{atom,{32,5},sample_lib}, {atom,{32,16},fn_utils1}},[{var,{32,26},'X'}]} 47 | {call, _LocationCall, {remote, _, {atom, _,ModuleName}, {atom, _, FName}}, Args} -> 48 | %to avoid collision with local function name, add module as prefix 49 | % ~p to avoid crash on function name with unicode characters 50 | FuncName = lists:flatten(io_lib:format("~s.~p", [ModuleName, FName])), 51 | case get_remote_function_content(ModuleName, FName) of 52 | {true, RemoteFuns} -> 53 | F = #{ 54 | func_name => FuncName, 55 | args => index_args(Args) 56 | }, 57 | NewDict = maps:put(calls, Calls ++ [F], Dict), 58 | FDefs = [#{ 59 | func_name => FuncName, 60 | arity => Arity, 61 | args => extract_function_args(Content) 62 | } || {Arity, Content} <- RemoteFuns], 63 | %?LOG("remote_functions:~p <==> ~p",[F, FDefs]), 64 | maps:put(defs, Defs ++ FDefs, NewDict); 65 | 66 | _ -> Dict 67 | end; 68 | _Other -> 69 | Dict 70 | end. 71 | 72 | get_remote_function_content(ModuleName, FName) -> 73 | SModuleName = lsp_utils:to_string(ModuleName), 74 | FilteredModules = lists:filter(fun (X) -> X =:= SModuleName end, 75 | gen_lsp_doc_server:project_modules()), 76 | case FilteredModules of 77 | [] -> undefined; 78 | [_FindModule] -> 79 | case gen_lsp_doc_server:get_module_file(ModuleName) of 80 | undefined -> undefined; 81 | SourceFile -> 82 | %check is file under workspace 83 | Functions = lsp_navigation:functions(SourceFile, FName), 84 | {true, [{Arity, Content} || 85 | {function, _, _FName, Arity, Content} <- Functions]} 86 | end; 87 | _ -> undefined 88 | end. 89 | 90 | index_args(Args) -> 91 | %%add index for each arg, will use later to match with definition 92 | {LR, _} = lists:mapfoldl(fun(A, Acc) -> {{Acc, A}, Acc + 1} end, 0, Args), 93 | LR. 94 | 95 | extract_function_args([]) -> 96 | []; 97 | extract_function_args(Clauses) -> 98 | ArgsList = lists:filtermap(fun (X) -> 99 | case X of 100 | {clause, _, Args, _, _} -> {true, Args}; 101 | _ -> false 102 | end 103 | end, Clauses), 104 | zip_args(ArgsList). 105 | 106 | %% match the better human readable args, by skipping '_xxx' var names 107 | zip_args([]) -> []; 108 | zip_args([Args]) -> 109 | Args; 110 | zip_args([Args1,Args2|Tail]) -> 111 | %?LOG("zip_args(~p,~p)",[Args1, Args2]), 112 | Res = lists:zipwith(fun (Arg1,Arg2) -> 113 | %compare two args, if var is '_' => skip it 114 | case Arg1 of 115 | {var, _, VarName} -> choose_better_arg(lsp_utils:to_string(VarName), Arg1, Arg2); 116 | _ -> Arg2 117 | end 118 | end, Args1, Args2), 119 | zip_args([Res]++Tail). 120 | 121 | choose_better_arg(VarName, Arg1, Arg2) -> 122 | F = string:find(VarName, "_"), 123 | if 124 | F =:= VarName -> Arg2; 125 | true -> Arg1 126 | end. 127 | 128 | 129 | generate_inlayhints([], _Defs, _Macros) -> 130 | []; 131 | generate_inlayhints([#{args := Args, func_name := FName} | RestCalls], Defs, Macros) -> 132 | L = length(Args), 133 | %% filter calls and definitions by arity and name 134 | case 135 | lists:filter( 136 | fun(#{arity := Arity, func_name := Dfn}) -> Arity =:= L andalso FName =:= Dfn end, Defs 137 | ) 138 | of 139 | [#{args := DArgs}] -> filter_and_map_args(Args, DArgs, Macros); 140 | _ -> [] 141 | end ++ 142 | generate_inlayhints(RestCalls, Defs, Macros). 143 | 144 | filter_and_map_args([], _Defs, _Macros) -> 145 | []; 146 | filter_and_map_args([{Index, Call} | RestCalls], Defs, Macros) -> 147 | %get corresponding argument in definition 148 | D = lists:nth(Index + 1, Defs), 149 | %?LOG("try_match:~p, ~p", [Call, D]), 150 | try_match_parameter(Call, D, Macros) ++ filter_and_map_args(RestCalls, Defs, Macros). 151 | 152 | try_match_parameter({var, _, VarName}, {var, _, DefVarName}, _Macros) when VarName =:= DefVarName -> 153 | % call var name and definition var name are equal, no inlay for this argument 154 | []; 155 | try_match_parameter({_P, Position, _}, DefArg, Macros) -> 156 | NewPosition = update_position(Position, Macros), 157 | new_inlay(NewPosition, DefArg); 158 | try_match_parameter({call, Position, _,_}, DefArg, _Macros) -> 159 | new_inlay(Position, DefArg); 160 | try_match_parameter(_, _, _) -> 161 | []. 162 | 163 | update_position(LC, undefined) -> 164 | LC; 165 | update_position(LC, []) -> 166 | LC; 167 | update_position({Line, Column}, Macros) -> 168 | %% check if the position is inside a macro 169 | case lists:filter(fun 170 | ({{L,C}, _, Length}) when L =:= Line, C =< Column, C + Length >= Column -> true; 171 | (_) -> false 172 | end, 173 | Macros) of 174 | [] -> {Line, Column}; 175 | [{{NewLine, NewColumn}, _, _}] -> {NewLine, NewColumn}; 176 | _ -> {Line, Column} 177 | end. 178 | 179 | new_inlay(Position, DefArg) -> 180 | Label = case DefArg of 181 | {match, _, _,_} -> "match: "; 182 | {map, _,_} -> "map: "; 183 | {cons, _,_,_} -> "cons: "; 184 | {var, _, DefVarName} -> lsp_utils:to_string(DefVarName) ++ ": "; 185 | _ -> false 186 | end, 187 | if 188 | Label =:= false -> []; 189 | true -> 190 | [{ 191 | Position, 192 | Label, 193 | "parameter" 194 | }] 195 | end. 196 | -------------------------------------------------------------------------------- /lib/GenericShell.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ChildProcess, spawn } from 'child_process' 3 | import { EventEmitter } from 'events' 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | import { fstat } from 'fs'; 7 | import { ErlangSettings } from './erlangSettings'; 8 | 9 | //inspired from https://github.com/WebFreak001/code-debug/blob/master/src/backend/mi2/mi2.ts for inspiration of an EventEmitter 10 | const nonOutput = /^(?:\d*|undefined)[\*\+\=]|[\~\@\&\^]/; 11 | 12 | function couldBeOutput(line: string) { 13 | if (nonOutput.exec(line)) 14 | return false; 15 | return true; 16 | } 17 | 18 | /** 19 | * Defines support for log output from the GenericShell class. 20 | */ 21 | export interface ILogOutput { 22 | appendLine(value: string): void; 23 | debug(msg : string) : void; 24 | } 25 | 26 | /** 27 | * Defines support for raw shell output from processes spawned by GenericShell. 28 | */ 29 | export interface IShellOutput { 30 | append(value: string): void; 31 | } 32 | 33 | export class GenericShell extends EventEmitter { 34 | protected childProcess: ChildProcess; 35 | protected logOutput: ILogOutput; 36 | protected shellOutput: IShellOutput; 37 | protected buffer: string = ""; 38 | protected errbuf: string = ""; 39 | public erlangPath: string = null; 40 | public erlangArgs : string[] = []; 41 | public erlangDistributedNode: boolean = false; 42 | public cacheManagement: string = "memory"; 43 | 44 | //provide IGenericShellConfiguration, in order to avoid dependencies on vscode module (it doesn't works with debugger-adpater) 45 | constructor(logOutput?: ILogOutput, shellOutput?: IShellOutput, erlangConfiguration?: ErlangSettings) { 46 | super(); 47 | this.logOutput = logOutput; 48 | this.shellOutput = shellOutput; 49 | 50 | if (erlangConfiguration) { 51 | // Find Erlang 'bin' directory 52 | let erlangPath = erlangConfiguration.erlangPath; 53 | if (erlangPath) { 54 | if (erlangPath.match(/^[A-Za-z]:/)) { 55 | // Windows absolute path (C:\...) is applicable on Windows only 56 | if (process.platform == 'win32') { 57 | this.erlangPath = path.win32.normalize(erlangPath); 58 | } 59 | } else { 60 | erlangPath = path.normalize(erlangPath); 61 | if (! fs.existsSync(erlangPath)) { 62 | erlangPath = path.join(erlangConfiguration.rootPath, erlangPath); 63 | } 64 | if (fs.existsSync(erlangPath)) { 65 | this.erlangPath = erlangPath; 66 | } 67 | } 68 | } 69 | this.erlangArgs = erlangConfiguration.erlangArgs; 70 | this.erlangDistributedNode = erlangConfiguration.erlangDistributedNode; 71 | this.cacheManagement = erlangConfiguration.cacheManagement; 72 | } 73 | } 74 | 75 | protected RunProcess(processName, startDir: string, args: string[]): Promise { 76 | 77 | return new Promise((resolve, reject) => { 78 | this.LaunchProcess(processName, startDir, args).then(started => { 79 | this.on('close', (exitCode) => { 80 | if (exitCode == 0) { 81 | resolve(0); 82 | } else { 83 | reject(exitCode); 84 | } 85 | }); 86 | }) 87 | }); 88 | } 89 | 90 | protected LaunchProcess(processName, startDir: string, args: string[], quiet: boolean = false): Promise { 91 | return new Promise((resolve, reject) => { 92 | try { 93 | if (!quiet) { 94 | if (this.erlangPath) { 95 | this.log("log",`using erlang binaries from path : '${this.erlangPath}'`); 96 | } 97 | this.log("log", `starting : ${processName} \r\n` + args.join(" ")); 98 | } 99 | var childEnv = null; 100 | if (this.erlangPath) { 101 | childEnv = process.env; 102 | var separator = process.platform == 'win32' ? ";" : ":"; 103 | childEnv.PATH = this.erlangPath + separator + childEnv.PATH; 104 | } 105 | 106 | this.childProcess = spawn(processName, args, { cwd: startDir, shell: true, stdio: 'pipe', env : childEnv }); 107 | this.childProcess.on('error', error => { 108 | this.log("stderr", error.message); 109 | if (process.platform == 'win32') { 110 | this.log("stderr", "ensure '" + processName + "' is in your path."); 111 | } 112 | }); 113 | this.childProcess.stdout.on('data', this.stdout.bind(this)); 114 | this.childProcess.stderr.on('data', this.stderr.bind(this)); 115 | 116 | this.childProcess.on('exit', (exitCode: number, signal: string) => { 117 | this.log("log", processName + ' exit code:' + exitCode); 118 | this.emit('close', exitCode); 119 | }); 120 | resolve(true); 121 | } 122 | catch (error) { 123 | reject(error); 124 | } 125 | }); 126 | } 127 | 128 | onOutput(lines) { 129 | lines = lines.split('\n'); 130 | lines.forEach(line => { 131 | this.log("stdout", line); 132 | this.appendToShellOutput(`${line}\n`); 133 | }); 134 | } 135 | 136 | onOutputPartial(line) { 137 | if (couldBeOutput(line)) { 138 | this.logNoNewLine("stdout", line); 139 | this.appendToShellOutput(line); 140 | return true; 141 | } 142 | return false; 143 | } 144 | 145 | stdout(data) { 146 | if (typeof data == "string") 147 | this.buffer += data; 148 | else 149 | this.buffer += data.toString("utf8"); 150 | let end = this.buffer.lastIndexOf('\n'); 151 | if (end != -1) { 152 | this.onOutput(this.buffer.substr(0, end)); 153 | this.buffer = this.buffer.substr(end + 1); 154 | } 155 | if (this.buffer.length) { 156 | if (this.onOutputPartial(this.buffer)) { 157 | this.buffer = ""; 158 | } 159 | } 160 | } 161 | 162 | stderr(data) { 163 | if (typeof data == "string") 164 | this.errbuf += data; 165 | else 166 | this.errbuf += data.toString("utf8"); 167 | let end = this.errbuf.lastIndexOf('\n'); 168 | if (end != -1) { 169 | this.onOutputStderr(this.errbuf.substr(0, end)); 170 | this.errbuf = this.errbuf.substr(end + 1); 171 | } 172 | if (this.errbuf.length) { 173 | this.logNoNewLine("stderr", this.errbuf); 174 | this.errbuf = ""; 175 | } 176 | } 177 | 178 | onOutputStderr(lines) { 179 | lines = lines.split('\n'); 180 | lines.forEach(line => { 181 | this.log("stderr", line); 182 | this.appendToShellOutput(line); 183 | }); 184 | } 185 | 186 | protected logNoNewLine(type: string, msg: string): void { 187 | this.logOutput && this.logOutput.appendLine(msg); 188 | this.emit("msg", type, msg); 189 | } 190 | 191 | protected log(type: string, msg: string): void { 192 | this.logOutput && this.logOutput.appendLine(msg); 193 | this.emit("msg", type, msg[msg.length - 1] == '\n' ? msg : (msg + "\n")); 194 | } 195 | 196 | protected debug(msg : string) : void { 197 | this.logOutput && this.logOutput.debug(msg); 198 | } 199 | 200 | public Send(what: string) { 201 | this.log("log", what); 202 | this.childProcess.stdin.write(what); 203 | this.childProcess.stdin.write('\r\n'); 204 | } 205 | 206 | private appendToShellOutput(data: string) { 207 | this.shellOutput && this.shellOutput.append(data); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /lib/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as path from 'path'; 4 | import { 5 | workspace as Workspace, window as Window, ExtensionContext, TextDocument, OutputChannel, WorkspaceFolder, Uri, debug, 6 | languages, IndentAction, DebugAdapterDescriptorFactory 7 | } from 'vscode'; 8 | 9 | import * as Adapter from './vscodeAdapter'; 10 | import * as Rebar from './RebarRunner'; 11 | import * as Eunit from './eunitRunner'; 12 | import { ErlangDebugConfigurationProvider, configurationChanged, getElangConfigConfiguration } from './ErlangConfigurationProvider'; 13 | import { ErlangDebugAdapterDescriptorFactory, InlineErlangDebugAdapterFactory, ErlangDebugAdapterExecutableFactory } from './ErlangAdapterDescriptorFactory'; 14 | import * as erlangConnection from './erlangConnection'; 15 | 16 | import * as LspClient from './lsp/lspclientextension'; 17 | 18 | var myoutputChannel: OutputChannel; 19 | 20 | // this method is called when your extension is activated 21 | // your extension is activated the very first time the command is executed 22 | export function activate(context: ExtensionContext) { 23 | erlangConnection.setExtensionPath(context.extensionPath); 24 | 25 | myoutputChannel = Adapter.ErlangOutput(); 26 | // Use the console to output diagnostic information (console.log) and errors (console.error) 27 | // This line of code will only be executed once when your extension is activated 28 | console.log('Congratulations, your extension "erlang" is now active!'); 29 | myoutputChannel.appendLine("erlang extension is active"); 30 | 31 | //configuration of erlang language -> documentation : https://code.visualstudio.com/Docs/extensionAPI/vscode-api#LanguageConfiguration 32 | var disposables = []; 33 | // The command has been defined in the package.json file 34 | // Now provide the implementation of the command with registerCommand 35 | // The commandId parameter must match the command field in package.json 36 | //disposables.push(vscode.commands.registerCommand('extension.rebarBuild', () => { runRebarCommand(['compile']);})); 37 | 38 | var rebar = new Rebar.RebarRunner(); 39 | rebar.activate(context); 40 | 41 | var eunit = new Eunit.EunitRunner(); 42 | eunit.activate(context); 43 | 44 | disposables.push(debug.registerDebugConfigurationProvider("erlang", new ErlangDebugConfigurationProvider())); 45 | disposables.push(Workspace.onDidChangeConfiguration((e) => configurationChanged())); 46 | disposables.push(Workspace.onDidChangeWorkspaceFolders((e) => configurationChanged())); 47 | 48 | let runMode = getElangConfigConfiguration().debuggerRunMode; 49 | let factory: DebugAdapterDescriptorFactory; 50 | switch (runMode) { 51 | case 'server': 52 | // run the debug adapter as a server inside the extension and communicating via a socket 53 | factory = new ErlangDebugAdapterDescriptorFactory(); 54 | break; 55 | 56 | case 'inline': 57 | // run the debug adapter inside the extension and directly talk to it 58 | factory = new InlineErlangDebugAdapterFactory(); 59 | break; 60 | 61 | case 'external': default: 62 | // run the debug adapter as a separate process 63 | factory = new ErlangDebugAdapterExecutableFactory(context.extensionPath); 64 | break; 65 | } 66 | 67 | disposables.push(debug.registerDebugAdapterDescriptorFactory('erlang', factory)); 68 | if ('dispose' in factory) { 69 | disposables.push(factory); 70 | } 71 | disposables.forEach((disposable => context.subscriptions.push(disposable))); 72 | LspClient.activate(context); 73 | 74 | languages.setLanguageConfiguration("erlang", { 75 | onEnterRules: [ 76 | // Module comment: always continue comment 77 | { 78 | beforeText: /^%%% .*$/, 79 | action: { indentAction: IndentAction.None, appendText: "%%% " } 80 | }, 81 | { 82 | beforeText: /^%%%.*$/, 83 | action: { indentAction: IndentAction.None, appendText: "%%%" } 84 | }, 85 | // Comment line with double %: continue comment if needed 86 | { 87 | beforeText: /^\s*%% .*$/, 88 | afterText: /^.*\S.*$/, 89 | action: { indentAction: IndentAction.None, appendText: "%% " } 90 | }, 91 | { 92 | beforeText: /^\s*%%.*$/, 93 | afterText: /^.*\S.*$/, 94 | action: { indentAction: IndentAction.None, appendText: "%%" } 95 | }, 96 | // Comment line with single %: continue comment if needed 97 | { 98 | beforeText: /^\s*% .*$/, 99 | afterText: /^.*\S.*$/, 100 | action: { indentAction: IndentAction.None, appendText: "% " } 101 | }, 102 | { 103 | beforeText: /^\s*%.*$/, 104 | afterText: /^.*\S.*$/, 105 | action: { indentAction: IndentAction.None, appendText: "%" } 106 | }, 107 | // Any other comment line: do nothing, ignore below rules 108 | { 109 | beforeText: /^\s*%.*$/, 110 | afterText: /^\s*$/, 111 | action: { indentAction: IndentAction.None } 112 | }, 113 | 114 | // Empty line: do nothing, ignore below rules 115 | { 116 | beforeText: /^\s*$/, 117 | action: { indentAction: IndentAction.None } 118 | }, 119 | 120 | // Before guard sequence (before 'when') 121 | { 122 | beforeText: /.*/, 123 | afterText: /^\s*when(\s.*)?$/, 124 | action: { indentAction: IndentAction.None, appendText: " " } 125 | }, 126 | // After guard sequence (after 'when') 127 | { 128 | beforeText: /^\s*when(\s.*)?->\s*$/, 129 | action: { indentAction: IndentAction.None, appendText: " " } 130 | }, 131 | { 132 | beforeText: /^\s*when(\s.*)?(,|;)\s*$/, 133 | action: { indentAction: IndentAction.None, appendText: " " } 134 | }, 135 | 136 | // Start of clause, right hand side of an assignment, after 'after', etc. 137 | { 138 | beforeText: /^.*(->|[^=]=|\s+(after|begin|case|catch|if|maybe|of|receive|try))\s*$/, 139 | action: { indentAction: IndentAction.Indent } 140 | }, 141 | 142 | // Not closed bracket 143 | { 144 | beforeText: /^.*[(][^)]*$/, 145 | action: { indentAction: IndentAction.Indent } 146 | }, 147 | { 148 | beforeText: /^.*[{][^}]*$/, 149 | action: { indentAction: IndentAction.Indent } 150 | }, 151 | { 152 | beforeText: /^.*[[][^\]]*$/, 153 | action: { indentAction: IndentAction.Indent } 154 | }, 155 | 156 | // One liner clause but not the last 157 | { 158 | beforeText: /^.*->.+;\s*$/, 159 | action: { indentAction: IndentAction.None } 160 | }, 161 | // End of function or attribute (e.g. export list) 162 | { 163 | beforeText: /^.*\.\s*$/, 164 | action: { indentAction: IndentAction.Outdent, removeText: 9000 } 165 | }, 166 | // End of clause but not the last 167 | // FIXME: After a guard (;) it falsely outdents 168 | { 169 | beforeText: /^.*;\s*$/, 170 | action: { indentAction: IndentAction.Outdent } 171 | }, 172 | // Last statement of a clause 173 | // TODO: double outdent or outdent + removeText: 174 | { 175 | beforeText: /^.*[^;,[({<]\s*$/, 176 | action: { indentAction: IndentAction.Outdent } 177 | } 178 | ] 179 | }); 180 | } 181 | 182 | export function deactivate(): Thenable { 183 | return LspClient.deactivate(); 184 | } 185 | -------------------------------------------------------------------------------- /syntaxes/test/syntax_test_data_types.erl: -------------------------------------------------------------------------------- 1 | -module(syntax_test_data_types). 2 | 3 | -compile(export_all). 4 | 5 | -spec ascii_numbers() -> ok. 6 | ascii_numbers() -> 7 | $ , 8 | $!, $", $#, $$, $%, $&, $', $(, $), $*, $+, $,, $-, $., $/, 9 | $0, $1, $2, $3, $4, $5, $6, $7, $8, $9, 10 | $:, $;, $<, $=, $>, $?, $@, 11 | $A, $B, $C, $D, $E, $F, $G, $H, $I, $J, $K, $L, $M, $N, $O, $P, $Q, $R, $S, $T, $U, $V, $W, $X, $Y, $Z, 12 | $[, $\\, $], $^, $_, $`, 13 | $a, $b, $c,$d, $e, $f, $g, $h, $i, $j, $k, $l, $m, $n, $o, $p, $q, $r, $s, $t, $u, $v, $w, $x, $y, $z, 14 | ${, $|, $}, $~, 15 | ok. 16 | 17 | -spec integers() -> ok. 18 | integers() -> 19 | 42, -1, +1, 20 | -1_234_567_890, 21 | 2#101, 2#1010_1010, 22 | 8#765, 8#765_432, 23 | 10#123, 10#123_456, 24 | 16#1f2E, 16#4865_316F_774F_6C64, 25 | 36#1234567890abcdefghijklmnopqrstuvwxyz, 36#1234567890ABC_DEF_GHIJ_KLMNOP_QR_STUVWXYZ, 26 | ok. 27 | 28 | -spec floats() -> ok. 29 | floats() -> 30 | 2.3, 2.3e3, 2.3e-3, 31 | 1_234.333_333, 32 | ok. 33 | 34 | -spec atoms() -> ok. 35 | atoms() -> 36 | hello, phone_number, 'Monday', 'phone number', bob123@best_place, 37 | %% ASCII characters in single quoted atom. Note: the escaped single quote (\') is not in the right position ... 38 | ' !"#$%&()*+,-./0123456789:;\'<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~', 39 | %% ... and the reason is that if the escaped single quote followed by an opening parenthesis then it's falsely 40 | %% interpreted as a function call 41 | 'atom\'(A)', %' % TODO: escaped single quote and opening parenthesis ("\'(") shall not break atoms 42 | a, 'atom\'(A)', %' % TODO: -"- 43 | ok. 44 | 45 | -spec bitstrings_and_binaries() -> ok. 46 | bitstrings_and_binaries() -> 47 | <<10,20>>, 48 | <<1:1, 0:1>>, 49 | <<$a, $b, $c>>, 50 | <<42/integer, 42:16/integer>>, 51 | <<2.3/float, 2.3e3:32/float>>, 52 | <<"ABC", "ABC"/utf8, "ABC"/utf16, "ABC"/utf32>>, 53 | <<$a/utf8, 98/utf16, $c/utf32, 1024/utf8>>, 54 | <<2.3/unsigned-big-float>>, 55 | <<42:16/signed-little-unit:2-integer>>, 56 | <<1:1/binary>>, 57 | <<2:2/bytes>>, 58 | <<3:3/bitstring>>, 59 | <<4:4/bits>>, 60 | 61 | _Bin1 = <<1,17,42>>, 62 | _Bin2 = <<"abc">>, 63 | _Bin3 = <<1,17,42:16>>, 64 | <<_A,_B,_C:16>> = <<1,17,42:16>>, 65 | <<_D:16,_E,_F>> = <<1,17,42:16>>, 66 | <> = <<1,17,42:16>>, 67 | <> = <<1,17,42:12>>, 68 | ok. 69 | 70 | -spec explicit_function_expressions1() -> ok. 71 | explicit_function_expressions1() -> 72 | fun(0) -> 1; (A) -> A+1 end, 73 | ok. 74 | 75 | -spec explicit_function_expressions2() -> ok. 76 | explicit_function_expressions2() -> 77 | fun (A) when 0/=A -> 1/A; 78 | (_) -> 1 79 | end, 80 | ok. 81 | 82 | -spec explicit_function_expressions3() -> ok. 83 | explicit_function_expressions3() -> 84 | fun 85 | (A) when 0/=A -> 86 | 1/A; 87 | (_) -> 88 | 1 89 | end, 90 | ok. 91 | 92 | -spec explicit_function_expressions4() -> ok. 93 | explicit_function_expressions4() -> 94 | fun Fact(1) -> 1; % TODO: Name 'Fact' breaks syntax highlight. See '-define' below. 95 | Fact(N) -> N * Fact(N-1) 96 | end, 97 | ok. 98 | 99 | -spec explicit_function_expressions5() -> ok. 100 | explicit_function_expressions5() -> 101 | fun 102 | Fact(1) -> % TODO: Name 'Fact' breaks syntax highlight. See '-define' below. 103 | 1; 104 | Fact(N) -> 105 | N * Fact(N-1) 106 | end, 107 | ok. 108 | 109 | -define(PLUS_ONE, plus_one). 110 | -define(ARITY, 1). 111 | -spec implicit_function_expressions() -> ok. 112 | implicit_function_expressions() -> 113 | %% In Name/Arity, Name is an atom and Arity is an integer. 114 | lists:map(fun plus_one/1, [1, 2, 3]), 115 | lists:map(fun plus_one/?ARITY, [1, 2, 3]), 116 | lists:map(fun ?PLUS_ONE/1, [1, 2, 3]), 117 | lists:map(fun ?PLUS_ONE/?ARITY, [1, 2, 3]), 118 | 119 | %% In Module:Name/Arity, Module, and Name are atoms and Arity is an integer. 120 | %% Starting from Erlang/OTP R15, Module, Name, and Arity can also be variables. 121 | Mod = ?MODULE, 122 | Fun = plus_one, 123 | Arity = 1, 124 | lists:map(fun syntax_test_data_types:plus_one/1, [1, 2, 3]), 125 | lists:map(fun syntax_test_data_types:plus_one/Arity, [1, 2, 3]), 126 | lists:map(fun syntax_test_data_types:plus_one/?ARITY, [1, 2, 3]), 127 | lists:map(fun syntax_test_data_types:Fun/1, [1, 2, 3]), 128 | lists:map(fun syntax_test_data_types:Fun/Arity, [1, 2, 3]), 129 | lists:map(fun syntax_test_data_types:Fun/?ARITY, [1, 2, 3]), 130 | lists:map(fun syntax_test_data_types:?PLUS_ONE/1, [1, 2, 3]), 131 | lists:map(fun syntax_test_data_types:?PLUS_ONE/Arity, [1, 2, 3]), 132 | lists:map(fun syntax_test_data_types:?PLUS_ONE/?ARITY, [1, 2, 3]), 133 | 134 | lists:map(fun Mod:plus_one/1, [1, 2, 3]), 135 | lists:map(fun Mod:plus_one/Arity, [1, 2, 3]), 136 | lists:map(fun Mod:plus_one/?ARITY, [1, 2, 3]), 137 | lists:map(fun Mod:Fun/1, [1, 2, 3]), 138 | lists:map(fun Mod:Fun/Arity, [1, 2, 3]), 139 | lists:map(fun Mod:Fun/?ARITY, [1, 2, 3]), 140 | lists:map(fun Mod:?PLUS_ONE/1, [1, 2, 3]), 141 | lists:map(fun Mod:?PLUS_ONE/Arity, [1, 2, 3]), 142 | lists:map(fun Mod:?PLUS_ONE/?ARITY, [1, 2, 3]), 143 | 144 | lists:map(fun ?MODULE:plus_one/1, [1, 2, 3]), % TODO: syntax highlight function name 145 | lists:map(fun ?MODULE:plus_one/Arity, [1, 2, 3]), % TODO: syntax highlight function name 146 | lists:map(fun ?MODULE:plus_one/?ARITY, [1, 2, 3]), % TODO: syntax highlight function name 147 | lists:map(fun ?MODULE:Fun/1, [1, 2, 3]), 148 | lists:map(fun ?MODULE:Fun/Arity, [1, 2, 3]), 149 | lists:map(fun ?MODULE:Fun/?ARITY, [1, 2, 3]), 150 | lists:map(fun ?MODULE:?PLUS_ONE/1, [1, 2, 3]), 151 | lists:map(fun ?MODULE:?PLUS_ONE/Arity, [1, 2, 3]), 152 | lists:map(fun ?MODULE:?PLUS_ONE/?ARITY, [1, 2, 3]), 153 | ok. 154 | 155 | -spec plus_one(N::integer()) -> ok. 156 | plus_one(N) -> 157 | N + 1. 158 | 159 | -spec 'Plus One'(N::integer()) -> ok. 160 | 'Plus One'(N) -> 161 | N + 1. 162 | 163 | -spec 'Plus \'()ne'(N::integer()) -> ok. %' % TODO: escaped single quote and opening parenthesis ("\'(") shall not break atoms 164 | 'Plus \'()ne'(N) -> 165 | N + 1. %'. % TODO: escaped single quote and opening parenthesis ("\'(") shall not break atoms 166 | 167 | -spec tuples() -> ok. 168 | tuples() -> 169 | {adam, 24, {july, 29}}, 170 | ok. 171 | 172 | -spec maps() -> ok. 173 | maps() -> 174 | #{}, 175 | #{name => adam, age => 24, date => {july, 29}}, 176 | ok. 177 | 178 | -spec lists() -> ok. 179 | lists() -> 180 | [a, 2, {c, 4} | []], 181 | "string" "42", 182 | ok. 183 | 184 | -record(rec, 185 | {a = 1 :: integer(), % a 186 | b = 1, % b 187 | c :: integer(), % c 188 | d % d 189 | %% after last field 190 | } % after fields 191 | ). % end of record definition 192 | 193 | -spec records() -> ok. 194 | records() -> 195 | #rec.a, % a 196 | _A = 1, 197 | #rec{}, 198 | _B = 2, 199 | #rec{b = "2", % b 200 | c = 1 % c 201 | }, % b 202 | _C = 2, 203 | #rec{a = 1, _ = '_'}, 204 | _E = 4, 205 | ok. 206 | 207 | -spec booleans() -> ok. 208 | booleans() -> 209 | true or false. 210 | 211 | -spec other_language_constants() -> ok. 212 | other_language_constants() -> 213 | undefined, 214 | ok. 215 | 216 | -spec escape_sequences() -> ok. 217 | escape_sequences() -> 218 | "a\bc\d\e\fghijklm\nopq\r\s\tu\vwxyz", 219 | "\^a\^b\^c\^d\^e\^f\^g\^h\^i\^j\^k\^l\^m\^n\^o\^p\^q\^r\^s\^t\^u\^v\^w\^x\^y\^z", 220 | "\^A\^B\^C\^D\^E\^F\^G\^H\^I\^J\^K\^L\^M\^N\^O\^P\^Q\^R\^S\^T\^U\^V\^W\^X\^Y\^Z", 221 | "a\"b\'c\\d", 222 | "n\157p", 223 | "j\x6bl\x6Dn", 224 | 225 | 'a\bc\d\e\fghijklm\nopq\r\s\tu\vwxyz', 226 | '\^a\^b\^c\^d\^e\^f\^g\^h\^i\^j\^k\^l\^m\^n\^o\^p\^q\^r\^s\^t\^u\^v\^w\^x\^y\^z', 227 | '\^A\^B\^C\^D\^E\^F\^G\^H\^I\^J\^K\^L\^M\^N\^O\^P\^Q\^R\^S\^T\^U\^V\^W\^X\^Y\^Z', 228 | 'a\"b\'c\\d', 229 | 'n\157p', 230 | 'j\x6bl\x6Dn', 231 | 232 | $ , 233 | $\b, $\d, $\e, $\f, $\n, $\r, $\s, $\t, $\v, 234 | $\^a, $\^b, $\^c, $\^d, $\^e, $\^f, $\^g, $\^h, $\^i, $\^j, $\^k, $\^l, $\^m, 235 | $\^n, $\^o, $\^p, $\^q, $\^r, $\^s, $\^t, $\^u, $\^v, $\^w, $\^x, $\^y, $\^z, 236 | $\^A, $\^B, $\^C, $\^D, $\^E, $\^F, $\^G, $\^H, $\^I, $\^J, $\^K, $\^L, $\^M, 237 | $\^N, $\^O, $\^P, $\^Q, $\^R, $\^S, $\^T, $\^U, $\^V, $\^W, $\^X, $\^Y, $\^Z, 238 | $\", $\', 239 | $\157, 240 | $\x6b, $\x6D, 241 | ok. 242 | 243 | -define(THE_END, ok). 244 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/gen_lsp_server.erl: -------------------------------------------------------------------------------- 1 | -module(gen_lsp_server). 2 | -behavior(gen_server). 3 | 4 | %inspired from https://github.com/kevinlynx/erlang-tcpserver/blob/master/test/test.erl 5 | %http://20bits.com/article/erlang-a-generalized-tcp-server 6 | 7 | % à regarder 8 | % http://learnyousomeerlang.com/buckets-of-sockets 9 | 10 | 11 | %% API 12 | -export([start_link/1, start_link/2]). 13 | -export([lsp_log/2, send_to_client/2]). 14 | 15 | %% gen_server callbacks 16 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 17 | 18 | -define(SERVER, ?MODULE). 19 | %state 20 | -record(state, {socket, content_length, contents}). 21 | 22 | % because stacktrace is deprecated from OTP 21 23 | -ifdef(OTP_RELEASE). 24 | safeApply(Function, Socket, ArgsMap) -> 25 | try apply(lsp_handlers, Function, [Socket, ArgsMap]) of 26 | {error, ErrorResult } -> {error, ErrorResult }; 27 | Result -> {ok, Result} 28 | catch 29 | throw:Reason when is_binary(Reason) -> 30 | lsp_log("LSP handler returned '~p'", [Reason]), 31 | {handler_error, Reason}; 32 | throw:Reason when is_list(Reason) -> 33 | lsp_log("LSP handler returned '~p'", [Reason]), 34 | {handler_error, list_to_binary(Reason)}; 35 | Error:Exception:StackTrace -> 36 | error_logger:error_msg("LSP handler error ~p:~p while executing lsp_handlers:~p(_, ~p), stacktrace:~p", 37 | [Error, Exception,Function,ArgsMap, StackTrace]), 38 | {handler_error, <<"Handler error">>} 39 | end. 40 | -else. 41 | safeApply(Function, Socket, ArgsMap) -> 42 | try apply(lsp_handlers, Function, [Socket, ArgsMap]) of 43 | {error, ErrorResult } -> {error, ErrorResult }; 44 | Result -> {ok, Result} 45 | catch 46 | throw:Reason when is_binary(Reason) -> 47 | lsp_log("LSP handler returned '~p'", [Reason]), 48 | {handler_error, Reason}; 49 | throw:Reason when is_list(Reason) -> 50 | lsp_log("LSP handler returned '~p'", [Reason]), 51 | {handler_error, list_to_binary(Reason)}; 52 | Error:Exception -> 53 | error_logger:error_msg("LSP handler error ~p:~p while executing lsp_handlers:~p(_, ~p), stacktrace:~p", 54 | [Error, Exception,Function,ArgsMap, erlang:get_stacktrace()]), 55 | {handler_error, <<"Handler error">>} 56 | end. 57 | -endif. 58 | 59 | 60 | start_link(VsCodePort) -> 61 | start_link(VsCodePort, undefined). 62 | 63 | start_link(VsCodePort, Socket) -> 64 | gen_server:start_link({local, ?SERVER}, ?MODULE, [VsCodePort, Socket, self()],[]). 65 | 66 | init([_VsCodePort, Socket, _Parent]) -> 67 | {ok, #state{socket = Socket, contents = <<"">>}, 0}. 68 | 69 | handle_call(_Request, _From, State) -> 70 | {reply, ok, State}. 71 | 72 | handle_cast(_Request, State) -> 73 | {stop, normal, State}. 74 | 75 | handle_info({tcp, Socket, Contents}, State) -> 76 | inet:setopts(Socket, [{active, once}]), 77 | {noreply, handle_tcp_data(Socket, Contents, State)}; 78 | handle_info(timeout, #state{socket = Socket} = State) -> 79 | {ok, _} = gen_tcp:accept(Socket),  80 | {noreply, State}; 81 | handle_info({tcp_closed, _Socket}, State) -> 82 | {stop, normal, State}; 83 | handle_info(_Data, State) -> 84 | {noreply, State}. 85 | 86 | lsp_log(Msg, Args) -> 87 | gen_lsp_config_server:verbose() andalso error_logger:info_msg(Msg, Args). 88 | 89 | lsp_log(Method, Msg, Args) -> 90 | % Method can be excluded from verbose logging by adding it to verboseExcludeFilter in the config 91 | gen_lsp_config_server:verbose() andalso gen_lsp_config_server:verbose_is_include(Method) andalso error_logger:info_msg(Msg, Args). 92 | 93 | remove_text_for_logging(#{params := #{contentChanges := ChangesList} = Params} = Input) -> 94 | Input#{params := Params#{contentChanges := lists:map(fun 95 | (#{text := <>} = Change) when byte_size(Text) > 20 -> 96 | Cut = binary:part(Text, 0, 20), 97 | Change#{text := <>}; 98 | (Change) -> 99 | Change 100 | end, ChangesList)}}; 101 | remove_text_for_logging(#{params := #{textDocument := #{text := Text} = TextDocument} = Params} = Input) when byte_size(Text) > 20 -> 102 | Cut = binary:part(Text, 0, 20), 103 | Input#{params := Params#{textDocument := TextDocument#{text := <>}}}; 104 | remove_text_for_logging(Input) -> 105 | Input. 106 | 107 | do_contents(Socket, #{method := Method} = Input) -> 108 | lsp_log(Method, "LSP received ~p", [remove_text_for_logging(Input)]), 109 | case call_handler(Socket, Method, maps:get(params, Input, undefined)) of 110 | {ok, Result} -> 111 | send_response_with_id(Socket, Input, #{result => Result}); 112 | {error, Result} -> 113 | send_response_with_id(Socket, Input, #{error => Result}); 114 | handler_not_found -> 115 | error_logger:error_msg("Method not handled: ~p", [Method]), 116 | send_response_with_id(Socket, Input, #{error => #{code => -32001, message => <<"Method not handled">>}}); 117 | {handler_error, Message} -> 118 | send_response_with_id(Socket, Input, #{error => #{code => -32001, message => Message}}) 119 | end; 120 | do_contents(Socket, #{id := Id} = Input) -> 121 | lsp_log("LSP received ~p", [Input]), 122 | case call_handler(Socket, Id, maps:get(result, Input, undefined)) of 123 | {ok, _Result} -> 124 | ok; 125 | {error, _Result} -> 126 | ok; 127 | handler_not_found -> 128 | error_logger:error_msg("Notification not handled: ~p ~p", [Id, Input]); 129 | {handler_error, Message} -> 130 | Message 131 | end. 132 | 133 | call_handler(Socket, Name, ArgsMap) -> 134 | case lists:keyfind(handler_name(Name), 1, lsp_handlers:module_info(exports)) of 135 | false -> 136 | handler_not_found; 137 | {Function, 2} -> 138 | %uncomment to show commands sent by vscode 139 | %lsp_log("LSP call_handler lsp_handlers:'~p':~p", [Function, ArgsMap]), 140 | safeApply(Function, Socket, ArgsMap) 141 | end. 142 | 143 | handler_name(<<"$/", Name/binary>>) -> 144 | list_to_atom(binary_to_list(Name)); 145 | handler_name(Name) -> 146 | list_to_atom(binary_to_list(binary:replace(Name, <<"/">>, <<"_">>))). 147 | 148 | send_response_with_id(Socket, #{method := Method} = Input, Response) -> 149 | case maps:get(id, Input, undefined) of 150 | undefined -> 151 | ok; 152 | Id -> 153 | send_to_client(Socket, Method, Response#{id => Id}) 154 | end. 155 | 156 | send_to_client(Socket, Method, Body) -> 157 | lsp_log(Method, "LSP sends ~p", [Body]), 158 | {ok, Json} = vscode_jsone:encode(Body), 159 | Header = iolist_to_binary(io_lib:fwrite("Content-Length: ~p", [byte_size(Json)])), 160 | gen_tcp:send(Socket, <
>). 161 | 162 | send_to_client(Socket, Body) -> 163 | send_to_client(Socket, <<"unknown">>, Body). 164 | 165 | 166 | handle_tcp_data(Socket, Contents, State) -> 167 | StateWithContents = State#state{contents = <<(State#state.contents)/binary, Contents/binary>>}, 168 | StateWithLength = case StateWithContents#state.content_length of 169 | undefined -> 170 | HeadersEnd = binary:match(StateWithContents#state.contents, <<"\r\n\r\n">>), 171 | case HeadersEnd of 172 | nomatch -> 173 | StateWithContents; 174 | {HeadersSeparatorStart, HeadersSeparatorLen} -> 175 | {match, [_, {LengthStart, LengthLen}]} = 176 | re:run(StateWithContents#state.contents, "Content-Length: *([0-9]+)"), 177 | Length = binary_to_integer(binary:part(StateWithContents#state.contents, LengthStart, LengthLen)), 178 | BodyStart = HeadersSeparatorStart + HeadersSeparatorLen, 179 | BodyLen = byte_size(StateWithContents#state.contents) - BodyStart, 180 | StateWithContents#state{ 181 | contents = binary:part(StateWithContents#state.contents, BodyStart, BodyLen), 182 | content_length = Length 183 | } 184 | end; 185 | _ -> 186 | StateWithContents 187 | end, 188 | case StateWithLength#state.content_length of 189 | undefined -> 190 | StateWithLength; 191 | ContentLength when ContentLength > byte_size(StateWithLength#state.contents) -> 192 | StateWithLength; 193 | ContentLength when ContentLength =:= byte_size(StateWithLength#state.contents) -> 194 | {ok, Input, _} = vscode_jsone_decode:decode(StateWithLength#state.contents, [{keys, atom}]), 195 | spawn(fun() -> do_contents(Socket, Input) end), 196 | StateWithLength#state{contents = <<"">>, content_length = undefined}; 197 | ContentLength when ContentLength < byte_size(StateWithLength#state.contents) -> 198 | ShorterContents = binary:part(StateWithLength#state.contents, 0, ContentLength), 199 | {ok, Input, _} = vscode_jsone_decode:decode(ShorterContents, [{keys, atom}]), 200 | spawn(fun() -> do_contents(Socket, Input) end), 201 | handle_tcp_data( 202 | Socket, 203 | binary:part(StateWithLength#state.contents, ContentLength, byte_size(StateWithLength#state.contents) - ContentLength), 204 | StateWithLength#state{contents = <<"">>, content_length = undefined}) 205 | end. 206 | 207 | terminate(_Reason, _State) -> 208 | ok. 209 | 210 | code_change(_OldVersion, State, _Extra) -> 211 | {ok, State}. 212 | -------------------------------------------------------------------------------- /lib/lsp/lspclientextension.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as os from 'os'; 3 | import { 4 | workspace as Workspace, window as Window, ExtensionContext, TextDocument, OutputChannel, 5 | Uri, Disposable, CodeLens, FileSystemWatcher, workspace, languages 6 | } from 'vscode'; 7 | 8 | import { 9 | ConfigurationParams, 10 | CancellationToken, DidChangeConfigurationNotification, Middleware, 11 | DidChangeWatchedFilesNotification, FileChangeType 12 | } from 'vscode-languageclient'; 13 | 14 | import { 15 | LanguageClient, 16 | LanguageClientOptions, 17 | ServerOptions, 18 | TransportKind, 19 | StreamInfo 20 | } from 'vscode-languageclient/node'; 21 | 22 | import { ErlangShellLSP } from './ErlangShellLSP'; 23 | import { erlangBridgePath } from '../erlangConnection'; 24 | import * as Net from 'net'; 25 | 26 | import * as lspcodelens from './lspcodelens'; 27 | 28 | import * as lspValue from './lsp-inlinevalues'; 29 | import * as lspRename from './lsp-rename'; 30 | 31 | 32 | // import { ErlangShellForDebugging } from '../ErlangShellDebugger'; 33 | 34 | // import * as erlConnection from '../erlangConnection'; 35 | 36 | // import { ErlangSettings } from '../erlangSettings'; 37 | import RebarShell from '../RebarShell'; 38 | import { ErlangOutputAdapter } from '../vscodeAdapter'; 39 | import { getElangConfigConfiguration, resolveErlangSettings } from '../ErlangConfigurationProvider'; 40 | import { ErlangLanguageClient, erlangDocumentSelector } from './lsp-context'; 41 | 42 | /* 43 | other LSP 44 | https://github.com/rust-lang-nursery/rls-vscode/blob/master/src/extension.ts 45 | https://github.com/tintoy/msbuild-project-tools-vscode/blob/master/src/extension/extension.ts 46 | https://microsoft.github.io/language-server-protocol/implementors/servers/ 47 | https://microsoft.github.io/language-server-protocol/specification 48 | https://github.com/mtsmfm/language_server-ruby/blob/master/lib/language_server.rb 49 | 50 | 51 | exemple TS <-> TS <--> C# 52 | https://tomassetti.me/language-server-dot-visual-studio/ 53 | 54 | */ 55 | 56 | export let client: LanguageClient; 57 | let clients: Map = new Map(); 58 | let lspOutputChannel: OutputChannel; 59 | 60 | namespace Configuration { 61 | 62 | let configurationListener: Disposable; 63 | let fileSystemWatcher: FileSystemWatcher; 64 | 65 | // Convert VS Code specific settings to a format acceptable by the server. Since 66 | // both client and server do use JSON the conversion is trivial. 67 | export function computeConfiguration(params: ConfigurationParams, _token: CancellationToken, _next: Function): any[] { 68 | 69 | if (!params.items) { 70 | return null; 71 | } 72 | let result: any[] = []; 73 | for (let item of params.items) { 74 | if (item.section) { 75 | if (item.section === "") { 76 | result.push({ 77 | autosave: Workspace.getConfiguration("files").get("autoSave", "afterDelay") === "afterDelay", 78 | tmpdir: os.tmpdir(), 79 | username: os.userInfo().username 80 | }); 81 | } else if (item.section === "erlang") { 82 | result.push(resolveErlangSettings(Workspace.getConfiguration(item.section))) 83 | } 84 | else { 85 | result.push(Workspace.getConfiguration(item.section)); 86 | } 87 | } 88 | else { 89 | result.push(null); 90 | } 91 | } 92 | return result; 93 | } 94 | 95 | export function initialize() { 96 | //force to read configuration 97 | lspcodelens.configurationChanged(); 98 | // VS Code currently doesn't sent fine grained configuration changes. So we 99 | // listen to any change. However this will change in the near future. 100 | configurationListener = Workspace.onDidChangeConfiguration(() => { 101 | lspcodelens.configurationChanged(); 102 | client.sendNotification(DidChangeConfigurationNotification.type, { settings: null }); 103 | }); 104 | fileSystemWatcher = workspace.createFileSystemWatcher('**/*.erl'); 105 | fileSystemWatcher.onDidCreate(uri => { 106 | client.sendNotification(DidChangeWatchedFilesNotification.type, 107 | { changes: [{ uri: uri.fsPath, type: FileChangeType.Created }] }); 108 | }); 109 | fileSystemWatcher.onDidDelete(uri => { 110 | client.sendNotification(DidChangeWatchedFilesNotification.type, 111 | { changes: [{ uri: uri.fsPath, type: FileChangeType.Deleted }] }); 112 | }); 113 | } 114 | 115 | export function dispose() { 116 | if (configurationListener) { 117 | configurationListener.dispose(); 118 | } 119 | } 120 | } 121 | 122 | 123 | 124 | var MAX_TRIES = 10; 125 | var WAIT_BETWEEN_TRIES_MS = 250; 126 | 127 | /** 128 | * Tries to connect to a given socket location. 129 | * Time between retires grows in relation to attempts (attempt * RETRY_TIMER). 130 | * 131 | * waitForSocket({ port: 2828, maxTries: 10 }, function(err, socket) { 132 | * }); 133 | * 134 | * Note- there is a third argument used to recursion that should 135 | * never be used publicly. 136 | * 137 | * Options: 138 | * - (Number) port: to connect to. 139 | * - (String) host: to connect to. 140 | * - (Number) tries: number of times to attempt the connect. 141 | * 142 | * @param {Object} options for connection. 143 | * @param {Function} callback [err, socket]. 144 | */ 145 | function waitForSocket(options: any, callback: any, _tries: any) { 146 | if (!options.port) 147 | throw new Error('.port is a required option'); 148 | 149 | var maxTries = options.tries || MAX_TRIES; 150 | var host = options.host || 'localhost'; 151 | var port = options.port; 152 | 153 | 154 | _tries = _tries || 0; 155 | if (_tries >= maxTries) 156 | return callback(new Error('cannot open socket')); 157 | 158 | function handleError() { 159 | // retry connection 160 | setTimeout( 161 | waitForSocket, 162 | // wait at least WAIT_BETWEEN_TRIES_MS or a multiplier 163 | // of the attempts. 164 | (WAIT_BETWEEN_TRIES_MS * _tries) || WAIT_BETWEEN_TRIES_MS, 165 | options, 166 | callback, 167 | ++_tries 168 | ); 169 | } 170 | 171 | var socket = Net.connect(port, host, () => { 172 | socket.removeListener('error', handleError); 173 | callback(null, socket); 174 | }); 175 | socket.once('error', handleError); 176 | } 177 | 178 | /** 179 | * Uses the extension-provided rebar3 executable to compile the erlangbridge app. 180 | * 181 | * @param extensionPath - Path to the editor extension. 182 | * @returns Promise resolved or rejected when compilation is complete. 183 | */ 184 | // TODO: convert to async function 185 | function compileErlangBridge(extensionPath: string): Thenable { 186 | return new RebarShell([getElangConfigConfiguration().rebarPath], extensionPath, ErlangOutputAdapter()) 187 | .compile(extensionPath) 188 | .then(({ output }) => output); 189 | // TODO: handle failure to compile erlangbridge 190 | } 191 | 192 | function getPort(callback) { 193 | var server = Net.createServer(function (sock) { 194 | sock.end('OK\n'); 195 | }); 196 | server.listen(0, function () { 197 | var port = (server.address()).port; 198 | server.close(function () { 199 | callback(port); 200 | }); 201 | }); 202 | } 203 | 204 | export function activate(context: ExtensionContext) { 205 | let erlangCfg = getElangConfigConfiguration(); 206 | if (erlangCfg.verbose) 207 | lspOutputChannel = Window.createOutputChannel('Erlang Language Server', 'erlang'); 208 | 209 | lspValue.activate(context, lspOutputChannel); 210 | lspRename.activate(context, lspOutputChannel); 211 | 212 | let middleware: Middleware = { 213 | workspace: { 214 | configuration: Configuration.computeConfiguration 215 | }, 216 | provideCodeLenses: (document, token) => { 217 | return Promise.resolve(lspcodelens.onProvideCodeLenses(document, token)).then(x => x); 218 | }, 219 | resolveCodeLens: (codeLens) => { 220 | return Promise.resolve(lspcodelens.onResolveCodeLenses(codeLens)).then(x => x); 221 | }, 222 | didSave: async (data, next) => { 223 | await next(data);//call LSP 224 | lspcodelens.onDocumentDidSave(); 225 | } 226 | }; 227 | // Options to control the language client 228 | let clientOptions: LanguageClientOptions = { 229 | // Register the server for plain text documents 230 | documentSelector: [{ scheme: 'file', language: 'erlang' }], 231 | synchronize: { 232 | // Notify the server about file changes to '.clientrc files contain in the workspace 233 | fileEvents: Workspace.createFileSystemWatcher('**/.clientrc'), 234 | // In the past this told the client to actively synchronize settings. Since the 235 | // client now supports 'getConfiguration' requests this active synchronization is not 236 | // necessary anymore. 237 | // configurationSection: [ 'lspMultiRootSample' ] 238 | }, 239 | middleware: middleware, 240 | diagnosticCollectionName: 'Erlang Language Server', 241 | outputChannel: lspOutputChannel 242 | } 243 | 244 | let clientName = erlangCfg.verbose ? 'Erlang Language Server' : ''; 245 | client = new ErlangLanguageClient(clientName, async () => { 246 | return new Promise(async (resolve, reject) => { 247 | await compileErlangBridge(context.extensionPath); 248 | let erlangLsp = new ErlangShellLSP(ErlangOutputAdapter(lspOutputChannel)); 249 | 250 | getPort(async function (port) { 251 | erlangLsp.Start("", erlangBridgePath, port, "src", ""); 252 | let socket = await waitForSocket({ port: port }, 253 | function (error, socket) { 254 | resolve({ reader: socket, writer: socket }); 255 | }, 256 | undefined); 257 | // 258 | (client).onReady(); 259 | }); 260 | }); 261 | }, clientOptions, lspOutputChannel, true); 262 | Configuration.initialize(); 263 | // Start the client. This will also launch the server 264 | client.start(); 265 | } 266 | 267 | export function debugLog(msg: string): void { 268 | if (lspOutputChannel) { 269 | lspOutputChannel.appendLine(msg); 270 | } 271 | } 272 | 273 | export function deactivate(): Thenable { 274 | if (!client) { 275 | return undefined; 276 | } 277 | Configuration.dispose(); 278 | return client.stop(); 279 | } 280 | -------------------------------------------------------------------------------- /lib/RebarRunner.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path' 3 | import * as fs from 'fs' 4 | import * as child_process from 'child_process' 5 | import RebarShell from './RebarShell'; 6 | import * as utils from './utils' 7 | import { ErlangOutputAdapter } from './vscodeAdapter'; 8 | import { getElangConfigConfiguration } from './ErlangConfigurationProvider'; 9 | 10 | var rebarOutputChannel: vscode.OutputChannel; 11 | 12 | /* 13 | Rebar Compile 14 | see : https://github.com/hoovercj/vscode-extension-tutorial 15 | 16 | */ 17 | 18 | export class RebarRunner implements vscode.Disposable { 19 | 20 | private extensionPath: string; 21 | diagnosticCollection: vscode.DiagnosticCollection; 22 | private static commandId: string = 'extension.rebarBuild'; 23 | private compileCommand: vscode.Disposable; 24 | private getDepsCommand: vscode.Disposable; 25 | private updateDepsCommand: vscode.Disposable; 26 | private eunitCommand: vscode.Disposable; 27 | private dialyzerCommand: vscode.Disposable; 28 | 29 | public activate(context: vscode.ExtensionContext) { 30 | const subscriptions = context.subscriptions; 31 | this.extensionPath = context.extensionPath; 32 | 33 | this.compileCommand = vscode.commands.registerCommand('extension.rebarBuild', () => { this.runRebarCompile(); }); 34 | this.getDepsCommand = vscode.commands.registerCommand('extension.rebarGetDeps', () => { this.runRebarCommand(['get-deps']); }); 35 | this.updateDepsCommand = vscode.commands.registerCommand('extension.rebarUpdateDeps', () => { this.runRebarCommand(['update-deps']); }); 36 | this.eunitCommand = vscode.commands.registerCommand('extension.rebareunit', () => { this.runRebarCommand(['eunit']) }); 37 | this.dialyzerCommand = vscode.commands.registerCommand('extension.dialyzer', () => { this.runDialyzer() }); 38 | vscode.workspace.onDidCloseTextDocument(this.onCloseDocument.bind(this), null, subscriptions); 39 | vscode.workspace.onDidOpenTextDocument(this.onOpenDocument.bind(this), null, subscriptions); 40 | this.diagnosticCollection = vscode.languages.createDiagnosticCollection("erlang"); 41 | subscriptions.push(this); 42 | } 43 | 44 | 45 | public dispose(): void { 46 | this.diagnosticCollection.dispose(); 47 | this.compileCommand.dispose(); 48 | this.getDepsCommand.dispose(); 49 | this.updateDepsCommand.dispose(); 50 | this.eunitCommand.dispose(); 51 | this.dialyzerCommand.dispose(); 52 | } 53 | 54 | private runRebarCompile() { 55 | const statusBarMessage = vscode.window.setStatusBarMessage('$(loading~spin) Building'); 56 | try { 57 | const buildArgs = getElangConfigConfiguration().rebarBuildArgs; 58 | this.runScript(buildArgs).then(data => { 59 | //RebarRunner.RebarOutput.appendLine(`buildArgs: ${buildArgs}`); 60 | this.diagnosticCollection.clear(); 61 | this.parseCompilationResults(data); 62 | statusBarMessage.dispose(); 63 | }); 64 | } catch (e) { 65 | statusBarMessage.dispose(); 66 | vscode.window.showErrorMessage('Couldn\'t execute rebar.\n' + e); 67 | } 68 | } 69 | 70 | private parseForDiag(data: string, diagnostics: { [id: string]: vscode.Diagnostic[]; }, regex: RegExp, severity: vscode.DiagnosticSeverity): string { 71 | //parse data while regex return matches 72 | do { 73 | var m = regex.exec(data); 74 | if (m) { 75 | var fileName = m[1]; 76 | var line = Number(m[2]); 77 | var column = 0; 78 | var splittedFileName = fileName.split(':'); 79 | if (splittedFileName.length == 2) 80 | { 81 | //filename contains ':number' 82 | column = line; 83 | fileName = splittedFileName[0]; 84 | line = Number(splittedFileName[1]); 85 | } 86 | var peace = data.substring(m.index, regex.lastIndex); 87 | data = data.replace(peace, ""); 88 | 89 | let message = m[m.length - 1]; 90 | 91 | let range = new vscode.Range(line - 1, 0, line- 1, peace.length - 1); 92 | let diagnostic = new vscode.Diagnostic(range, message, severity); 93 | regex.lastIndex = 0; 94 | if (!diagnostics[fileName]) { 95 | diagnostics[fileName] = []; 96 | } 97 | diagnostics[fileName].push(diagnostic); 98 | } 99 | } 100 | while (m != null); 101 | return data; 102 | } 103 | 104 | private parseCompilationResults(data: string): void { 105 | //how to test regexp : https://regex101.com/#javascript 106 | var diagnostics: { [id: string]: vscode.Diagnostic[]; } = {}; 107 | //parsing warning at first 108 | var warnings = new RegExp("^(.*):(\\d+):(.*)Warning:(.*)$", "gmi"); 109 | data = this.parseForDiag(data, diagnostics, warnings, vscode.DiagnosticSeverity.Warning); 110 | //then parse errors (because regex to detect errors include warnings too) 111 | var errors = new RegExp("^(.*):(\\d+):(.*)$", "gmi"); 112 | data = this.parseForDiag(data, diagnostics, errors, vscode.DiagnosticSeverity.Error); 113 | var keys = utils.keysFromDictionary(diagnostics); 114 | const rootPath = getElangConfigConfiguration().rootPath; 115 | keys.forEach(element => { 116 | var fileUri = vscode.Uri.file(path.join(rootPath, element)); 117 | var diags = diagnostics[element]; 118 | this.diagnosticCollection.set(fileUri, diags); 119 | }); 120 | } 121 | 122 | private runRebarCommand(command: string[]): void { 123 | try { 124 | this.runScript(command).then(data => { 125 | }, reject => {}); 126 | } catch (e) { 127 | vscode.window.showErrorMessage('Couldn\'t execute rebar.\n' + e); 128 | } 129 | } 130 | 131 | private lineAndMessage(input: string): [number, number, string] | null { 132 | var lineAndMessage1 = new RegExp("^ *Line +([0-9]+) +Column +([0-9]+): +(.+)$"); 133 | var match1 = lineAndMessage1.exec(input); 134 | if (match1) { 135 | return [Number(match1[1]), Number(match1[2]), match1[3]]; 136 | } 137 | else { 138 | var lineAndMessage2 = new RegExp("^ +([0-9]+): *(.+)$"); 139 | var match2 = lineAndMessage2.exec(input); 140 | if (match2) { 141 | return [Number(match2[1]), 1, match2[2]]; 142 | } 143 | } 144 | return null; 145 | } 146 | 147 | private runDialyzer(): void { 148 | try { 149 | const statusBarMessage = vscode.window.setStatusBarMessage('$(loading~spin) Running Dialyzer'); 150 | this.runScript(["dialyzer"]).then(data => { 151 | this.diagnosticCollection.clear(); 152 | var lines = data.split("\n"); 153 | var currentFile = null; 154 | var diagnostics: { [id: string]: vscode.Diagnostic[]; } = {}; 155 | const rootPath = getElangConfigConfiguration().rootPath; 156 | for (var i = 0; i < lines.length; ++i) { 157 | if (lines[i]) { 158 | var match = this.lineAndMessage(lines[i]); 159 | if (match && currentFile) { 160 | if (!diagnostics[currentFile]) 161 | diagnostics[currentFile] = []; 162 | var range = new vscode.Range(match[0] - 1, match[1] - 1, match[0] - 1, 255); 163 | diagnostics[currentFile].push(new vscode.Diagnostic(range , match[2], vscode.DiagnosticSeverity.Information)); 164 | } 165 | else { 166 | var filepath = path.join(rootPath, lines[i]); 167 | if (fs.existsSync(filepath)) 168 | currentFile = filepath; 169 | } 170 | } 171 | else 172 | currentFile = null; 173 | } 174 | utils.keysFromDictionary(diagnostics).forEach(filepath => { 175 | var fileUri = vscode.Uri.file(filepath); 176 | var diags = diagnostics[filepath]; 177 | this.diagnosticCollection.set(fileUri, diags); 178 | }); 179 | if (utils.keysFromDictionary(diagnostics).length > 0) 180 | vscode.commands.executeCommand("workbench.action.problems.focus"); 181 | else 182 | vscode.window.showInformationMessage('Dialyzer did not find any problems'); 183 | statusBarMessage.dispose(); 184 | }, reject => {}); 185 | } catch (e) { 186 | vscode.window.showErrorMessage('Couldn\'t execute Dialyzer.\n' + e); 187 | } 188 | } 189 | 190 | /** 191 | * Get search paths for the rebar executable in order of priority. 192 | * 193 | * @returns Directories to search for the rebar executable 194 | */ 195 | private getRebarSearchPaths(): string[] { 196 | 197 | const cfgRebarPath = getElangConfigConfiguration().rebarPath, 198 | rebarSearchPaths = [], 199 | rootPath = getElangConfigConfiguration().rootPath; 200 | if (cfgRebarPath) { 201 | rebarSearchPaths.push(cfgRebarPath); 202 | } 203 | if (cfgRebarPath !== rootPath) { 204 | rebarSearchPaths.push(rootPath); 205 | } 206 | return rebarSearchPaths; 207 | } 208 | 209 | /** 210 | * Execute rebar on the workspace project with supplied arguments. 211 | * 212 | * @param commands - Arguments to rebar 213 | * @returns Promise resolved or rejected when rebar exits 214 | */ 215 | public async runScript(commands: string[]): Promise { 216 | const rootPath = getElangConfigConfiguration().rootPath; 217 | const { output } = await new RebarShell(this.getRebarSearchPaths(), this.extensionPath, ErlangOutputAdapter(RebarRunner.RebarOutput)) 218 | .runScript(rootPath, commands); 219 | return output; 220 | } 221 | 222 | private onCloseDocument(doc: vscode.TextDocument): any { 223 | //RebarRunner.RebarOutput.appendLine("doc close : " + doc.uri.toString()); 224 | if (this.diagnosticCollection) { 225 | this.diagnosticCollection.delete(doc.uri); 226 | } 227 | } 228 | 229 | private onOpenDocument(doc: vscode.TextDocument): any { 230 | //RebarRunner.RebarOutput.appendLine("doc open : " + doc.uri.toString()); 231 | } 232 | 233 | public static get RebarOutput(): vscode.OutputChannel { 234 | if (!rebarOutputChannel) { 235 | rebarOutputChannel = vscode.window.createOutputChannel('rebar', 'erlang'); 236 | } 237 | return rebarOutputChannel; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "erlang", 3 | "description": "Erlang language extension for Visual Studio Code", 4 | "version": "1.1.3", 5 | "icon": "images/icon.png", 6 | "publisher": "pgourlain", 7 | "engines": { 8 | "vscode": "^1.52.0" 9 | }, 10 | "categories": [ 11 | "Programming Languages", 12 | "Snippets", 13 | "Debuggers" 14 | ], 15 | "activationEvents": [ 16 | "onLanguage:erlang", 17 | "onDebugInitialConfigurations", 18 | "onDebugResolve:erlang", 19 | "onCommand:extension.rebarBuild", 20 | "onCommand:extension.rebarGetDeps", 21 | "onCommand:extension.rebarUpdateDeps", 22 | "onCommand:extension.erleunit", 23 | "onCommand:extension.dialyzer" 24 | ], 25 | "main": "./out/lib/extension.js", 26 | "contributes": { 27 | "breakpoints": [ 28 | { 29 | "language": "erlang" 30 | } 31 | ], 32 | "debuggers": [ 33 | { 34 | "type": "erlang", 35 | "program": "./out/lib/erlangDebug.js", 36 | "runtime": "node", 37 | "label": "Erlang Debug", 38 | "languages": [ 39 | "erlang" 40 | ], 41 | "configurationAttributes": { 42 | "launch": { 43 | "required": [], 44 | "properties": { 45 | "arguments": { 46 | "type": "string", 47 | "description": "Arguments to append erl command line." 48 | }, 49 | "cwd": { 50 | "type": "string", 51 | "description": "Path of project", 52 | "default": "${workspaceRoot}" 53 | }, 54 | "erlpath": { 55 | "type": "string", 56 | "description": "Path to the erl executable or the command if in PATH", 57 | "default": "erl" 58 | }, 59 | "addEbinsToCodepath": { 60 | "type": "boolean", 61 | "description": "Add ebin directories in _build to code path", 62 | "default": true 63 | } 64 | } 65 | } 66 | }, 67 | "initialConfigurations": [ 68 | { 69 | "name": "Launch erlang", 70 | "type": "erlang", 71 | "request": "launch", 72 | "cwd": "${workspaceRoot}" 73 | } 74 | ] 75 | } 76 | ], 77 | "languages": [ 78 | { 79 | "id": "erlang", 80 | "aliases": [ 81 | "Erlang", 82 | "erlang" 83 | ], 84 | "extensions": [ 85 | ".erl", 86 | ".hrl", 87 | ".xrl", 88 | ".yrl", 89 | ".es", 90 | ".escript", 91 | ".app.src", 92 | "rebar.config" 93 | ], 94 | "configuration": "./erlang.configuration.json", 95 | "icon": { 96 | "dark": "images/erlang-fileicon-dark.png", 97 | "light": "images/erlang-fileicon-light.png" 98 | } 99 | } 100 | ], 101 | "grammars": [ 102 | { 103 | "language": "erlang", 104 | "scopeName": "source.erlang", 105 | "path": "./grammar/Erlang.plist" 106 | } 107 | ], 108 | "commands": [ 109 | { 110 | "command": "extension.rebarBuild", 111 | "title": "Erlang: rebar compile" 112 | }, 113 | { 114 | "command": "extension.rebarGetDeps", 115 | "title": "Erlang: rebar get-deps" 116 | }, 117 | { 118 | "command": "extension.rebarUpdateDeps", 119 | "title": "Erlang: rebar update-deps" 120 | }, 121 | { 122 | "command": "extension.rebareunit", 123 | "title": "Erlang: rebar eunit" 124 | }, 125 | { 126 | "command": "extension.erleunit", 127 | "title": "Erlang: run eunit tests with erlang shell" 128 | }, 129 | { 130 | "command": "extension.dialyzer", 131 | "title": "Erlang: rebar dialyzer" 132 | } 133 | ], 134 | "keybindings": [ 135 | { 136 | "command": "extension.rebarBuild", 137 | "mac": "shift+cmd+b", 138 | "key": "ctrl+shift+b", 139 | "when": "editorLangId == 'erlang'" 140 | }, 141 | { 142 | "command": "extension.erleunit", 143 | "mac": "shift+cmd+t", 144 | "key": "ctrl+shift+t", 145 | "when": "editorLangId == 'erlang'" 146 | } 147 | ], 148 | "menus": { 149 | "explorer/context": [ 150 | { 151 | "when": "resourceLangId == erlang", 152 | "command": "extension.erleunit", 153 | "group": "compile" 154 | } 155 | ] 156 | }, 157 | "snippets": [ 158 | { 159 | "language": "erlang", 160 | "path": "./snippets/erlang.json" 161 | } 162 | ], 163 | "configuration": { 164 | "type": "object", 165 | "title": "Erlang", 166 | "properties": { 167 | "erlang.erlangPath": { 168 | "type": "string", 169 | "default": "", 170 | "description": "Directory where erl/escript are located. Leave empty to use default." 171 | }, 172 | "erlang.erlangArgs": { 173 | "type": "array", 174 | "items": { 175 | "type": "string", 176 | "title": "argument", 177 | "default": "" 178 | }, 179 | "default": [], 180 | "description": "Arguments passed to Erlang backend. Leave empty unless you really have to tweak the Erlang VM." 181 | }, 182 | "erlang.erlangDistributedNode": { 183 | "type": "boolean", 184 | "description": "Start the Erlang backend in a distributed Erlang node. Could be useful for extension development. Note, it starts EPMD if not running yet.", 185 | "default": false 186 | }, 187 | "erlang.rebarPath": { 188 | "type": "string", 189 | "default": "", 190 | "description": "Directory where rebar/rebar3 are located. Leave empty to use default." 191 | }, 192 | "erlang.rebarBuildArgs": { 193 | "type": "array", 194 | "items": { 195 | "type": "string", 196 | "title": "argument", 197 | "default": "" 198 | }, 199 | "default": [ 200 | "compile" 201 | ], 202 | "description": "Arguments passed to rebar/rebar3 build command." 203 | }, 204 | "erlang.includePaths": { 205 | "type": "array", 206 | "items": { 207 | "type": "string", 208 | "title": "path", 209 | "default": "" 210 | }, 211 | "default": [], 212 | "description": "Include paths used when extension analyses the sources. Paths are read from rebar.config, and also standard set of paths is used. This setting is for special cases when the default behaviour is not enough." 213 | }, 214 | "erlang.linting": { 215 | "type": "boolean", 216 | "default": true, 217 | "description": "Enable/disable dynamic validation of opened Erlang source files." 218 | }, 219 | "erlang.cacheManagement": { 220 | "type": "string", 221 | "default": "memory", 222 | "description": "Specify where and how to store large cache tables.", 223 | "enum": [ 224 | "memory", 225 | "compressed memory", 226 | "file" 227 | ], 228 | "enumDescriptions": [ 229 | "Store in memory", 230 | "Store in memory and apply lightweight compression to consume less memory (approx. 50%)", 231 | "Store in temporary files" 232 | ] 233 | }, 234 | "erlang.codeLensEnabled": { 235 | "type": "boolean", 236 | "default": false, 237 | "description": "Enable/disable references CodeLens on functions." 238 | }, 239 | "erlang.inlayHintsEnabled": { 240 | "type": "boolean", 241 | "default": false, 242 | "description": "Enable/disable references InlayHints on functions." 243 | }, 244 | "erlang.verbose": { 245 | "type": "boolean", 246 | "description": "Enable/disable technical traces for use in the extension development.", 247 | "default": false 248 | }, 249 | "erlang.verboseExcludeFilter": { 250 | "type": "string", 251 | "default": "textDocument/inlayHints,textDocument/hover", 252 | "description": "List of excluded methods (i.e: textDocument/hover,textDocument/inlayHints, ...) from technical traces in the extension development." 253 | }, 254 | "erlang.debuggerRunMode": { 255 | "type": "string", 256 | "default": "external", 257 | "description": "Specifies how to run vscode debugadapter. Useful in extension development.", 258 | "enum": [ 259 | "external", 260 | "server", 261 | "inline" 262 | ], 263 | "enumDescriptions": [ 264 | "external : launch debug adapter in separate process", 265 | "server: launch debugadapter as a socket based server", 266 | "inline: launch debugadapter in current process" 267 | ] 268 | }, 269 | "erlang.formattingLineLength": { 270 | "type": "number", 271 | "default": 100, 272 | "description": "Maximum line length for document formatting." 273 | } 274 | } 275 | }, 276 | "problemMatchers": [ 277 | { 278 | "name": "erlang", 279 | "owner": "erlang", 280 | "fileLocation": [ 281 | "relative", 282 | "${workspaceRoot}" 283 | ], 284 | "pattern": { 285 | "regexp": "^(.*):(\\d+):\\s+((Warning): )?(.*)$", 286 | "file": 1, 287 | "line": 2, 288 | "severity": 4, 289 | "message": 5 290 | } 291 | } 292 | ] 293 | }, 294 | "repository": { 295 | "type": "git", 296 | "url": "https://github.com/pgourlain/vscode_erlang.git" 297 | }, 298 | "license": "MIT", 299 | "scripts": { 300 | "vscode:prepublish": "webpack --mode production", 301 | "webpack": "webpack --mode development", 302 | "webpack-dev": "webpack --mode development --watch", 303 | "pretest": "npm run compile", 304 | "test": "vscode-test", 305 | "deploy": "vsce publish --no-yarn", 306 | "compile": "tsc -p ./" 307 | }, 308 | "dependencies": { 309 | "fs-extra": "^8.1.0", 310 | "@vscode/debugadapter": "^1.68.0", 311 | "@vscode/debugprotocol": "^1.68.0", 312 | "vscode-languageclient": "^9.0.1", 313 | "vscode-languageserver": "^9.0.1", 314 | "vscode-uri": "^1.0.3" 315 | }, 316 | "devDependencies": { 317 | "@types/fs-extra": "^8.0.0", 318 | "@types/glob": "^7.1.1", 319 | "@types/mocha": "^5.2.6", 320 | "@types/node": "^16.11.7", 321 | "@types/vscode": "^1.52.0", 322 | "@typescript-eslint/eslint-plugin": "^5.42.0", 323 | "@typescript-eslint/parser": "^5.42.0", 324 | "@vscode/test-cli": "^0.0.9", 325 | "@vscode/test-electron": "^2.3.9", 326 | "@vscode/vsce": "^2.26.1", 327 | "eslint": "^8.26.0", 328 | "glob": "^7.1.4", 329 | "gulp": "^5.0.0", 330 | "mocha": "^10.2.0", 331 | "source-map-support": "^0.5.12", 332 | "ts-loader": "^8.1.0", 333 | "typescript": "^5.0.2", 334 | "vscode-test": "^1.3.0", 335 | "webpack": "^5.75.0", 336 | "webpack-cli": "^5.0.1" 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /apps/erlangbridge/src/lsp_parse.erl: -------------------------------------------------------------------------------- 1 | -module(lsp_parse). 2 | -export([parse_source_file/2, parse_config_file/2, get_include_path/1, get_include_path_no_build/1, scan_source_file/2]). 3 | 4 | %% @doc 5 | %% @param File is a reference to the real file (file opened by editor) 6 | %% @param ContentsFile is used for parsing (can be a temp file) 7 | scan_source_file(File, ContentsFile) -> 8 | case epp_scan_file(ContentsFile, get_include_path(File), get_define_from_rebar_config(File)) of 9 | {ok, FileSyntaxTree} -> 10 | UpdatedSyntaxTree = update_file_in_forms(File, ContentsFile, FileSyntaxTree), 11 | {UpdatedSyntaxTree, undefined}; 12 | _ -> 13 | io:format("epp:scan_file could not parse ~p~n", [File]), 14 | {undefined, undefined} 15 | end. 16 | 17 | parse_source_file(File, ContentsFile) -> 18 | case epp_parse_file(ContentsFile, get_include_path(File), get_define_from_rebar_config(File)) of 19 | {ok, FileSyntaxTree} -> 20 | UpdatedSyntaxTree = update_file_in_forms(File, ContentsFile, FileSyntaxTree), 21 | case epp_dodger_parse_file(ContentsFile) of 22 | {ok, Forms} -> 23 | {UpdatedSyntaxTree, Forms}; 24 | _ -> 25 | io:format("epp_dodger:parse_file could not parse ~p~n", [File]), 26 | {UpdatedSyntaxTree, undefined} 27 | end; 28 | _ -> 29 | io:format("epp:parse_file could not parse ~p~n", [File]), 30 | {undefined, undefined} 31 | end. 32 | 33 | %% in order to have LMine and column information in syntax tree, we need to parse file with epp_dodger 34 | epp_dodger_parse_file(File) -> 35 | case file:open(File, [read]) of 36 | {ok, Handle} -> 37 | R = epp_dodger:parse(Handle, {1,1}), 38 | file:close(Handle), 39 | R; 40 | Error -> Error 41 | end. 42 | 43 | parse_config_file(File, ContentsFile) -> 44 | case file:path_consult(filename:dirname(ContentsFile), ContentsFile) of 45 | {ok,_, _} -> #{parse_result => true}; 46 | {error, Reason} -> #{ 47 | parse_result => true, 48 | errors_warnings => [#{type => <<"error">>, 49 | file => list_to_binary(File), 50 | info => extract_info(Reason)}] } 51 | end. 52 | 53 | get_include_path(File) -> 54 | Candidates = get_file_include_paths(File) ++ 55 | get_settings_include_paths() ++ 56 | get_include_paths_from_rebar_config(File) ++ 57 | get_standard_include_paths(), 58 | Paths = lists:filter(fun filelib:is_dir/1, Candidates), 59 | % activate it only for debugging, on big projects it can generate a lot of logs 60 | %gen_lsp_server:lsp_log("get_include_path: ~p", [Paths]), 61 | Paths. 62 | 63 | get_standard_include_paths() -> 64 | RootDir = gen_lsp_config_server:root(), 65 | [ 66 | filename:join([RootDir, "apps"]), 67 | filename:join([RootDir, "lib"]), 68 | filename:join([RootDir, "_build", "default", "lib"]), 69 | filename:join([RootDir, "_build", "default", "plugins"]) 70 | ]. 71 | 72 | get_include_path_no_build(File) -> 73 | RootDir = gen_lsp_config_server:root(), 74 | StandardIncludePathsNoBuild = 75 | [ 76 | filename:join([RootDir, "apps"]), 77 | filename:join([RootDir, "lib"]) 78 | ], 79 | Candidates = get_file_include_paths(File) ++ 80 | get_settings_include_paths() ++ 81 | get_include_paths_from_rebar_config(File) ++ 82 | StandardIncludePathsNoBuild, 83 | Paths = lists:filter(fun filelib:is_dir/1, Candidates), 84 | % activate it only for debugging, on big projects it can generate a lot of logs 85 | %gen_lsp_server:lsp_log("get_include_path: ~p", [Paths]), 86 | Paths. 87 | 88 | get_settings_include_paths() -> 89 | SettingPaths = gen_lsp_config_server:includePaths(), 90 | RootDir = gen_lsp_config_server:root(), 91 | lists:map(fun (Path) -> 92 | lsp_utils:absolute_path(RootDir, Path) 93 | end, SettingPaths). 94 | 95 | get_file_include_paths(File) -> 96 | Paths = [filename:dirname(File), filename:rootname(File)], 97 | case get_file_include_directory(File) of 98 | undefined -> 99 | Paths; 100 | Path -> 101 | [Path|Paths] 102 | end. 103 | 104 | get_file_include_directory(File) -> 105 | case lists:reverse(filename:split(filename:dirname(File))) of 106 | [DirName|Rest] when DirName =:= "src" orelse DirName =:= "test" -> 107 | filename:join(lists:reverse(["include"|Rest])); 108 | [_, DirName|Rest] when DirName =:= "src" orelse DirName =:= "test" -> 109 | filename:join(lists:reverse(["include"|Rest])); 110 | _Other -> 111 | undefined 112 | end. 113 | 114 | get_include_paths_from_rebar_config(File) -> 115 | RebarConfig = find_rebar_config(filename:dirname(File)), 116 | case RebarConfig of 117 | undefined -> 118 | []; 119 | _ -> 120 | Consult = file:consult(RebarConfig), 121 | ErlOptsPaths = case Consult of 122 | {ok, Terms} -> 123 | ErlOpts = proplists:get_value(erl_opts, Terms, []), 124 | IncludePaths = proplists:get_all_values(i, ErlOpts), 125 | lists:map(fun (Path) -> 126 | filename:absname(Path, filename:dirname(RebarConfig)) 127 | end, IncludePaths); 128 | _ -> 129 | [] 130 | end, 131 | Deps = [], 132 | %TODO: if include of each rebar dependency should be add to include paths, uncomment below 133 | % Deps = case Consult of 134 | % {ok, DepsTerms} -> 135 | % Profiles = proplists:get_value(profiles, DepsTerms, []), 136 | % ErlDeps = lists:flatmap(fun({_,Opts})-> proplists:get_value(deps,Opts) end, Profiles), 137 | % RootDir = gen_lsp_config_server:root(), 138 | % [filename:join([RootDir, "_build", "default", "plugins", X, "include"]) || X <- ErlDeps ]; 139 | % _ -> 140 | % [] 141 | % end, 142 | DefaultPaths = [filename:dirname(RebarConfig), filename:join([filename:dirname(RebarConfig), "include"])], 143 | ErlOptsPaths ++ Deps ++ DefaultPaths 144 | end. 145 | 146 | extract_info({Line, Module, MessageBody}) when is_number(Line) -> 147 | extract_info({{Line, 1}, Module, MessageBody}); 148 | extract_info({{Line, Column}, Module, MessageBody}) -> 149 | % samples of X 150 | %{20,erl_parse,["syntax error before: ","load_xy"]} 151 | %{11,erl_lint,{undefined_function,{load_xy,1}}}]} 152 | #{ 153 | line => Line, 154 | character => Column, 155 | message => erlang:list_to_binary(lists:flatten(apply(Module, format_error, [MessageBody]), [])) 156 | }. 157 | 158 | get_define_from_rebar_config(File) -> 159 | RebarConfig = find_rebar_config(filename:dirname(File)), 160 | case RebarConfig of 161 | undefined -> 162 | []; 163 | _ -> 164 | Consult = file:consult(RebarConfig), 165 | ErlOptsDefines = case Consult of 166 | {ok, Terms} -> 167 | ErlOpts = proplists:get_value(erl_opts, Terms, []), 168 | Defines = rebar_define_to_epp_define(proplists:lookup_all(d, ErlOpts)), 169 | gen_lsp_server:lsp_log("get_defines: ~p", [Defines]), 170 | Defines; 171 | _ -> 172 | [] 173 | end, 174 | DefaultDefines = [], 175 | ErlOptsDefines ++ DefaultDefines 176 | end. 177 | 178 | find_rebar_config(Dir) -> 179 | RebarConfig = filename:join(Dir, "rebar.config"), 180 | case filelib:is_file(RebarConfig) of 181 | true -> 182 | RebarConfig; 183 | _ -> 184 | Elements = filename:split(Dir), 185 | case Elements of 186 | [_] -> 187 | undefined; 188 | _ -> 189 | find_rebar_config(filename:join(lists:droplast(Elements))) 190 | end 191 | end. 192 | 193 | 194 | epp_scan_file(File, IncludePath, Defines) -> 195 | case otp_24_or_newer() of 196 | true -> 197 | case epp:scan_file(as_string(File), [{includes, IncludePath}, {macros, Defines}, {location, {1, 1}}]) of 198 | {ok, Result, _Extra} -> 199 | {ok, Result}; 200 | _Err -> 201 | gen_lsp_server:lsp_log("epp_parse_file error : ~p", [_Err]), 202 | {error, file_could_not_scanned} 203 | end; 204 | false -> 205 | {error, scan_notimplemented_before_otp24} 206 | end. 207 | 208 | epp_parse_file(File, IncludePath, Defines) -> 209 | case otp_24_or_newer() of 210 | true -> 211 | case epp:parse_file(as_string(File), [{includes, IncludePath}, {macros, Defines}, {location, {1, 1}}]) of 212 | {ok, Result} -> 213 | {ok, Result}; 214 | _Err -> 215 | gen_lsp_server:lsp_log("epp_parse_file error : ~p", [_Err]), 216 | {error, file_could_not_parsed} 217 | end; 218 | false -> 219 | case file:open(File, [read]) of 220 | {ok, FIO} -> 221 | Ret = do_epp_parse_file(File, FIO, IncludePath, Defines), 222 | file:close(FIO), 223 | Ret; 224 | _Err -> 225 | gen_lsp_server:lsp_log("epp_parse_file error : ~p", [_Err]), 226 | {error, file_could_not_opened} 227 | end 228 | end. 229 | 230 | do_epp_parse_file(File, FIO, IncludePath, Defines) -> 231 | case epp:open(as_string(File), FIO, {1,1}, IncludePath, Defines) of 232 | {ok, Epp} -> {ok, epp:parse_file(Epp)}; 233 | {error, _Err} -> {error, _Err} 234 | end. 235 | 236 | as_string(Text) when is_binary(Text) -> 237 | binary_to_list(Text); 238 | as_string(Text) -> 239 | Text. 240 | 241 | rebar_define_to_epp_define([]) -> 242 | []; 243 | rebar_define_to_epp_define([none]) -> 244 | []; 245 | rebar_define_to_epp_define([H|T]) -> 246 | case H of 247 | {d, Atom, Value} -> [{Atom, Value}] ++ rebar_define_to_epp_define(T); 248 | {d, Atom} -> [Atom] ++ rebar_define_to_epp_define(T); 249 | _ -> rebar_define_to_epp_define(T) 250 | end. 251 | 252 | update_file_in_forms(File, File, FileSyntaxTree) -> 253 | FileSyntaxTree; 254 | update_file_in_forms(File, ContentsFile, FileSyntaxTree) -> 255 | lists:map(fun 256 | ({attribute, A1, file, {FunContentsFile, A2}}) when FunContentsFile =:= ContentsFile -> 257 | {attribute, A1, file, {File, A2}}; 258 | (Form) -> 259 | Form 260 | end, FileSyntaxTree). 261 | 262 | otp_24_or_newer() -> 263 | VersionNumber = (catch list_to_integer(erlang:system_info(otp_release))), 264 | is_integer(VersionNumber) andalso VersionNumber >= 24. 265 | --------------------------------------------------------------------------------