Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add language support for notebook cells #236

Open
tgodzik opened this issue Oct 20, 2021 · 33 comments
Open

Add language support for notebook cells #236

tgodzik opened this issue Oct 20, 2021 · 33 comments

Comments

@tgodzik
Copy link
Contributor

tgodzik commented Oct 20, 2021

Is your feature request related to a problem? Please describe.

Currently, when using notebook cells in VS Code, Metals is unable to provide any language support. Most likely connected with an exception:

WARNING: Notification threw an exception: {
  "jsonrpc": "2.0",
  "method": "metals/didFocusTextDocument",
  "params": [
    "vscode-notebook-cell:Untitled-1.ipynb?jupyter-notebook#ch0000000untitled"
  ]
}
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
	at org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint.lambda$null$0(GenericEndpoint.java:67)
	at org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint.notify(GenericEndpoint.java:152)
	at org.eclipse.lsp4j.jsonrpc.RemoteEndpoint.handleNotification(RemoteEndpoint.java:220)
	at org.eclipse.lsp4j.jsonrpc.RemoteEndpoint.consume(RemoteEndpoint.java:187)
	at org.eclipse.lsp4j.jsonrpc.TracingMessageConsumer.consume(TracingMessageConsumer.java:114)
	at org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.handleMessage(StreamMessageProducer.java:194)
	at org.eclipse.lsp4j.jsonrpc.json.StreamMessageProducer.listen(StreamMessageProducer.java:94)
	at org.eclipse.lsp4j.jsonrpc.json.ConcurrentMessageProcessor.run(ConcurrentMessageProcessor.java:113)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.eclipse.lsp4j.jsonrpc.services.GenericEndpoint.lambda$null$0(GenericEndpoint.java:65)
	... 12 more
Caused by: java.nio.file.FileSystemNotFoundException: Provider "vscode-notebook-cell" not installed
	at java.nio.file.Paths.get(Paths.java:147)
	at scala.meta.internal.metals.MetalsEnrichments$XtensionString.toAbsolutePath(MetalsEnrichments.scala:577)
	at scala.meta.internal.metals.MetalsEnrichments$XtensionString.toAbsolutePath(MetalsEnrichments.scala:574)
	at scala.meta.internal.metals.MetalsLanguageServer.didFocus(MetalsLanguageServer.scala:1055)
	... 17 more

Describe the solution you'd like

Support all the standard language features. We would most likely also need to provide some notebook specific features (such as using values from other cells?).

Describe alternatives you've considered

None I can think of.

Additional contex

This came up in scalameta/metals#3213 and is separate to supporting https://code.visualstudio.com/api/extension-guides/notebook

Search terms

notebooks vscode

@Quafadas
Copy link

Quafadas commented May 6, 2022

@tgodzik This is something that I'd love to have in metals! Depending on how hard it is, I would consider a tilt at it :-)...

I've done some experiments in the past, which prove that one can indeed run scala in a VSCode notebook, simply by wheeling in the Almond kernel. However, it's a "raw" experience right now.

I hope, that could be improved in a couple of ways, which I believe to be 'independent'.

  1. convince the notebooks to recognise the metals LSP server. I did manage to get metals working in a jupyterhub via LSP, so something is at least close already. My (naive) hope is that this might just be a case of trawling some documentation to get a config right. This would be cool in its own right, even if I didn't mange to go further.

  2. FTW, what would be amazing, would be if it was possible to execute a notebook cell, in the context of a project - same like kicking up an ammonite / scala console out the build tool. I fear, that this may be a rather difficult problem. My hope would be that it was possible to 'trick' almond into having the project context, without having to change almond itself. I am not yet sure, how... but hope springs eternal.

Is this something you'd devoted any thought to?

If so, is the above rational? On a scale of 1-10, how hard would you perceive these problems to be? Something much above 5, and it's likely I'm out of my skill zone, sadly.

If they aren't too bad, would you be willing to guide an effort along these lines, and / or set out a solution sketch? I'm not familiar with the project, so if you had a hint on where to look, it would be great, or any other thoughts?

@tgodzik
Copy link
Contributor Author

tgodzik commented May 9, 2022

@Quafadas thanks for showing the interest! This is actually part of GSoC projects this year, but there it's possible it will not be picked. I will let you know once we have the results.

convince the notebooks to recognise the metals LSP server. I did manage to get metals working in a jupyterhub via LSP, so something is at least close already. My (naive) hope is that this might just be a case of trawling some documentation to get a config right. This would be cool in its own right, even if I didn't mange to go further.

It already recognises the notebook cells written in Scala from what I've seen, but there is no support for the particular URI, which we would need to work on (we get the exceptions as shown in the description)

FTW, what would be amazing, would be if it was possible to execute a notebook cell, in the context of a project - same like kicking up an ammonite / scala console out the build tool. I fear, that this may be a rather difficult problem. My hope would be that it was possible to 'trick' almond into having the project context, without having to change almond itself. I am not yet sure, how... but hope springs eternal.

I might be possible to execute almond with the proper classpath and settings to make it work, but I am not familiar with the almond API right now.

If so, is the above rational? On a scale of 1-10, how hard would you perceive these problems to be? Something much above 5, and it's likely I'm out of my skill zone, sadly.

I think it's around 5, but mostly it would be getting all the existing tools to work together, so a bit of things to figure out and change, but nothing that would require large scale rewrites.

If they aren't too bad, would you be willing to guide an effort along these lines, and / or set out a solution sketch? I'm not familiar with the project, so if you had a hint on where to look, it would be great, or any other thoughts?

I will let you know if this project gets to be a part of gsoc, if not we can try and figure it out together 😄

@Quafadas
Copy link

Quafadas commented May 10, 2022

This is actually part of GSoC projects this year

Ohhhh... excellent.

Thanks for taking the time to write back. I'll keep my fingers crossed that it gets picked up by that project!

@tgodzik
Copy link
Contributor Author

tgodzik commented May 23, 2022

This is actually part of GSoC projects this year

Ohhhh... excellent.

Thanks for taking the time to write back. I'll keep my fingers crossed that it gets picked up by that project!

This project hasn't been picked for GSoC, so if you are interested in working on it that would be awesome! I can try and provide some pointers, but generally I think the work you already did could be automated almond setup plus making sure that the notebook file provider works (there might be already an implementation for Java, but not sure.)

@Quafadas
Copy link

Quafadas commented May 23, 2022 via email

@tgodzik
Copy link
Contributor Author

tgodzik commented May 23, 2022

*This project hasn't been picked for GSoC * Oh no! I’m away right now, so will write back in a week or so. I’d like to have a think before committing to spending a lot of time, and probably do need to figure out just how long it might take. I do think this would be a very cool thing to have though!

I think this one can be done iteratively, and I can fill in any missing pieces if it turns out you don't have time. Also, it's totally fine if you at some point decide that you can't work on it further.

@Quafadas
Copy link

Quafadas commented Jun 1, 2022

@tgodzik So I'm definitely up for this, although I have no experience with this sort of tooling. Should be fun.

Sadly however for "reasons", I need to ban myself before mid-July - I have something else to focus on that I can't drop.

I will spend some time now, trying to get started with changing metals and getting over the "contributor getting started" hump.

Hopefully, I'll be posting up here around mid-July / early August, when I think I can allocate some time to this (if it's still open - which weirdly, I hope it is!).

@Quafadas
Copy link

Quafadas commented Jul 7, 2022

@tgodzik I hope to have some time over the coming weeks to look at this... my plan over the coming days is to look at getting started with changing metals and then go from there...

@tgodzik
Copy link
Contributor Author

tgodzik commented Jul 7, 2022

Sure, I also mentioned it as a possible student project at EPFL, but that would be in septemeber and we can remove it from the list if you manage to take a look at it. Similar to GSoC this might also not be picked up at all.

Some pointers:

@Quafadas
Copy link

Quafadas commented Jul 10, 2022

@tgodzik So, I have invested a little time and sat down with this, and have (already!) reached the stage at which I'm not sure how to proceed. This is complex for me!

For the sake of transparency, here is my branch. Changes thus far are basically trivial - add an almond version, and follow the pattern set out in the download methods. To be clear, it doesn't work!

[info] 2022.07.10 16:38:20 INFO  Downloading almond (for notebook support)
[error] Exception in thread "main" coursierapi.error.SimpleResolutionError$1: Error downloading sh.almond:scala-kernel_2.13.8:0.13.0
[error]   not found: /Users/simon/.ivy2/local/sh.almond/scala-kernel_2.13.8/0.13.0/ivys/ivy.xml

Because that artefact, doesn't exist.
Branch
https://github.com/Quafadas/metals/tree/jupyter_notebooks

Maven
https://mvnrepository.com/artifact/sh.almond/scala-kernel

Given that maintenance of the almond project itself is somewhat beyond the scope of this issue... that has some implications. Here are the things I'm now unsure about.

  1. Which artefact to even download (!)...
  2. As almond interacts with the scala kernel, I believe it it publishes against the specific kernel versions rather than the binary versions. The versions of scala Almond supports / publish will not intersect with metals published / supported versions except through chance.
  3. What the purpose of the download step is? Is it exactly to allow projects like almond to exist inside metals, without the explicit, and hence transitive dependance?

What (I think) about the above... I hope I'm not heading in the wrong direction

  1. I believe the artefact of interest to be https://mvnrepository.com/artifact/sh.almond/scala-kernel against the assumptions that
    1.1 our aim is to instantiate one of these; https://github.com/almond-sh/almond/blob/main/modules/scala/scala-kernel/src/main/scala/almond/ScalaKernel.scala.
    1.1 and hence have metals own an almond kernel... for supported versions?
  2. For mismatched almond / metals scala (x.x.x) version problem, I assume I could somehow use the coursier API to check the set of published versions of that scala-kernal artefact, and intersect this with the set of supported metals versions... and only attempt to download the intersection?

If the idea was instead to use metals to construct an almond launcher, which would then be launched as an independent service, then I guess this download step would be done rather differently by using coursier to bootstrap a launcher? I am assuming, that an "independent launcher" strategy, means that it would ultimately be very difficult to get the class paths of our build into the notebook...and hence that it is not the right direction of travel?

I'm sorry this post is so long...

@tgodzik
Copy link
Contributor Author

tgodzik commented Jul 11, 2022

Which artefact to even download (!)...
As almond interacts with the scala kernel, I believe it it publishes against the specific kernel versions rather than the binary versions. The versions of scala Almond supports / publish will not intersect with metals published / supported versions except through chance.

That's a separate issue altogether, I can try ask about it. We could just have the latest supported version specified similar to how we deal currently with Ammonite. We can show a warning if a version is not yet supported

What the purpose of the download step is? Is it exactly to allow projects like almond to exist inside metals, without the explicit, and hence transitive dependance?

We don't want to always download all possible dependecies especially if we would need to download it for each Scala version, which would mean downloading every Scala version for the user even if they don't need it. The download step is used to avoid that.

I believe the artefact of interest to be https://mvnrepository.com/artifact/sh.almond/scala-kernel against the assumptions that
1.1 our aim is to instantiate one of these; https://github.com/almond-sh/almond/blob/main/modules/scala/scala-kernel/src/main/scala/almond/ScalaKernel.scala.
1.1 and hence have metals own an almond kernel... for supported versions?

We could use ShellRunner for running the specific kernel. When is the kernel actually needed? Only when running?

For mismatched almond / metals scala (x.x.x) version problem, I assume I could somehow use the coursier API to check the set of published versions of that scala-kernal artefact, and intersect this with the set of supported metals versions... and only attempt to download the intersection?

We can put that information in the build.sbt and show the users proper warnings.

@Quafadas
Copy link

Quafadas commented Jul 11, 2022

This is potentially a very useful note

microsoft/vscode-jupyter#10813

https://jupyter-client.readthedocs.io/en/latest/kernels.html#kernel-specs

microsoft/vscode-jupyter#10151

In summary; this now all looks tractable, if I can figure out how to bootstrap almond out of the coursier API.

The first link says VSCode respects the kernel specs. The second link sets out the kernel specs, and suggests that a JSON file which allows one to, for example set environment variables for the notebook process.

The third link sets out how one may influence the class path through setting environment variables.

@Quafadas
Copy link

Quafadas commented Jul 21, 2022

I made a slight change (pushed to branch above) to the shell runner to allow it to accept extra repositories when downloading dependancies, because almond has a dependancy on JVM repr which appears only to be available on jitpack. Easily solved.

In the metals test project, if I fire up a console and run this script, it downloads the key almond kernel dependancy. The last line even installs the kernel in (apparently)... the right place.

import scala.meta.internal.metals._
import scala.meta.internal.builds.ShellRunner
import coursierapi._
implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global

val absPath = scala.meta.io.AbsolutePath("/Users/simon/Code/metals")

val aClient = new tests.TestingClient(absPath, scala.meta.internal.metals.Buffers())

val uConf = scala.meta.internal.metals.UserConfiguration()
val cConf = new ClientConfiguration(new MetalsServerConfig())
 
val t = Time.system.currentMillis()

val s = new StatusBar(() => aClient, Time.system, ProgressTicks.braille, cConf)

val sr = new ShellRunner(aClient, () => uConf, Time.system, s)

val playdoh =  scala.meta.io.AbsolutePath("/Users/simon/Code/playdoh")

val almondDep =     Dependency.of(
      "sh.almond",
      s"scala-kernel_2.13.7",
      "0.13.0"
    )
    
val jvmReprRepo =   coursierapi.MavenRepository
    .of("https://maven.imagej.net/content/repositories/public/")

 sr.runJava(almondDep, "almond.ScalaKernel", playdoh, List("--install", "--command", "java -jar /Users/simon/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/sh/almond/scala-kernel_2.13.7/0.13.0/scala-kernel_2.13.7-0.13.0.jar", "--id", "metalsAlmond", "--display-name", "metalsAlmond", "--global", "false", "--jupyter-path", "/Users/simon/Library/Jupyter/kernels/" ), false, extraRepos=Array(jvmReprRepo))

// "--copy-launcher", "true" too see what it's trying to run.

This gets tantalisingly close. It places the kernel in the right place, and it's even found by VSCode! There are two problems;

  1. I manually figured out the path to the almond kernel artefact. Is there an obvious way to do that programmatically?

  2. Bigger issues, is that the kernel cannot be started outside of that metals context. A manual attempt at doing so

java -jar /Users/simon/Library/Caches/Coursier/v1/https/repo1.maven.org/maven2/sh/almond/scala-kernel_2.13.7/0.13.0/scala-kernel_2.13.7-0.13.0.jar
Error: Unable to initialize main class almond.ScalaKernel
Caused by: java.lang.NoClassDefFoundError: scala/Product

So what I think is happening, is that normally, coursier is bundling together all he stuff the JAR needs to launch vanilla almond. That's not gonna work for us. Based on my google-fu,
https://stackoverflow.com/questions/18413014/run-a-jar-file-from-the-command-line-and-specify-classpath

I guess that I now need to tamper with class paths...

I do not know much about class paths. I don't even know if that's embarrassing or not :-(. Can I get the class path of the project metals is working on before using the shell runner? Any ideas?

@tgodzik
Copy link
Contributor Author

tgodzik commented Jul 22, 2022

  1. I manually figured out the path to the almond kernel artefact. Is there an obvious way to do that programmatically?

Actually, fetch which is done in runJava will return the full classpath, which it used to run and you can also get the scala-kernel from there.

  1. Bigger issues, is that the kernel cannot be started outside of that metals context. A manual attempt at doing so

Och, that is interesting, so --command requires to set the classpath again? Is it possible to just use the same classpath as ScalaKernel was run with? If there is no other way you would need to manually get the classpath from the fetch command and give to that command, so a custom ShellRunner method would most likely be needed.

@Quafadas
Copy link

Quafadas commented Jul 22, 2022

Actually, fetch which is done in runJava will return the full classpath, which it used to run and you can also get the scala-kernel from there.

Ooooo, okay, let me try to figure that out.

Och, that is interesting, so --command requires to set the classpath again?

Yes - I believe because it's ultimately VSCode which is going to launch the kernel, not us!

I'm thinking that we should use metals to "install" the right version of the kernel. The massive + point about "installing", is that it puts it "in the right place", and VSCode finds it and we're inside that infrastructure for free... basically we take ruthless advantage of all the work already done in Almond on that front :-D!

The rather negative point, is exactly that almond requires you to give it the command to start the kernel, when you install. I can imagine two solutions.

Solution 1

Figure out the minimal set of classes to launch the kernel per scala version, and feed it those.

Solution 2

We're in metals, right? So if can we get the class path of the project that the user is working on (during import build step perhaps?), and install the kernel at that moment? If so then I think it's a slam dunk.

We

  1. Give almond that class path...
  2. Copy the JAR during installation to the directory, so it knows how to launch itself, and launch via -cp launcher.jar:THE_REST
  3. And we'll have a kernel per project... with the last problem being naming... as we'll have a few of them!

But at that point, the kernel has the project context - so it ought to able to use, whatever is on that class path? Which is kind of the big goal!

Here's an example of the important kernel.json file. If no other classes are bundled, the launcher itself is like 200Kb, so I'm relaxed about having a few of them! I believe the Jupiter spec will replace the "connection file" variable with whatever VSCode sends it.

 {
  "argv": [
    "java",
    "-jar",
    "/Users/simon/Library/Jupyter/kernels/scala/launcher.jar",
    "--display-name",
    "scala 3.0.2",
    "--connection-file",
    "{connection_file}"
  ],
  "display_name": "scala 3.0.2",
  "language": "scala"
}

@Quafadas
Copy link

Quafadas commented Jul 22, 2022

Rather a long message with some big leaps. Did it make sense? Still thinking.

@tgodzik
Copy link
Contributor Author

tgodzik commented Jul 22, 2022

Rather a long message with some big leaps. Did it make sense? Still thinking.

That makes sense!

Give almond that class path...
Copy the JAR during installation to the directory, so it knows how to launch itself, and launch via -cp launcher.jar:THE_REST
And we'll have a kernel per project... with the last problem being naming... as we'll have a few of them!

Actually, maybe what the launcher needs is the classpath of the project? Or classpath + kernel jar? But anyway we should be ok installing one kernel per project (lazily if possible) and using the project name (or target ID), which shouldn't be a big issue.

@Quafadas
Copy link

Or classpath + kernel jar?

I think it's this... or at least that'll be my next attempt!

I'll aim in this direction. I'm probably not going to look at this again for a few days, but would you be able to point me at

  1. a place in metals, where I can get at the project class path?
  2. a place in metals, where I can obtain a project name and / or target ID?

@tgodzik
Copy link
Contributor Author

tgodzik commented Jul 25, 2022

a place in metals, where I can get at the project class path?
a place in metals, where I can obtain a project name and / or target ID?

You can find an example of how we do it in https://github.com/scalameta/metals/blob/main/metals/src/main/scala/scala/meta/internal/metals/JavaInteractiveSemanticdb.scala#L55

@Quafadas
Copy link

@tgodzik Hi ... thankyou again for taking the time to write back and help... that looks perfect. still a bit delayed... and trying to find time a block of time to look at this again because I'm pretty excited about it...

if I get an actual proof of concept working, I guess that I will struggle to get it to the stage where it works properly inside metals... but first things first... baby steps!

@tgodzik
Copy link
Contributor Author

tgodzik commented Jul 26, 2022

If you have anything ready, you can send me a diff and I can offer some suggestions!

@Quafadas
Copy link

@tgodzik I haven't managed to spend enough time with this, but here is an initial (horrible) take on this. Which currently, doesn't work.

scalameta/metals@main...Quafadas:metals:jupyter_notebooks

scalameta/metals@main...Quafadas:metals:jupyter_notebooks

The idea is to add a command to metals "setup notebook kernel for this project". Which I hope would do exactly what is says on the tin.

I think that this is along the right lines, but has the following problems, in order of "I don't know how to solve this".

  1. Currently, this will crash for me, with a permission problem. Is it possible to get the shell runner permission to put things in user directories? Uncommenting this line; did install something, but I think we should use almonds build in directories, if possible. The author of almond knew a lot more about what he was doing, than I do! https://github.com/Quafadas/metals/blob/f21285b81ab8ea68662358751a872aba3b613000/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala#L2019
  2. Didn't mange to get a useful class path. So kernel doesn't start :-(. https://github.com/Quafadas/metals/blob/f21285b81ab8ea68662358751a872aba3b613000/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala#L19773.
    https://github.com/Quafadas/metals/blob/f21285b81ab8ea68662358751a872aba3b613000/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala#L2012
  3. Need to figure out the version of scala metals is using. https://github.com/Quafadas/metals/blob/f21285b81ab8ea68662358751a872aba3b613000/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala#L1989

Currently, it's all run synchronously. I believe that should be tidied up and put in a future context, but you can see, that my progress is not rapid. Unfortunately.

@tgodzik
Copy link
Contributor Author

tgodzik commented Aug 11, 2022

Currently, it's all run synchronously. I believe that should be tidied up and put in a future context, but you can see, that my progress is not rapid. Unfortunately.

No worries, it's great to hear that you managed to work on it! You could create a PR inside your repository and maybe this way we could comment and discuss specifics of your changes?

@Quafadas
Copy link

I've created a PR... I hope to the right place... both marked WIP

@tanishiking
Copy link
Member

tanishiking commented Aug 23, 2022

I wrote some words about how can we add language support for notebooks

TODOs

  • (easy) Enables metals-vscode to fire textDocument/*
  • Adjust the LSP request position to the actual source position (generated by almond / ammonite)
    • How to know the URI of generated Scala file for the ipynb? Can we get that information from running Kernel somehow?
    • How to map "uri of notebook cell" + "position inside the cell" to the position of generated Scala source file?

Enables metals-vscode to fire textDocument/*

According to notebookCellTextDocumentFilter of LSP 3.17.
We can enable vscode to fire textDocument/* for all jupyter-notebook, where the Cell's language is Scala.

diff --git a/src/extension.ts b/src/extension.ts
index 13b49f6f..32a99c36 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -291,6 +291,10 @@ function launchMetals(
       { scheme: "file", language: "java" },
       { scheme: "jar", language: "scala" },
       { scheme: "jar", language: "java" },
+      {
+        notebook: { scheme: "file", notebookType: "jupyter-notebook" },
+        language: "scala",
+      },
     ],
     synchronize: {
       configurationSection: "metals",

Like this:

[Trace - 01:09:28 PM] Received request 'textDocument/completion - (140)'
Params: {
  "context": {
    "triggerKind": 2,
    "triggerCharacter": "."
  },
  "textDocument": {
    "uri": "vscode-notebook-cell:/Users/tanishiking/src/github.com/tanishiking/scala3-playground/src/main/scala/Untitled-1.ipynb#W2sZmlsZQ%3D%3D"
  },
  "position": {
    "line": 0,
    "character": 2
  }
}

[Trace - 00:04:24 AM] Received request 'textDocument/hover - (107)'
Params: {
  "textDocument": {
    "uri": "vscode-notebook-cell:/Users/tanishiking/src/github.com/tanishiking/scala3-playground/src/main/scala/Untitled-1.ipynb#W1sZmlsZQ%3D%3D"
  },
  "position": {
    "line": 0,
    "character": 8
  },
  "range": {
    "start": {
      "line": 0,
      "character": 8
    },
    "end": {
      "line": 0,
      "character": 8
    }
  }
}

We can find the

  • uri is like "vscode-notebook-cell:/Users/tanishiking/src/github.com/tanishiking/scala3-playground/src/main/scala/Untitled-1.ipynb#W1sZmlsZQ%3D%3D" that point to a specific cell in the notebook Untitled-1.ipynb
    • fragment hash is the id of the cell (we can find it from *.ipynb file)
  • position is the position inside the cell

The problems here are

  • Each cell is not compilable, in order to properly analyze the notebook, we have to combine the notebook cells into a valid form.
  • Metals have to map the "uri of the cell" and "position in the cell" into the position in the combined Scala source.
    • It seems pretty tricky

How to "combine" notebook cells?

For combining the cells, LSP 3.17 provide a notebookDocumentSync feature (but I don't think it works for Metals).

The following change in scalameta/metals enables client to fire notebookDocument/did* (like didOpen, didChange and didClose)

change in scalameta/metals
diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala
index 00aa1d791f..35b0ea2dc9 100644
--- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala
+++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLanguageServer.scala
@@ -860,6 +860,16 @@ class MetalsLanguageServer(
             ServerCommands.all.map(_.id).asJava
           )
         )
+        val selector = new NotebookSelector()
+        selector.setNotebook(
+          JEither.forLeft[String, NotebookDocumentFilter]("*")
+        )
+        selector.setCells(List(new NotebookSelectorCell("scala")).asJava)
+        capabilities.setNotebookDocumentSync(
+          new NotebookDocumentSyncRegistrationOptions(
+            List(selector).asJava
+          )
+        )
         capabilities.setFoldingRangeProvider(true)
         capabilities.setSelectionRangeProvider(true)
         capabilities.setCodeLensProvider(new CodeLensOptions(false))

Those notifications send the content of the notebooks, and enable LSP servers track the content of notebooks

Example of didOpen
[Trace - 09:07:44 PM] Received notification 'notebookDocument/didOpen'
Params: {
  "notebookDocument": {
    "uri": "file:///Users/tanishiking/src/github.com/tanishiking/scala3-playground/src/main/scala/Untitled-1.ipynb",
    "notebookType": "jupyter-notebook",
    "version": 0,
    "cells": [
      {
        "kind": 2,
        "document": "vscode-notebook-cell:/Users/tanishiking/src/github.com/tanishiking/scala3-playground/src/main/scala/Untitled-1.ipynb#W0sZmlsZQ%3D%3D",
        "metadata": {
          "custom": {
            "metadata": {}
          }
        }
      },
      {
        "kind": 2,
        "document": "vscode-notebook-cell:/Users/tanishiking/src/github.com/tanishiking/scala3-playground/src/main/scala/Untitled-1.ipynb#W1sZmlsZQ%3D%3D",
        "metadata": {
          "custom": {
            "metadata": {}
          }
        }
      },
      {
        "kind": 2,
        "document": "vscode-notebook-cell:/Users/tanishiking/src/github.com/tanishiking/scala3-playground/src/main/scala/Untitled-1.ipynb#W2sZmlsZQ%3D%3D",
        "metadata": {
          "custom": {
            "metadata": {}
          }
        }
      }
    ],
    "metadata": {
      "custom": {
        "cells": [],
        "metadata": {
          "kernelspec": {
            "display_name": "Scala 2.13",
            "language": "scala",
            "name": "scala213"
          },
          "language_info": {
            "codemirror_mode": "text/x-scala",
            "file_extension": ".sc",
            "mimetype": "text/x-scala",
            "name": "scala",
            "nbconvert_exporter": "script",
            "version": "2.13.8"
          },
          "orig_nbformat": 4
        },
        "nbformat": 4,
        "nbformat_minor": 2
      },
      "indentAmount": " "
    }
  },
  "cellTextDocuments": [
    {
      "uri": "vscode-notebook-cell:/Users/tanishiking/src/github.com/tanishiking/scala3-playground/src/main/scala/Untitled-1.ipynb#W0sZmlsZQ%3D%3D",
      "languageId": "scala",
      "version": 1,
      "text": "val x \u003d 1"
    },
    {
      "uri": "vscode-notebook-cell:/Users/tanishiking/src/github.com/tanishiking/scala3-playground/src/main/scala/Untitled-1.ipynb#W1sZmlsZQ%3D%3D",
      "languageId": "scala",
      "version": 1,
      "text": "println(x)"
    },
    {
      "uri": "vscode-notebook-cell:/Users/tanishiking/src/github.com/tanishiking/scala3-playground/src/main/scala/Untitled-1.ipynb#W2sZmlsZQ%3D%3D",
      "languageId": "scala",
      "version": 1,
      "text": ""
    }
  ]
}

[Trace - 09:08:25 PM] Received notification 'notebookDocument/didChange'
Params: {
  "notebookDocument": {
    "version": 0,
    "uri": "file:///Users/tanishiking/src/github.com/tanishiking/scala3-playground/src/main/scala/Untitled-1.ipynb"
  },
  "change": {
    "cells": {
      "textContent": [
        {
          "document": {
            "uri": "vscode-notebook-cell:/Users/tanishiking/src/github.com/tanishiking/scala3-playground/src/main/scala/Untitled-1.ipynb#W2sZmlsZQ%3D%3D",
            "version": 2
          },
          "changes": [
            {
              "range": {
                "start": {
                  "line": 0,
                  "character": 0
                },
                "end": {
                  "line": 0,
                  "character": 0
                }
              },
              "rangeLength": 0,
              "text": "x"
            }
          ]
        }
      ]
    }
  }
}

However, it doesn't work for Metals for 2 reasons

  • Metals expect to have a file in the filesystem
    • Even we can track the content in memory, Metals basically treat the files on the filesytem as a ground truth
  • Being able to track the cell content doesn't make any sense with Almond / Ammonite
    • Because what Metals should compile is the converted Scala source file by Ammonite, tracking cell content doesn't help

How should we convert notebook into a Scala source then?

We should do the same thing as ScalaCli and Ammonite Script (though notebook would be even tricker)

How sourceAdjustment works for ScalaCli and Ammonite Script

sourceAdjustment for (SemanticDB) indexing should work if we properly registered the MappedSource for ipynb

How about Almond case?

  • We need to convert ipynb into a Ammonite generated Scala source, how can we map it?
    • It might be needed to run build server (by whom???) to map the ipynb URI to generated Scala source's URI.
    • Can we talk with running Kernel to get the URI?
  • How to convert the position in the cell, into a position in the generated Scala source?
    • not sure...

@Arthurm1
Copy link

@tanishiking I'm not familiar with how notebook cells work in LSP.

Will all LSP messages need to have their incoming URIs translated into a local filesystem version (or some more extensive mapping info)?
e.g. textdocument/didOpen didClose didSave hover codeLens documentSymbol etc.

And then will these local filesystem URIs have to be translated back into a notebook cell URI when the replies are sent?

@tanishiking
Copy link
Member

tanishiking commented Aug 23, 2022

@Arthurm1

Will all LSP messages need to have their incoming URIs translated into a local filesystem version (or some more extensive mapping info)?

Yes, I think all the LSP requests that requires semantic information from compiler / SemanticDB requires translation from incoming URI -> local filesystem version.

For example, consider we have the following two cells in a notebook.

// cell1
def add(a: Int, b: Int): Int = a + b

// cell2
add(1, 2)

cell2 itself isn't valid (both syntactically and semantically invalid). So, Almond / Ammonite translate the whole notebook into a combined compilable Scala source file (that is local filesystem version) (I guess). I think all the compiler related analysis should be done on that combined compilable Scala source file.

However, I don't think we need to support all the LSP requests / notifications for notebooks (for example, foldRange might not be helpful in notebook environment) :)


And then will these local filesystem URIs have to be translated back into a notebook cell URI when the replies are sent?

Yes, because LSP client doesn't recognize the "local filesystem URI". LSP server need to translate back to the notebook cell URI and position.

@Arthurm1
Copy link

@tanishiking I've had to do something similar here to allow for a simple jar filesystem and translate something like metalsfs:/some-source-jar.jar <-> file:///some-horrendously-long-path/some-source-jar.jar

See textDocument/implementation as an example

All messages get their URIs translated in and out but it's only file-to-file rather than file-to-multiple-cells.

If notebook cells need URIs translated on (nearly) every message then maybe we should consider the needs of this feature request and scalameta/metals#4114 together.

The other option for the jar file system is this PR which creates a java filesystem for the metalsfs URI scheme. The same could be done for a vscode-notebook-cell uri scheme.
This makes the code a lot tidier as no mapping of URIs is needed within the message handling as the java file system can now handle these URIs directly and AbsolutePath can wrap those Paths. I'm not sure how multiple cells would be mapped to one file.

I thought this option was a lot cleaner as it didn't interfere with the main codebase as much but my implementation was slow and I got bogged down in URLClassLoader

@Quafadas
Copy link

@tanishiking I'm not sure how relevant this is to the discussion, but I had a look at almond 0.13.1 (which is awesome) and following the instruction here https://jupyterlab-lsp.readthedocs.io/en/latest/Configuring.html with metals 0.11.8, produced this screenshot;

image

Does anyone know - is that screenshot proof that juypter is is talking to metals LSP? Or how I might be able to tell?

@tgodzik
Copy link
Contributor Author

tgodzik commented Aug 25, 2022

Och, that's interesting. I wonder if they translate all the cells to a single document automatically 🤔 Maybe we could copy over their solution?

@alexarchambault did you maybe work on that at any point?

@alexarchambault
Copy link

It looks like standard completion (the Ammonite one). LSP isn't involved.

@tanishiking
Copy link
Member

tanishiking commented Aug 26, 2022

Interesting! but jupyter-lsp shouldn't work with Metals out of the box because

  • jupyter-lsp concatenate notebook cells on the frontend which makes usually generates invalid Scala code
  • While jupyter-lsp create a concatenated virtual temporary file, the Metals server cannot recognize the file without build information.

the notebook cells are concatenated into a single temporary (“virtual”) document on the frontend, which is then sent to the backend,
...
as a workaround for some language servers requiring actual presence of the file on the filesystem (against the LSP spec, but common in some less advanced servers), our backend Jupyter server extension creates a temporary file on the file system (by default in the .virtual_documents directory); this is scheduled for deprecation,

https://jupyter.org/enhancement-proposals/72-language-server-protocol/language-server-protocol.html#reference-level-explanation

tanishiking added a commit to tanishiking/metals that referenced this issue Aug 31, 2022
Added a design document for scalameta/metals-feature-requests#236
This document is for
- design review the architecture before going further
- make sure we are on the same page about the goal, design, and
  requirements for this feature
@tanishiking
Copy link
Member

added design doc for this feature scalameta/metals#4434

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants