Skip to content

Commit

Permalink
Support notebook-scoped VS Code actions (#351)
Browse files Browse the repository at this point in the history
## Summary

VS Code added support for `notebook.*`-scoped code actions
(https://code.visualstudio.com/api/references/vscode-api#CodeActionKind),
which are intended to run over the entire document, unlike the actions
that omit the `notebook.` namespace, which instead only operate over
individual cells. The latter are problematic for actions that need
global context, e.g., our unused imports rules.

When the `notebook.*` actions are triggered, VS Code sends down the
first cell (see: microsoft/vscode#193120) as
the URI. This PR adds handlers for the actions and logic to use the
notebook, rather than the cell.

Closes #320.

## Test Plan

Used the following `settings.json`:

```json
{
  "[python]": {
      "editor.defaultFormatter": "charliermarsh.ruff",
      "editor.codeActionsOnSave": {
        "source.organizeImports.ruff": true
      }
    },
    "notebook.codeActionsOnSave": {
      "notebook.source.fixAll": true
    }
}
```

Verified that imports used across cells were not removed (whereas
`"source.fixAll": true` _did_ cause imports to be removed).
  • Loading branch information
charliermarsh authored Jan 5, 2024
1 parent 44b52ad commit 03fbbdd
Showing 1 changed file with 56 additions and 14 deletions.
70 changes: 56 additions & 14 deletions ruff_lsp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,22 @@ class VersionModified(NamedTuple):
# "--stdin-filename",
]

# Standard code action kinds, scoped to Ruff.
SOURCE_FIX_ALL_RUFF = f"{CodeActionKind.SourceFixAll.value}.ruff"
SOURCE_ORGANIZE_IMPORTS_RUFF = f"{CodeActionKind.SourceOrganizeImports.value}.ruff"

# Notebook code action kinds.
NOTEBOOK_SOURCE_FIX_ALL = f"notebook.{CodeActionKind.SourceFixAll.value}"
NOTEBOOK_SOURCE_ORGANIZE_IMPORTS = (
f"notebook.{CodeActionKind.SourceOrganizeImports.value}"
)

# Notebook code action kinds, scoped to Ruff.
NOTEBOOK_SOURCE_FIX_ALL_RUFF = f"notebook.{CodeActionKind.SourceFixAll.value}.ruff"
NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF = (
f"notebook.{CodeActionKind.SourceOrganizeImports.value}.ruff"
)


###
# Document
Expand Down Expand Up @@ -804,11 +820,19 @@ class LegacyFix(TypedDict):
TEXT_DOCUMENT_CODE_ACTION,
CodeActionOptions(
code_action_kinds=[
# Standard code action kinds.
CodeActionKind.QuickFix,
CodeActionKind.SourceFixAll,
CodeActionKind.SourceOrganizeImports,
f"{CodeActionKind.SourceFixAll.value}.ruff",
f"{CodeActionKind.SourceOrganizeImports.value}.ruff",
# Standard code action kinds, scoped to Ruff.
SOURCE_FIX_ALL_RUFF,
SOURCE_ORGANIZE_IMPORTS_RUFF,
# Notebook code action kinds.
NOTEBOOK_SOURCE_FIX_ALL,
NOTEBOOK_SOURCE_ORGANIZE_IMPORTS,
# Notebook code action kinds, scoped to Ruff.
NOTEBOOK_SOURCE_FIX_ALL_RUFF,
NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF,
],
resolve_provider=True,
),
Expand All @@ -821,26 +845,44 @@ async def code_action(params: CodeActionParams) -> list[CodeAction] | None:
first, and if there's no cell with the given URI, it will fallback to the text
document.
"""

def document_from_kind(uri: str, kind: str) -> Document:
if kind in (
# For `notebook`-scoped actions, use the Notebook Document instead of
# the cell, despite being passed the URI of the first cell.
# See: https://github.com/microsoft/vscode/issues/193120
NOTEBOOK_SOURCE_FIX_ALL,
NOTEBOOK_SOURCE_ORGANIZE_IMPORTS,
NOTEBOOK_SOURCE_FIX_ALL_RUFF,
NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF,
):
return Document.from_uri(uri)
else:
return Document.from_cell_or_text_uri(uri)

document_path = _uri_to_fs_path(params.text_document.uri)
if utils.is_stdlib_file(document_path):
# Don't format standard library files.
# Publishing empty list clears the entry.
return None

settings = _get_settings_by_document(document_path)

if settings["organizeImports"]:
# Generate the "Ruff: Organize Imports" edit
for kind in (
CodeActionKind.SourceOrganizeImports,
f"{CodeActionKind.SourceOrganizeImports.value}.ruff",
SOURCE_ORGANIZE_IMPORTS_RUFF,
NOTEBOOK_SOURCE_ORGANIZE_IMPORTS,
NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF,
):
if (
params.context.only
and len(params.context.only) == 1
and kind in params.context.only
):
workspace_edit = await _fix_document_impl(
Document.from_cell_or_text_uri(params.text_document.uri),
document_from_kind(params.text_document.uri, kind),
only=["I001", "I002"],
)
if workspace_edit:
Expand All @@ -860,15 +902,17 @@ async def code_action(params: CodeActionParams) -> list[CodeAction] | None:
# Generate the "Ruff: Fix All" edit.
for kind in (
CodeActionKind.SourceFixAll,
f"{CodeActionKind.SourceFixAll.value}.ruff",
SOURCE_FIX_ALL_RUFF,
NOTEBOOK_SOURCE_FIX_ALL,
NOTEBOOK_SOURCE_FIX_ALL_RUFF,
):
if (
params.context.only
and len(params.context.only) == 1
and kind in params.context.only
):
workspace_edit = await _fix_document_impl(
Document.from_cell_or_text_uri(params.text_document.uri)
document_from_kind(params.text_document.uri, kind)
)
if workspace_edit:
return [
Expand Down Expand Up @@ -1063,17 +1107,14 @@ async def resolve_code_action(params: CodeAction) -> CodeAction:

settings = _get_settings_by_document(document.path)

if settings["organizeImports"] and params.kind in (
CodeActionKind.SourceOrganizeImports,
f"{CodeActionKind.SourceOrganizeImports.value}.ruff",
if (
settings["organizeImports"]
and params.kind == CodeActionKind.SourceOrganizeImports
):
# Generate the "Ruff: Organize Imports" edit
params.edit = await _fix_document_impl(document, only=["I001", "I002"])

elif settings["fixAll"] and params.kind in (
CodeActionKind.SourceFixAll,
f"{CodeActionKind.SourceFixAll.value}.ruff",
):
elif settings["fixAll"] and params.kind == CodeActionKind.SourceFixAll:
# Generate the "Ruff: Fix All" edit.
params.edit = await _fix_document_impl(document)

Expand Down Expand Up @@ -1125,7 +1166,8 @@ async def apply_format(arguments: tuple[TextDocument]):
async def format_document(params: DocumentFormattingParams) -> list[TextEdit] | None:
# For a Jupyter Notebook, this request can only format a single cell as the
# request itself can only act on a text document. A cell in a Notebook is
# represented as a text document.
# represented as a text document. The "Notebook: Format notebook" action calls
# this request for every cell.
document = Document.from_cell_or_text_uri(params.text_document.uri)

result = await _run_format_on_document(document)
Expand Down

0 comments on commit 03fbbdd

Please sign in to comment.