Add Java Support
It'd be nice to have Java support as well.
JShell, the Java REPL introduced in Java 9, should allow a stdio client.
We'd want to pass the classpath for the project somehow.
nvim-jdtls uses the Java language server to get this:
https://github.com/mfussenegger/nvim-jdtls/blob/c23f200fee469a415c77265ca55b496feb646992/lua/jdtls/util.lua#L122-L151
It then launches jshell passing the CLASSPATH as an environment variable:
https://github.com/mfussenegger/nvim-jdtls/blob/c23f200fee469a415c77265ca55b496feb646992/lua/jdtls.lua#L1144-L1163
Alternatively, jshell accepts a --class-path flag.
I'm not sure yet, but we may need to process all the import_declarations (the tree-sitter node for an import statement) in the current buffer to execute snippets.
As a first pass at a JShell client, I would suggest starting with using g:conjure#client#jshell#stdio#command set to jshell with the instruction that the CLASSPATH environment variable be set before running Neovim from the project directory.
This way, the focus can be on interacting with JShell.
Automatically setting the CLASSPATH before running JShell is nice but complicated like setting up and running virtual environments for Python. My feeling (I'm not the maintainer) is that having a Conjure client take care of these things would add additional complexity that someone would need to debug and maintain going forward.
Would the filetype be java and jsh?
This should help with figuring out how a JShell client should interact with a JShell REPL.
https://docs.oracle.com/en/java/javase/21/jshell/introduction-jshell.html
The Introduction to Commands says:
Commands are distinguished from snippets by a slash (/).
So, it would be useful to be able to evaluate /imports from a buffer to see what JShell has imported into its environment.
I mention these things because there needs to be some clarity about what people might expect to be able to do from a buffer of Java code.
It's been over four years since I've programmed in Java 8 and haven't used JShell for development work.
As a first pass at a JShell client, I would suggest starting with using
g:conjure#client#jshell#stdio#commandset tojshellwith the instruction that theCLASSPATHenvironment variable be set before running Neovim from the project directory.This way, the focus can be on interacting with
JShell.Automatically setting the
CLASSPATHbefore runningJShellis nice but complicated like setting up and running virtual environments for Python. My feeling (I'm not the maintainer) is that having a Conjure client take care of these things would add additional complexity that someone would need to debug and maintain going forward.
Thanks for these thoughts @russtoku.
I agree. Setting the CLASSPATH manually as a first-pass sounds good to me to avoid the additional complications.
Would the filetype be
javaandjsh?
I wasn't thinking .jsh, but sounds good to me. This could be useful if people want to quickly bootstrap a session.
This should help with figuring out how a JShell client should interact with a JShell REPL.
https://docs.oracle.com/en/java/javase/21/jshell/introduction-jshell.html
The Introduction to Commands says:
Commands are distinguished from snippets by a slash (/).
So, it would be useful to be able to evaluate
/importsfrom a buffer to see what JShell has imported into its environment.I mention these things because there needs to be some clarity about what people might expect to be able to do from a buffer of Java code.
It's been over four years since I've programmed in Java 8 and haven't used JShell for development work.
Absolutely.
I'm not entirely sure if I have clear expectations in my mind about what we can / should expect from a buffer of Java code yet, so your questions are very helpful.
1. How might you expect to see the output from a /imports command? Appended to the conjure log? And what would be the trigger? Automatically print it when a new buffer is open?
I'm not sure if there's any similar examples or precedence with other Conjure clients yet.
About expectations, Ideally I'd like to be able to evaluate methods such as this:
public class Sandbox {
void main() {
increment(List.of(1, 2, 3));
}
List<Integer> increment(List<Integer> list) {
return list.stream().map(n -> n + 1).toList();
}
}
Where <localleader>ee on increment(List.of(1, 2, 3)) would work.
However, that's complicated because you first need to define the method in JShell.
2. Ideally this would happen automatically for the user, but maybe as a first pass we make the user manually select the method and send it to Jshell first?
Also, JShell creates "scratch variables" for the user.
For example, $2 below:
jshell> 2 + 2
$2 ==> 4
You can configure the level of feedback for Jshell, and whether these are printed.
3. We could also filter out the "$2 ==> " part, but they're maybe useful to the user?
- Ideally this would happen automatically for the user, but maybe as a first pass we make the user manually select the method and send it to Jshell first?
I'm guessing that most Conjure clients don't automatically evaluate all of the code in a buffer. That can be done after opening a source file using <localleader>eb (Evaluate buffer). Then, you can evaluate any methods after that given that is something you would do in the REPL.
One way to think about the desired interaction (from buffer to REPL and back) is to consider what it's like to incrementally develop a class in JShell.
jshell> class Sandbox {
...> void main() {
...> increment(List.of(1, 2, 3));
...> }
...> }
| created class Sandbox, however, it cannot be instantiated or its methods invoked until method increment(java.util.List<java.lang.Integer>) is declared
In Neovim, you might:
- Start with an empty buffer for a new file called Sandbox.java or Sandbox.jsh.
- You type in an initial implementation of the Sandbox class.
- With your cursor on the first line of the class definition, you type
<localleader>ee(evaluate current form) to evaluate the class definition. - The code block of the Sandbox class definition is sent to a JShell REPL.
- You see the
created class Sandbox, however, it ...message in the HUD, conjure log split, and/or virtual text near your cursor. - Then, you flesh out the
incrementmethod.
Now, it gets murky:
- Do you
<localleader>ee(evaluate current form) with the cursor in theincrementmethod? Evaluate current form would send just theincrementmethod's code block to JShell. - Do you
<localleader>er(evaluate root form) with the cursor in theincrementmethod? Evaluate root form would send the entire Sandboxclasscode block to JShell.
- We could also filter out the "$2 ==> " part, but they're maybe useful to the user?
The Guile (Scheme) REPL also prints out something similar like $2 = 42 and you can use the $2 in code where might use its value. The Conjure Guile client returns just the value without the $2 = part.
For example, I evaluate this in a buffer loaded from dev/guile/sandbox.scm:
(+ 5 6)
and I get:
(+ 5 6) => 11
where => 11 is in virtual text.
Meanwhile, in the Guile REPL that I'm connected to from Conjure via a UNIX socket, I can do this:
scheme@(guile-user)> (* 6 7)
$2 = 42
scheme@(guile-user)> $1
$3 = 11
As you can see the $1 variable can be used later. Guile calls this value history.
- Ideally this would happen automatically for the user, but maybe as a first pass we make the user manually select the method and send it to Jshell first?
I'm guessing that most Conjure clients don't automatically evaluate all of the code in a buffer. That can be done after opening a source file using
<localleader>eb(Evaluate buffer). Then, you can evaluate any methods after that given that is something you would do in the REPL.
Thanks for these thoughts @russtoku.
I was inspired by the Lua Neovim client, but I'm not entirely sure how it works.
I like the following behavior:
function increment(list)
local ret = {}
for _, e in ipairs(list) do
table.insert(ret, e + 1)
end
return ret
end
increment({1, 2, 3}) -- Cursor on this line
<localleader>ee outputs {2, 3, 4}.
However, if I make the increment function a local function, then it doesn't work and I have to send the increment function to the REPL first.
One way to think about the desired interaction (from buffer to REPL and back) is to consider what it's like to incrementally develop a class in JShell.
jshell> class Sandbox { ...> void main() { ...> increment(List.of(1, 2, 3)); ...> } ...> } | created class Sandbox, however, it cannot be instantiated or its methods invoked until method increment(java.util.List<java.lang.Integer>) is declared In Neovim, you might:
- Start with an empty buffer for a new file called Sandbox.java or Sandbox.jsh.
- You type in an initial implementation of the Sandbox class.
- With your cursor on the first line of the class definition, you type
<localleader>ee(evaluate current form) to evaluate the class definition.- The code block of the Sandbox class definition is sent to a JShell REPL.
- You see the
created class Sandbox, however, it ...message in the HUD, conjure log split, and/or virtual text near your cursor.- Then, you flesh out the
incrementmethod.Now, it gets murky:
- Do you
<localleader>ee(evaluate current form) with the cursor in theincrementmethod? Evaluate current form would send just theincrementmethod's code block to JShell.- Do you
<localleader>er(evaluate root form) with the cursor in theincrementmethod? Evaluate root form would send the entire Sandboxclasscode block to JShell.
In general, I think I would avoid sending the root form, or class in favor of sending smaller snippets like variables and methods.
This is so evaluation is easier since you don't need to type new MyClass(...).someMethod() to see the result, and can instead send smaller expressions like a method call.
However, I think you'd have to send the entire class and instantiate if you reference instance variables with this.
- We could also filter out the "$2 ==> " part, but they're maybe useful to the user?
The Guile (Scheme) REPL also prints out something similar like
$2 = 42and you can use the$2in code where might use its value. The Conjure Guile client returns just the value without the$2 =part.For example, I evaluate this in a buffer loaded from
dev/guile/sandbox.scm:(+ 5 6) and I get:
(+ 5 6) => 11 where
=> 11is in virtual text.Meanwhile, in the Guile REPL that I'm connected to from Conjure via a UNIX socket, I can do this:
scheme@(guile-user)> (* 6 7) $2 = 42 scheme@(guile-user)> $1 $3 = 11 As you can see the
$1variable can be used later. Guile calls this value history.
That's helpful.
Maybe we should keep similar behavior then. I see in the Guile socket client code where we parse the value: https://github.com/Olical/conjure/blob/0ac12d481141555cc4baa0ad656b590ed30d2090/fnl/conjure/client/guile/socket.fnl#L171
How can I connect to a Guile REPL from conjure via a UNIX socket?
I tried running guile --listen, and loading dev/guile/sandbox.scm in Neovim, but I see the following in the log:
; mit-scheme (ENOENT: no such file or directory)
Lastly, the JShell REPL seems to preface "feedback" with a pipe character. Your example eariler demonstrates this well:
jshell> class Sandbox {
...> void main() {
...> increment(List.of(1, 2, 3));
...> }
...> }
| created class Sandbox, however, it cannot be instantiated or its methods invoked until method increment(java.util.List<java.lang.Integer>) is declared
Ideally I think we'd show the feedback in the log, but with comment syntax highlighting.
I have my comment-prefix set to // since it's Java, and the filetype for the log buffer is set accordingly.
I'm wondering if I need to replace | with // manually with code, or there's some other way to achieve comment syntax highlighting for the REPL feedback.
How can I connect to a Guile REPL from conjure via a UNIX socket?
I tried running guile --listen, and loading dev/guile/sandbox.scm in Neovim, but I see the following in the log:
; mit-scheme (ENOENT: no such file or directory)
The log is saying that you're are using MIT Scheme and not Guile.
To connect to Guile REPL listening on a UNIX (domain) socket (pipe file), you'd need to start the Guile REPL with something like:
$ guile --listen="$HOME/guile-repl.socket"
where say HOME=/Users/gbroques or whatever. This starts up a Guile REPL listening on a UNIX socket file.
Then configure Conjure to use Guile for *.scm files using Lua:
vim.g["conjure#filetype#scheme"] = "conjure.client.guile.socket"
vim.g["conjure#client#guile#socket#pipename"] = "/Users/gbroques/guile-repl.socket"
Or using Vimscript:
let g:conjure#filetype#scheme = "conjure.client.guile.socket"
let g:conjure#client#guile#socket#pipename = "/Users/gbroques/guile-repl.socket"
Lastly, edit the dev/guile/sandbox.scm file (or write your own code in a Scheme buffer) and connect to the Guile REPL using <localleader>cc.
See :help conjure-client-guile-socket or https://github.com/Olical/conjure/blob/main/doc/conjure-client-guile-socket.txt.