From c1bdb26a564f5b417ed59011257d60863aaeaa46 Mon Sep 17 00:00:00 2001 From: Wojtek Surowka Date: Sat, 20 Jun 2026 14:24:19 +0100 Subject: [PATCH 1/8] Fix for "Launching debugger failed" error --- lib/erlangConnection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/erlangConnection.ts b/lib/erlangConnection.ts index f9dcc13..e641f4a 100644 --- a/lib/erlangConnection.ts +++ b/lib/erlangConnection.ts @@ -91,7 +91,7 @@ export abstract class ErlangConnection extends EventEmitter { //create dir if not exists //compile erlang_connection in specifc diretory to avoid that the target can access to lspxxx.beam at debug time if (!fs.existsSync(ebinDir)) { - fs.mkdirSync(ebinDir); + fs.mkdirSync(ebinDir, {recursive: true}); } let args = ["-o", path.normalize(ebinDir)].concat(erlFiles); From f84d0c4cc5aa5dfb48d514880c10440013f27212 Mon Sep 17 00:00:00 2001 From: Wojtek Surowka Date: Sat, 20 Jun 2026 15:33:22 +0100 Subject: [PATCH 2/8] Silence Erlang warnings --- apps/erlangbridge/src/lsp_parse.erl | 1 + .../src/lsp_signature_doc_layout.erl | 1 + apps/erlangbridge/src/lsp_syntax.erl | 166 +++++++++--------- apps/erlangbridge/src/vscode_erlfmt_parse.yrl | 1 + 4 files changed, 86 insertions(+), 83 deletions(-) diff --git a/apps/erlangbridge/src/lsp_parse.erl b/apps/erlangbridge/src/lsp_parse.erl index 82f2c98..cf97207 100644 --- a/apps/erlangbridge/src/lsp_parse.erl +++ b/apps/erlangbridge/src/lsp_parse.erl @@ -1,5 +1,6 @@ -module(lsp_parse). -export([parse_source_file/2, parse_config_file/2, get_include_path/1, get_include_path_no_build/1, scan_source_file/2]). +-compile(nowarn_deprecated_catch). %% @doc %% @param File is a reference to the real file (file opened by editor) diff --git a/apps/erlangbridge/src/lsp_signature_doc_layout.erl b/apps/erlangbridge/src/lsp_signature_doc_layout.erl index 6717c3c..39f475b 100644 --- a/apps/erlangbridge/src/lsp_signature_doc_layout.erl +++ b/apps/erlangbridge/src/lsp_signature_doc_layout.erl @@ -1,6 +1,7 @@ -module(lsp_signature_doc_layout). -export([module/2]). +-compile({nowarn_unused_function, {signatures_sample, 0}}). -include_lib("xmerl/include/xmerl.hrl"). diff --git a/apps/erlangbridge/src/lsp_syntax.erl b/apps/erlangbridge/src/lsp_syntax.erl index d2e93a3..5cb4d4d 100644 --- a/apps/erlangbridge/src/lsp_syntax.erl +++ b/apps/erlangbridge/src/lsp_syntax.erl @@ -183,91 +183,91 @@ lint(FileSyntaxTree, File) -> % max_lc(_, Acc) -> % Acc. -check_if_remote_fun_exists(RootWorkspace, FnModule, FnName, FnArity, {Line, Column, LE,LC}) -> - % check if module is under workspace - case gen_lsp_doc_server:get_module_file(FnModule) of - undefined -> []; - TargetFile -> - case string:str(TargetFile, RootWorkspace) of - 1 -> - case available_functions(TargetFile, FnName, FnArity) of - {[],[],[]} -> % no match => function doesn't exists - [ - #{info => - #{line => Line, - message => lsp_utils:to_binary("function ~p:~p/~p undefined", [FnModule,FnName,FnArity]), - character => Column, - line_end => LE, - character_end => LC}, - type => <<"error">>, - file => lsp_utils:to_binary(TargetFile), - source => ?LINTER, - correlation_data => #{ action => <<"create_function">>, arguments => [FnModule, FnName, FnArity]} - } - ]; - - {MatchFns, _, _} when length(MatchFns) > 0 -> % function is found, so no error - []; - {_, DefMatchFns, _} when length(DefMatchFns) > 0 -> % function is found, but not exported - [ - #{info => - #{line => Line, - message => - lsp_utils:to_binary(lsp_utils:to_string("function ~s:~s/~p is missing in export.",[FnModule, FnName, FnArity])), - character => Column, - line_end => LE, - character_end => LC}, - type => <<"error">>, - file => lsp_utils:to_binary(TargetFile), - source => ?LINTER, - correlation_data => #{ action => <<"export_function">>, arguments => [FnModule, FnName, FnArity]} - } - ]; - {[], [], NotMatchFns} when length(NotMatchFns) > 0 -> - %funtions with other arity - AvailableFns = lists:flatten(lists:join(",", - lists:filtermap(fun - ({exported, Fn, Fa}) -> {true, lsp_utils:to_string("~s/~p\n",[Fn, Fa])}; - (_) -> false - end, NotMatchFns))), - Message = lsp_utils:to_binary("function ~s:~s/~p arity mismatch.\navailable arity are :\n ~s\n", - [FnModule, FnName, FnArity, AvailableFns]), - [ - #{info => - #{line => Line, - message => Message, - character => Column, - line_end => LE, - character_end => LC}, - type => <<"error">>, - file => lsp_utils:to_binary(TargetFile), - source => ?LINTER - }]; - - _ -> [] - end; - _ -> [] - end - end. +%check_if_remote_fun_exists(RootWorkspace, FnModule, FnName, FnArity, {Line, Column, LE,LC}) -> +% % check if module is under workspace +% case gen_lsp_doc_server:get_module_file(FnModule) of +% undefined -> []; +% TargetFile -> +% case string:str(TargetFile, RootWorkspace) of +% 1 -> +% case available_functions(TargetFile, FnName, FnArity) of +% {[],[],[]} -> % no match => function doesn't exists +% [ +% #{info => +% #{line => Line, +% message => lsp_utils:to_binary("function ~p:~p/~p undefined", [FnModule,FnName,FnArity]), +% character => Column, +% line_end => LE, +% character_end => LC}, +% type => <<"error">>, +% file => lsp_utils:to_binary(TargetFile), +% source => ?LINTER, +% correlation_data => #{ action => <<"create_function">>, arguments => [FnModule, FnName, FnArity]} +% } +% ]; +% +% {MatchFns, _, _} when length(MatchFns) > 0 -> % function is found, so no error +% []; +% {_, DefMatchFns, _} when length(DefMatchFns) > 0 -> % function is found, but not exported +% [ +% #{info => +% #{line => Line, +% message => +% lsp_utils:to_binary(lsp_utils:to_string("function ~s:~s/~p is missing in export.",[FnModule, FnName, FnArity])), +% character => Column, +% line_end => LE, +% character_end => LC}, +% type => <<"error">>, +% file => lsp_utils:to_binary(TargetFile), +% source => ?LINTER, +% correlation_data => #{ action => <<"export_function">>, arguments => [FnModule, FnName, FnArity]} +% } +% ]; +% {[], [], NotMatchFns} when length(NotMatchFns) > 0 -> +% %funtions with other arity +% AvailableFns = lists:flatten(lists:join(",", +% lists:filtermap(fun +% ({exported, Fn, Fa}) -> {true, lsp_utils:to_string("~s/~p\n",[Fn, Fa])}; +% (_) -> false +% end, NotMatchFns))), +% Message = lsp_utils:to_binary("function ~s:~s/~p arity mismatch.\navailable arity are :\n ~s\n", +% [FnModule, FnName, FnArity, AvailableFns]), +% [ +% #{info => +% #{line => Line, +% message => Message, +% character => Column, +% line_end => LE, +% character_end => LC}, +% type => <<"error">>, +% file => lsp_utils:to_binary(TargetFile), +% source => ?LINTER +% }]; +% +% _ -> [] +% end; +% _ -> [] +% end +% end. % get functions that match and nearly match (by arity) -available_functions(File, FunName, FunArity) -> - Functions =fold_in_syntax_tree(fun - ({function, {_, _}, FName, FnArity, _}, _CurrentFile, {M, DM, NM}) when FName =:= FunName, FnArity =:= FunArity -> - {M, [{definition_match, FName, FunArity} | DM], NM}; - ({function, {_, _}, FName, Arity, _}, _CurrentFile, {M, DM, NM}) when FName =:= FunName -> - {M, DM, [{definition, FName, Arity} | NM]}; - ({attribute, {_L, _C}, export, Exports}, _CurrentFile, {M, DM, NM}=Acc) -> - case lists:keyfind(FunName, 1, Exports) of - {_, Arity} when Arity =:= FunArity -> {[{exported_match, FunName, FunArity} | M], DM, NM}; - {_, Arity} -> {M, DM, [{exported, FunName, Arity} | NM]}; - _ -> Acc - end; - (_SyntaxTree, _CurrentFile, Acc) -> - Acc - end, - {[], [], []}, File), - Functions. +%available_functions(File, FunName, FunArity) -> +% Functions =fold_in_syntax_tree(fun +% ({function, {_, _}, FName, FnArity, _}, _CurrentFile, {M, DM, NM}) when FName =:= FunName, FnArity =:= FunArity -> +% {M, [{definition_match, FName, FunArity} | DM], NM}; +% ({function, {_, _}, FName, Arity, _}, _CurrentFile, {M, DM, NM}) when FName =:= FunName -> +% {M, DM, [{definition, FName, Arity} | NM]}; +% ({attribute, {_L, _C}, export, Exports}, _CurrentFile, {M, DM, NM}=Acc) -> +% case lists:keyfind(FunName, 1, Exports) of +% {_, Arity} when Arity =:= FunArity -> {[{exported_match, FunName, FunArity} | M], DM, NM}; +% {_, Arity} -> {M, DM, [{exported, FunName, Arity} | NM]}; +% _ -> Acc +% end; +% (_SyntaxTree, _CurrentFile, Acc) -> +% Acc +% end, +% {[], [], []}, File), +% Functions. filter_unused_functions({_, []}) -> []; diff --git a/apps/erlangbridge/src/vscode_erlfmt_parse.yrl b/apps/erlangbridge/src/vscode_erlfmt_parse.yrl index e3e752f..89304ee 100644 --- a/apps/erlangbridge/src/vscode_erlfmt_parse.yrl +++ b/apps/erlangbridge/src/vscode_erlfmt_parse.yrl @@ -648,6 +648,7 @@ Erlang code. form_info/0, error_info/0 ]). +-compile(nowarn_update_literal). %% Start of Abstract Format From 5e2da13500da3f54b34ed6a1433955cc09a401ee Mon Sep 17 00:00:00 2001 From: Wojtek Surowka Date: Sat, 20 Jun 2026 15:41:57 +0100 Subject: [PATCH 3/8] Try to find rebar3 on PATH before using the included one --- lib/RebarShell.ts | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/RebarShell.ts b/lib/RebarShell.ts index 07f5e77..20ba47a 100644 --- a/lib/RebarShell.ts +++ b/lib/RebarShell.ts @@ -1,4 +1,5 @@ import * as fs from 'fs'; +import * as fsPromises from 'node:fs/promises'; import * as path from 'path'; import { GenericShell, ILogOutput, IShellOutput } from './GenericShell'; import { getElangConfigConfiguration } from './ErlangConfigurationProvider'; @@ -35,7 +36,7 @@ export default class RebarShell extends GenericShell { // Rebar may not have execution permission (e.g. if extension is built // on Windows but installed on Linux). Let's always run rebar by escript. let escript = (process.platform == 'win32' ? 'escript.exe' : 'escript'); - let rebarFileName = this.getRebarFullPath(); + let rebarFileName = await this.getRebarFullPath(); if (rebarFileName.search(' ') > -1) { // There is at least one space in rebarPath. Use double quotes // instead of single quotes for cross-operability between @@ -60,12 +61,17 @@ export default class RebarShell extends GenericShell { * * @returns Full path to rebar executable */ - private getRebarFullPath(): string { + private async getRebarFullPath(): Promise { const rebarSearchPaths = this.rebarSearchPaths.slice(); - if (!rebarSearchPaths.includes(this.defaultRebarSearchPath)) { - rebarSearchPaths.push(this.defaultRebarSearchPath); + const onSearchPaths = this.findBestFile(rebarSearchPaths, ['rebar3', 'rebar'], ''); + if (onSearchPaths !== '') { + return onSearchPaths; } - return this.findBestFile(rebarSearchPaths, ['rebar3', 'rebar'], 'rebar3'); + const onPATH = await this.findExecutable('rebar3'); + if (onPATH) { + return onPATH; + } + return this.findBestFile([this.defaultRebarSearchPath], ['rebar3', 'rebar'], 'rebar3'); } /** @@ -92,6 +98,25 @@ export default class RebarShell extends GenericShell { } return result; } + + private async findExecutable(name: string): Promise { + const envPath = process.env.PATH ?? ''; + const dirs = envPath.split(path.delimiter); + const exts = process.platform === 'win32' + ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';') + : ['']; + + for (const dir of dirs) { + for (const ext of exts) { + const full = path.join(dir, name + ext); + try { + await fsPromises.access(full, fsPromises.constants.X_OK); + return full; + } catch { /* keep looking */ } + } + } + return null; + } } export interface RebarShellResult { From 8c8c61cd8637ceecbcd0a33339673fdc74a9cfce Mon Sep 17 00:00:00 2001 From: Wojtek Surowka Date: Sat, 20 Jun 2026 16:03:51 +0100 Subject: [PATCH 4/8] Use erlc on Erlang path if set --- lib/ErlangShellDebugger.ts | 5 +++-- lib/erlangConnection.ts | 8 +++++--- lib/erlangDebugSession.ts | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/ErlangShellDebugger.ts b/lib/ErlangShellDebugger.ts index 792435a..e004018 100644 --- a/lib/ErlangShellDebugger.ts +++ b/lib/ErlangShellDebugger.ts @@ -247,11 +247,12 @@ export class ErlangShellForDebugging extends GenericShell { } /** compile specific files */ - public Compile(startDir: string, args: string[]): Promise { + public Compile(startDir: string, args: string[], erlPath: string): Promise { //if erl is used, -compile must be used //var processArgs = ["-compile"].concat(args); var processArgs = [].concat(args); - var result = this.RunProcess("erlc", startDir, processArgs); + const erlc = erlPath ? path.join(erlPath, 'erlc') : 'erlc'; + var result = this.RunProcess(erlc, startDir, processArgs); return result; } diff --git a/lib/erlangConnection.ts b/lib/erlangConnection.ts index e641f4a..ca76523 100644 --- a/lib/erlangConnection.ts +++ b/lib/erlangConnection.ts @@ -25,7 +25,7 @@ export abstract class ErlangConnection extends EventEmitter { protected events_receiver: http.Server; _output: ILogOutput; verbose: boolean; - + erlPath: string; public get isConnected(): boolean { return this.erlangbridgePort > 0; @@ -36,6 +36,7 @@ export abstract class ErlangConnection extends EventEmitter { this._output = output; this.erlangbridgePort = -1; this.verbose = true; + this.erlPath = ''; } protected log(msg: string): void { @@ -62,8 +63,9 @@ export abstract class ErlangConnection extends EventEmitter { } } - public async Start(verbose: boolean): Promise { + public async Start(verbose: boolean, erlPath: string): Promise { this.verbose = verbose; + this.erlPath = erlPath; return new Promise((a, r) => { //this.debug("erlangConnection.Start"); this.compile_erlang_connection().then(() => { @@ -95,7 +97,7 @@ export abstract class ErlangConnection extends EventEmitter { } let args = ["-o", path.normalize(ebinDir)].concat(erlFiles); - return compiler.Compile(path.join(erlangBridgePath,'src'), args).then(res => { + return compiler.Compile(path.join(erlangBridgePath,'src'), args, this.erlPath).then(res => { //this.debug("Compilation of erlang bridge...ok"); a(res); }, exitCode => { diff --git a/lib/erlangDebugSession.ts b/lib/erlangDebugSession.ts index 90358c1..6f8c1a6 100644 --- a/lib/erlangDebugSession.ts +++ b/lib/erlangDebugSession.ts @@ -150,7 +150,7 @@ export class ErlangDebugSession extends DebugSession implements ILogOutput { this.log(`debugger launchRequest arguments : ${JSON.stringify(args)}`); } // Based on JS output path, not TS path - this.erlangConnection.Start(this._LaunchArguments.verbose).then(port => { + this.erlangConnection.Start(this._LaunchArguments.verbose, this._LaunchArguments.erlangPath).then(port => { //this.debug("Local webserver for erlang is started"); this._port = port; //Initialize the workflow only when webserver is started From e4e1e01892d5b5dd8e16a5baf89e94de145f25c4 Mon Sep 17 00:00:00 2001 From: Wojtek Surowka Date: Sun, 21 Jun 2026 01:31:41 +0100 Subject: [PATCH 5/8] Use escript on Erlang path if set --- lib/RebarRunner.ts | 2 +- lib/RebarShell.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/RebarRunner.ts b/lib/RebarRunner.ts index 00af572..2178ccf 100644 --- a/lib/RebarRunner.ts +++ b/lib/RebarRunner.ts @@ -215,7 +215,7 @@ export class RebarRunner implements vscode.Disposable { public async runScript(commands: string[]): Promise { const rootPath = getElangConfigConfiguration().rootPath; const { output } = await new RebarShell(this.getRebarSearchPaths(), this.extensionPath, ErlangOutputAdapter(RebarRunner.RebarOutput)) - .runScript(rootPath, commands); + .runScript(rootPath, commands, getElangConfigConfiguration().erlangPath); return output; } diff --git a/lib/RebarShell.ts b/lib/RebarShell.ts index 20ba47a..a9a0866 100644 --- a/lib/RebarShell.ts +++ b/lib/RebarShell.ts @@ -32,10 +32,13 @@ export default class RebarShell extends GenericShell { * @param commands - Arguments to rebar * @returns Promise resolved or rejected when rebar exits */ - public async runScript(cwd: string, commands: string[]): Promise { + public async runScript(cwd: string, commands: string[], erlPath: string): Promise { // Rebar may not have execution permission (e.g. if extension is built // on Windows but installed on Linux). Let's always run rebar by escript. let escript = (process.platform == 'win32' ? 'escript.exe' : 'escript'); + if (erlPath !== '') { + escript = path.join(erlPath, escript); + } let rebarFileName = await this.getRebarFullPath(); if (rebarFileName.search(' ') > -1) { // There is at least one space in rebarPath. Use double quotes From 604702f3cebf5a3d3b6e4ad7e1b9b01c7398a61b Mon Sep 17 00:00:00 2001 From: Wojtek Surowka Date: Sun, 21 Jun 2026 01:35:30 +0100 Subject: [PATCH 6/8] Use escript on Erlang path if set --- lib/RebarShell.ts | 4 ++-- lib/lsp/lspclientextension.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/RebarShell.ts b/lib/RebarShell.ts index a9a0866..89574dd 100644 --- a/lib/RebarShell.ts +++ b/lib/RebarShell.ts @@ -21,8 +21,8 @@ export default class RebarShell extends GenericShell { * @param cwd - The working directory where compilation will take place * @returns Promise resolved or rejected when rebar exits */ - public compile(cwd: string) : Promise { - return this.runScript(cwd, ['compile']); + public compile(cwd: string, erlPath: string) : Promise { + return this.runScript(cwd, ['compile'], erlPath); } /** diff --git a/lib/lsp/lspclientextension.ts b/lib/lsp/lspclientextension.ts index b196a1f..47845ef 100644 --- a/lib/lsp/lspclientextension.ts +++ b/lib/lsp/lspclientextension.ts @@ -184,7 +184,7 @@ function waitForSocket(options: any, callback: any, _tries: any) { // TODO: convert to async function function compileErlangBridge(extensionPath: string): Thenable { return new RebarShell([getElangConfigConfiguration().rebarPath], extensionPath, ErlangOutputAdapter()) - .compile(extensionPath) + .compile(extensionPath, getElangConfigConfiguration().erlangPath) .then(({ output }) => output); // TODO: handle failure to compile erlangbridge } From 70ae9899a21f7c6f576f2de03eef52c0a6c87890 Mon Sep 17 00:00:00 2001 From: Wojtek Surowka Date: Mon, 22 Jun 2026 01:09:51 +0100 Subject: [PATCH 7/8] Submodule needed for verify step --- .github/workflows/pr-verify.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml index 68c9311..edcb26d 100644 --- a/.github/workflows/pr-verify.yml +++ b/.github/workflows/pr-verify.yml @@ -35,6 +35,8 @@ jobs: if: runner.os =='macOS' - name: Checkout Source uses: actions/checkout@v4 + with: + submodules: true - name: Install Node ${{ matrix.node }} uses: actions/setup-node@v4 with: From 7696938f28f4c726c9a509476f892578abae9620 Mon Sep 17 00:00:00 2001 From: Wojtek Surowka Date: Mon, 22 Jun 2026 01:24:02 +0100 Subject: [PATCH 8/8] CHange the way test waits for diagnostics --- test/test-suite/extension.test.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/test/test-suite/extension.test.ts b/test/test-suite/extension.test.ts index d96aae1..b0fa9af 100644 --- a/test/test-suite/extension.test.ts +++ b/test/test-suite/extension.test.ts @@ -36,17 +36,21 @@ suite('Erlang Language Extension', () => { assert.ok(document != null); assert.equal('erlang', document.languageId); - const waitForDiags = new Promise((resolve, reject) => { + const waitForDiags = new Promise((resolve) => { const disposeToken = vscode.languages.onDidChangeDiagnostics( - async (ev) => { - disposeToken.dispose(); - resolve(ev.uris); + (ev) => { + const docUri = document.uri.toString(); + if (ev.uris.some(u => u.toString() === docUri)) { + const diags = vscode.languages.getDiagnostics(document.uri); + if (diags.length > 0) { + disposeToken.dispose(); + resolve(diags); + } + } } - ) + ) }); - const uris = await waitForDiags; - assert.equal(true, uris.length > 0); - const diags = vscode.languages.getDiagnostics(uris[0]); + const diags = await waitForDiags; assert.equal(1, diags.length); }); }); \ No newline at end of file