Skip to content

Commit

Permalink
feat: jbang direct edit (#1042)
Browse files Browse the repository at this point in the history
* feat:edit directory now assumes editor has jbang enabled/installed

* update docs

* jbang edit now searches for nearest project root

* more tests for project locate

* flip edit tests to use -b and make jfx resolution pass on arch64

* fix itests

* remove list.of
  • Loading branch information
maxandersen committed Mar 18, 2023
1 parent cdc9aae commit 1c798a9
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 56 deletions.
35 changes: 32 additions & 3 deletions docs/modules/ROOT/pages/editing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ endif::[]

toc::[]

You can edit your script in an IDE/editor by using `jbang edit helloworld.java`. This will generate a project in a temporary location with symbolic links to your script and open a editor.
You can edit your script in an IDE/editor by using `jbang edit helloworld.java`.

[source, bash]
----
jbang edit helloworld.java
----

or

[source, bash]
----
jbang edit . helloworld.java
----

By default `jbang` will offer to install https://vscodium.com[VSCodium] (free/libre version of Visual Studio code) with default java extensions enabled in so called https://code.visualstudio.com/docs/editor/portable["portable mode"]. Portable mode means all the installed binaries and configuration does not affect rest of your system; everything is stored in `~/.jbang/editor`.

This automatic install and setup of editor is fully optional and if you have another IDE or editor already installed use it using `jbang edit --open=<editor>` or set JBANG_EDITOR environment variable to have jbang use it by default.
Expand All @@ -30,6 +37,8 @@ This automatic install and setup of editor is fully optional and if you have ano
jbang edit --open=[editor] helloworld.java
----

If your IDE does not support JBang style project you can use "sandbox" (`-b`) mode where `jbang` will generate a project in a temporary location with symbolic links to your script and open a editor.

Finally you can also if you prefer to call the editor/IDE yourself in a shell that supports variable evaluation specify `--no-open` to tell JBang to not open an editor.

[source, bash]
Expand All @@ -48,9 +57,29 @@ for your user or run your shell/terminal as administrator to have this feature w
You can also use `jbang edit --live` and `jbang` will launch your editor while watching
for file changes and regenerate the temporary project to pick up changes in dependencies.

== IDE and Editor support
== IDE Support

Some IDE's have dedicated JBang support and `jbang edit` assumes your IDE has this support installed.

If your IDE does not support JBang directly you can use "sandbox" mode a temporary project is created with
symoblic links and a project setup that works in most Java based IDE's. This is useful for single file scripts.
If you have multiple files we highly recommend to use a IDE that have JBang plugin.

The following IDE's have dedicated support (at time of writing):

:sicons: https://simpleicons.org/icons
.IDE's with JBang support
[width=75%,frame=none,grid=none]
|===
|image:{sicons}/visualstudiocode.svg[50,50] https://code.visualstudio.com[Visual Studio Code]
|image:{sicons}/eclipseide.svg[50,50] https://www.eclipse.org/downloads/download.php[Eclipse]
|image:{sicons}/intellijidea.svg[50,50] https://www.jetbrains.com/idea/download[IntelliJ Idea]
|image:{sicons}/gitpod.svg[50,50] https://www.gitpod.io[GitPod]
|===

== Sandbox edit support

The `edit` feature been tested with the following IDE's:
The sandbox `edit -b` feature using temporary directory and symbolic links been tested with the following IDE's:

:sicons: https://simpleicons.org/icons
.IDE's and Editors tested with `jbang`
Expand Down
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/organizing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Here `resource.properties` will be copied as is and `META-INF/resources/index.ht

All locations are relative to the script location.

WARNING: Currently `jbang edit` and http(s) based script do not work with `//FILES`. Will be added later.
WARNING: Currently `jbang edit` and http(s) based script do not work with `//FILES`.

== Extension-less/non-java files for cli-plugins

Expand Down
4 changes: 3 additions & 1 deletion docs/modules/cli/pages/jbang.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ jbang - jbang is a tool for building and running .java/.jsh scripts and jar pack
jbang init hello.java [args...]
(to initialize a script)
or jbang edit --open=code --live hello.java
(to edit a script in IDE with live updates)
(to edit a script in any IDE with live updates)
or jbang edit . hello.java
(to edit a folder in IDE that supports JBang, see https://jbang.dev/ide)
or jbang hello.java [args...]
(to run a .java file)
or jbang gavsearch@jbangdev [args...]
Expand Down
4 changes: 2 additions & 2 deletions itests/edit.feature
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ Feature: edit

Scenario: edit no-open a file should print to std out
* command('jbang init hello.java')
* command('jbang edit --no-open hello.java')
* match err == ''
* command('jbang edit -b --no-open hello.java')
* match err == '[jbang] Creating sandbox for script editing hello.java\n'
* match out contains 'hello'
172 changes: 137 additions & 35 deletions src/main/java/dev/jbang/cli/Edit.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
package dev.jbang.cli;

import static dev.jbang.Settings.CP_SEPARATOR;
import static dev.jbang.util.Util.pathToString;
import static dev.jbang.util.Util.verboseMsg;
import static java.lang.System.out;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.function.Function;
Expand Down Expand Up @@ -45,8 +40,11 @@ public class Edit extends BaseCommand {
@CommandLine.Mixin
DependencyInfoMixin dependencyInfoMixin;

@CommandLine.Parameters(index = "1", arity = "0..N")
List<String> additionalFiles = new ArrayList<>();

@CommandLine.Option(names = {
"--live" }, description = "Setup temporary project, regenerate project on dependency changes.")
"--live" }, description = "Open directory in IDE's that support JBang or generate temporary project with option to regenerate project on dependency changes.")
public boolean live;

@CommandLine.Option(names = {
Expand All @@ -56,41 +54,144 @@ public class Edit extends BaseCommand {
@CommandLine.Option(names = { "--no-open" })
public boolean noOpen;

@CommandLine.Option(names = { "-b", "--sandbox" })
boolean sandbox;

/**
* Returns a parent path if one of the targetFileNames exist in such parent.
*
* @param filePath
* @param targetFileNames list of markers to search for
* @return if found returns the marker path, if none found return null.
*/
static Path findNearestRoot(Path filePath, List<String> targetFileNames) {
Path file = filePath.toAbsolutePath().normalize();
Path parent = file.getParent();

// stop/skip local root dir (user.home)
while (parent != null && !Settings.getLocalRootDir().equals(parent)) {
for (String targetFileName : targetFileNames) {
Path targetFile = parent.resolve(targetFileName);
if (Files.exists(targetFile)) {
return targetFile;
}
}
parent = parent.getParent();
}
return null;
}

static List<String> buildRootMarkers = Arrays.asList("jbang.build", "pom.xml", "build.gradle", ".jbang",
".project");
static List<String> editorRootMarkers = Arrays.asList(".vscode", ".idea", ".eclipse");

static Path safeParent(Path p) {
if (!p.isAbsolute()) {
p = p.toAbsolutePath();
}
return p.getParent();
}

static public Path locateProjectDir(Path location) {
if (!Files.isDirectory(location)) {
// peek into file and find package statement - then start searching from that
// root instead.
try {
String pkg = Util.getSourcePackage(Util.readString(location)).orElse("");
if (!"".equals(pkg)) { // if package found try resolve matching parent dirs
String parentPath = pkg.replace(".", "/");
if (safeParent(location).endsWith(parentPath)) {
String relativeParent = String.join("/", Collections.nCopies(pkg.indexOf(".") + 1, ".."));
location = safeParent(location).resolve(relativeParent);
} else {
location = safeParent(location);
}
} else {
location = safeParent(location);
}
} catch (IOException e) {
Util.verboseMsg("Could not read package from " + location, e);
// ignore
}
}
// once we have a initial root dir to search from based on package or dir.
// Look for first level project markers by finding editor metadata
Path nearestRoot = findNearestRoot(location, editorRootMarkers);
if (nearestRoot == null) {
// if no edit location found try build projects
nearestRoot = findNearestRoot(location, buildRootMarkers);
}
if (nearestRoot != null) {
Util.verboseMsg("Found project root for edit: " + nearestRoot);
location = safeParent(nearestRoot);
}
return location.normalize();
}

@Override
public Integer doCall() throws IOException {
scriptMixin.validate();

// force download sources when editing
Util.setDownloadSources(true);

ProjectBuilder pb = createProjectBuilder();
final Project prj = pb.build(scriptMixin.scriptOrFile);
if (!sandbox) {
File location;
if (scriptMixin.scriptOrFile != null) {
location = new File(scriptMixin.scriptOrFile);
} else {
location = null;
}
info("Assuming your editor have JBang support installed. See https://jbang.dev/ide");
info("If you prefer to open in a sandbox run with `jbang edit -b` instead.");
Path path = null;
if (location == null) {
path = locateProjectDir(Paths.get("."));
} else {
path = locateProjectDir(location.toPath());
}
if (path != null && ((location != null) && !path.equals(location.toPath()))) {
additionalFiles.add(0, pathToString(location.toPath()));
}

if (prj.isJar() || prj.getMainSourceSet().getSources().isEmpty()) {
throw new ExitException(EXIT_INVALID_INPUT, "You can only edit source files");
}
if (!noOpen) {
openEditor(pathToString(path), additionalFiles);
}
System.out.println(path);
} else {
scriptMixin.validate(false);
File location = new File(scriptMixin.scriptOrFile);
info("Creating sandbox for script editing " + location);
ProjectBuilder pb = createProjectBuilder();
final Project prj = pb.build(scriptMixin.scriptOrFile);

if (prj.isJar() || prj.getMainSourceSet().getSources().isEmpty()) {
throw new ExitException(EXIT_INVALID_INPUT, "You can only edit source files");
}

Path project = createProjectForLinkedEdit(prj, Collections.emptyList(), false);
String projectPathString = Util.pathToString(project.toAbsolutePath());
// err.println(project.getAbsolutePath());
Path project = createProjectForLinkedEdit(prj, Collections
.emptyList(),
false);
String projectPathString = pathToString(project.toAbsolutePath());
// err.println(project.getAbsolutePath());

if (!noOpen) {
openEditor(project, projectPathString);
}
if (!noOpen) {
openEditor(projectPathString, additionalFiles);
}

if (!live) {
out.println(projectPathString); // quit(project.getAbsolutePath());
} else {
watchForChanges(prj, () -> {
// TODO only regenerate when dependencies changes.
info("Regenerating project.");
try {
createProjectForLinkedEdit(prj, Collections.emptyList(), true);
} catch (IOException e) {
throw new RuntimeException(e);
}
return null;
});
if (!live) {
out.println(projectPathString); // quit(project.getAbsolutePath());
} else {
watchForChanges(prj, () -> {
// TODO only regenerate when dependencies changes.
info("Regenerating project.");
try {
createProjectForLinkedEdit(prj, Collections.emptyList(), true);
} catch (IOException e) {
throw new RuntimeException(e);
}
return null;
});
}
}
return EXIT_OK;
}
Expand Down Expand Up @@ -140,7 +241,7 @@ private void watchForChanges(Project prj, Callable<Object> action) throws IOExce

// try open editor if possible and install if needed, returns true if editor
// started, false if not possible (i.e. editor not available)
private boolean openEditor(Path project, String projectPathString) throws IOException {
private boolean openEditor(String projectPathString, List<String> additionalFiles) throws IOException {
if (!editor.isPresent() || editor.get().isEmpty()) {
editor = askEditor();
if (!editor.isPresent()) {
Expand All @@ -151,11 +252,12 @@ private boolean openEditor(Path project, String projectPathString) throws IOExce
}
if ("gitpod".equals(editor.get()) && System.getenv("GITPOD_WORKSPACE_URL") != null) {
info("Open this url to edit the project in your gitpod session:\n\n"
+ System.getenv("GITPOD_WORKSPACE_URL") + "#" + project.toAbsolutePath() + "\n\n");
+ System.getenv("GITPOD_WORKSPACE_URL") + "#" + projectPathString + "\n\n");
} else {
List<String> optionList = new ArrayList<>();
optionList.add(editor.get());
optionList.add(projectPathString);
optionList.addAll(additionalFiles);

String[] cmd;
if (Util.getShell() == Shell.bash) {
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/dev/jbang/cli/Init.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ public Integer doCall() throws IOException {
info("File initialized. You can now run it with 'jbang " + renderedScriptOrFile
+ "' or edit it using 'jbang edit --open=[editor] "
+ renderedScriptOrFile + "' where [editor] is your editor or IDE, e.g. '"
+ Edit.knownEditors[new Random().nextInt(Edit.knownEditors.length)] + "'");
+ Edit.knownEditors[new Random().nextInt(Edit.knownEditors.length)]
+ "'. If your IDE supports JBang, you can edit the directory instead: 'jbang edit . '"
+ renderedScriptOrFile + ". See https://jbang.dev/ide");

return EXIT_OK;
}
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/dev/jbang/cli/ScriptMixin.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,9 @@ public void validate() {
}
}

public void validate(boolean scriptRequired) {
if (scriptRequired) {
validate();
}
}
}
22 changes: 10 additions & 12 deletions src/test/java/dev/jbang/cli/TestEdit.java
Original file line number Diff line number Diff line change
Expand Up @@ -235,29 +235,27 @@ void testEditFile(@TempDir Path outputDir) throws IOException {
});
}

@Test
void testEditMissingScript() {
assertThrows(IllegalArgumentException.class, () -> {
CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("edit");
Edit edit = (Edit) pr.subcommand().commandSpec().userObject();
edit.doCall();
});
}
/*
* @Test void testEditMissingScript() {
* assertThrows(IllegalArgumentException.class, () -> { CommandLine.ParseResult
* pr = JBang.getCommandLine().parseArgs("edit"); Edit edit = (Edit)
* pr.subcommand().commandSpec().userObject(); edit.doCall(); }); }
*/

@Test
void testEditNonSource() {
void testSandboxEditNonSource() {
assertThrows(ExitException.class, () -> {
Path jar = examplesTestFolder.resolve("hellojar.jar");
CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("edit", "--no-open", jar.toString());
CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("edit", "-b", "--no-open", jar.toString());
Edit edit = (Edit) pr.subcommand().commandSpec().userObject();
edit.doCall();
});
}

@Test
void testEdit() throws IOException {
void testSandboxEdit() throws IOException {
Path src = examplesTestFolder.resolve("helloworld.java");
CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("edit", "--no-open", src.toString());
CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("edit", "-b", "--no-open", src.toString());
Edit edit = (Edit) pr.subcommand().commandSpec().userObject();
edit.doCall();
}
Expand Down
Loading

0 comments on commit 1c798a9

Please sign in to comment.