Skip to content

Instantly share code, notes, and snippets.

@tanishiking
Last active August 23, 2022 07:00
Show Gist options
  • Save tanishiking/a4ad74cb012d9bf62c8a9d379b02cf63 to your computer and use it in GitHub Desktop.
Save tanishiking/a4ad74cb012d9bf62c8a9d379b02cf63 to your computer and use it in GitHub Desktop.
  • (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...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment