Skip to content

Support textDocument/rangeFormatting#1591

Merged
xvw merged 8 commits intoocaml:masterfrom
WardBrian:range-format
Mar 6, 2026
Merged

Support textDocument/rangeFormatting#1591
xvw merged 8 commits intoocaml:masterfrom
WardBrian:range-format

Conversation

@WardBrian
Copy link
Copy Markdown
Contributor

This PR implements the textDocument/rangeFormatting request for ocaml-lsp-server.

The basic idea is this:

  1. For a given selection, we split the text of the document into three parts: before, the piece we want to format, and after.
  2. We invoke ocamlformat on the piece we want to format as if it was its own file*
  3. We paste it back together with before and after and generate the TextEdits from this result.

The * in step two is because, if we want nice results, we need to modify our call to ocamlformat in the following ways:

  1. Because our selection may be at some deep indentation level in the surrounding code, special care is needed to preserve this. The approach I've taken here is to

    • adjust the margin to be smaller in the resulting call and
    • pad the result at the newline boundaries

    c.f. Feature request: Ability to request starter indentation level ocaml-ppx/ocamlformat#2773

  2. If the snippet you're highlighting is a let ... in where you've left of the in, some ocamlformat configs will desire to add a ;; at the end, which is annoying and breaks the surrounding code, so I special case this.

This PR closes ocamllabs/vscode-ocaml-platform#1891

@sidkshatriya
Copy link
Copy Markdown
Contributor

Why not ocamlformat the whole document but don't bring in changes to the before and after areas of the document. This approach may allow selecting arbitrary regions for range formatting and is more flexible ?

@WardBrian
Copy link
Copy Markdown
Contributor Author

Why not ocamlformat the whole document but don't bring in changes to the before and after areas of the document. This approach may allow selecting arbitrary regions for range formatting and is more flexible ?

I see two problems with this:

  1. Determining the start/end inside the post-formatting document seems hard, as they could be arbitrarily far away from where they were pre-formatting
  2. I am interested in using this feature for situations where the entire document is not valid OCaml; either because it is in a file that has syntax errors elsewhere, or if the things I am formatting are ocaml snippets in a larger file, like the semantic actions in a .mll file

@sidkshatriya
Copy link
Copy Markdown
Contributor

The problem with your suggested approach is also that you need to be conscious of whether the snippet you select constitutes a valid ocaml file if it were "copy + pasted" into a separate ocaml file. Sometimes you only want to range format say the inside of a for loop -- that snippet may not be valid top level code.

Typically when you are editing a file in an editor, the document as a whole enters invalid syntax occasionally but when you're asking for formatting it is not too big a deal to need provide a syntactically correct ocaml file.

Determining the start/end inside the post-formatting document seems hard, as they could be arbitrarily far away from where they were pre-formatting

In your logic you could add a ocaml comment (* SNIPPET START *) and (* SNIPPET END *) before sending to ocamlformat and then find-and-replace just the snippet when the whole document comes back ? Before returning back document to the user, strip out the (* SNIPPET START *) and (* SNIPPET END *).

@xvw
Copy link
Copy Markdown
Collaborator

xvw commented Mar 5, 2026

At least, I think that having the ability to format one range is very useful (and easier to maintain at the editor level, mostly because it fits with the LSP specification).
We can have, later, a reflection about how to manage diff after formating.
Ill review the pr today btw.

Copy link
Copy Markdown
Collaborator

@xvw xvw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some nitpicking comments (feel free to resolve them if you do not agree, I do not have strong opinion) but I think 3 things are missing:

  • Some tests to illustrate the behaviour of the command (in e2e-new)
  • Extracting the behaviour of the contextual formatting and testing it (that can be useful for formatting in context of other command like destruct and construct for example)
  • A change entry :)

Thanks a lot for the solid contribution!

Comment on lines +230 to +266
let contents = Document.source doc |> Msource.text in
let start, stop = Text_document.absolute_range (Document.tdoc doc) range in
(* basic idea:
- slice out the range to be formatted, send to ocamlformat
- whitespace pad the start of lines in the reply based on the selection start
- stitch everything back together. *)
let prefix, to_format, suffix =
( String.sub contents ~pos:0 ~len:start
, String.sub contents ~pos:start ~len:(stop - start)
, String.sub contents ~pos:stop ~len:(String.length contents - stop) )
in
let args = args formatter in
let args =
(* if we're formatting the start of a [let ... in] construct,
don't emit [;;] before the [in]! *)
let next =
String.trim suffix ~drop:(function
| ' ' | '\n' | '\r' | '\t' -> true
| _ -> false)
in
if String.is_prefix ~prefix:"in" next || String.is_prefix ~prefix:";;" next
then "--let-binding-spacing=compact" :: args
else args
in
let column_offset = range.start.character in
let pad s =
let padding = Bytes.make column_offset ' ' |> Bytes.to_string in
String.concat ~sep:"\n" (List.map (String.split_lines s) ~f:(( ^ ) padding))
|> String.trim ~drop:(( = ) ' ')
in
let open Fiber.O in
let* margin = compute_modified_margin binary cancel column_offset formatter in
let args = margin :: args in
let+ r = exec cancel binary args to_format in
Result.map r ~f:(fun { stdout = formatted; _ } ->
let to_ = prefix ^ pad formatted ^ suffix in
Diff.edit ~from:contents ~to_)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function should be extracted/exported (and unit tested) to make it easier to reuse and maintain. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure what function you're referring to here -- the padding function?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole manipulation on formatted fragment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if the new format_snippet function is what you had in mind. If so, I can start adding separate unit tests.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it is nice! Thanks!

@xvw
Copy link
Copy Markdown
Collaborator

xvw commented Mar 5, 2026

Since you add a new capability, you need to upgrade the test-suite (make tests + make promote if diff looks good).

@WardBrian
Copy link
Copy Markdown
Contributor Author

@xvw I would like some additional guidance on what you meant by extracting the contextual formatting capability (mainly, where you see the cut point in the current code/what the ideal signature of the extracted function would be, in your eyes)

Other than that, I believe I have addressed your comments. For the e2e-new testing, I also ported the old e2e/__tests__/textDocument-formatting.test.ts to OCaml first, both to make sure I was understanding the test system and to then give me some helper functions for use in the range_formatting file.

@xvw xvw merged commit a35513c into ocaml:master Mar 6, 2026
4 of 6 checks passed
@xvw
Copy link
Copy Markdown
Collaborator

xvw commented Mar 6, 2026

Thanks a lot!

@WardBrian WardBrian deleted the range-format branch March 6, 2026 15:14
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

Successfully merging this pull request may close these issues.

Format selection (not whole document)

3 participants