Skip to content

Commit

Permalink
New option --numeric to print indentation for emacs
Browse files Browse the repository at this point in the history
  • Loading branch information
gpetiot committed May 13, 2020
1 parent be33d02 commit dd4f310
Show file tree
Hide file tree
Showing 15 changed files with 404 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

+ Fix disabling with attributes on OCaml < 4.08 (#1322) (Etienne Millon)

#### New features

+ Return the indentation of a set of lines (new option `--numeric`) (#1207) (Guillaume Petiot)

### 0.14.2 (2020-05-11)

#### Changes
Expand Down
64 changes: 59 additions & 5 deletions bin/ocamlformat.ml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ let print_error conf opts ~input_name e =
Translation_unit.print_error ~debug:opts.Conf.debug ~quiet:conf.Conf.quiet
~input_name e

let check_line lines i =
if 1 <= i && i <= List.length lines then Ok ()
else Error (`Msg (Format.sprintf "Invalid line number %i." i))

open Result.Monad_infix

let run_action action opts =
match action with
| Conf.Inplace inputs ->
Expand All @@ -65,11 +71,59 @@ let run_action action opts =
Result.combine_errors_unit (List.map inputs ~f)
| In_out ({kind; file; name= input_name; conf}, output_file) -> (
let source = source_from_file file in
match format ?output_file ~kind ~input_name ~source conf opts with
| Ok s ->
to_output_file output_file s ;
Ok ()
| Error e -> Error [(fun () -> print_error conf opts ~input_name e)] )
match (opts.numeric, output_file) with
| Some (low, high), None -> (
let opts = {opts with format_invalid_files= true} in
match format ~kind ~input_name ~source conf opts with
| Ok fmted -> (
match String.split_lines source with
| [] ->
Error
[ (fun () ->
Format.fprintf Format.err_formatter
"Source can not be split into lines.\n%!") ]
| lines ->
let fmted_src = Source.create fmted in
let lines = List.map ~f:String.strip lines in
let on_line l =
check_line lines l
>>= fun () ->
let line_nb = l - 1 in
let pattern = List.nth_exn lines line_nb in
Source.substr_smaller_index fmted_src ~pattern ~line_nb
~lines
>>= fun idx ->
Source.indentation_of_index fmted_src idx
>>| fun indent ->
Stdio.print_endline (Int.to_string indent)
in
if low < high then
let rec loop i =
if i > high then Ok ()
else
match on_line i with
| Ok () -> loop (i + 1)
| Error (`Msg e) ->
Error
[ (fun () ->
Format.fprintf Format.err_formatter "%s\n%!"
e) ]
in
loop low
else
Result.map_error (on_line low) ~f:(fun (`Msg e) ->
[ (fun () ->
Format.fprintf Format.err_formatter "%s\n%!" e) ])
)
| Error e -> Error [(fun () -> print_error conf opts ~input_name e)]
)
| _ -> (
match format ?output_file ~kind ~input_name ~source conf opts with
| Ok s ->
to_output_file output_file s ;
Ok ()
| Error e -> Error [(fun () -> print_error conf opts ~input_name e)]
) )
| Check inputs ->
let f {Conf.kind; name= input_name; file; conf} =
let source = source_from_file file in
Expand Down
54 changes: 54 additions & 0 deletions emacs/ocamlformat.el
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,60 @@ function."
(delete-file bufferfile)
(delete-file outputfile)))

(defun ocamlformat-file-to-string (file)
(replace-regexp-in-string
"\n$" ""
(with-temp-buffer (insert-file-contents file) (buffer-string))))

(defun ocamlformat-args (start-line end-line)
(append
(list "--numeric"
"--lines" (format "%d-%d" start-line end-line))))

(defun ocamlformat-region (start end)
(interactive "r")
(let*
((start-line (line-number-at-pos start))
(end-line (line-number-at-pos end))
(errfile (make-temp-name (concat temporary-file-directory "ocamlformat-error")))
(indents-str
(with-output-to-string
(if (/= 0
(apply 'call-process-region
(point-min) (point-max) ocamlformat-command nil
(list standard-output errfile) nil
(ocamlformat-args start-line end-line)))
(error "Can't indent: %s returned failure" ocamlformat-command))))
(indents (mapcar 'string-to-number (split-string indents-str))))
(when (file-exists-p errfile)
(message (ocamlformat-file-to-string errfile))
(delete-file errfile))
(save-excursion
(goto-char start)
(mapcar
#'(lambda (indent) (indent-line-to indent) (forward-line))
indents))))

(defun ocamlformat-line ()
(interactive nil)
(ocamlformat-region (point) (point)))

;;;###autoload
(defun ocamlformat-setup-indent ()
(interactive nil)
(set (make-local-variable 'indent-line-function) #'ocamlformat-line))

;; (set (make-local-variable 'indent-region-function) #'ocamlformat-region)

;;;###autoload
(defun ocamlformat-caml-mode-setup ()
(ocamlformat-setup-indent)
(local-unset-key "\t")) ;; caml-mode rebinds TAB !

(add-hook 'tuareg-mode-hook 'ocamlformat-setup-indent t)

(add-hook 'caml-mode-hook 'ocamlformat-caml-mode-setup t)

(provide 'ocamlformat)

;;; ocamlformat.el ends here
25 changes: 23 additions & 2 deletions lib/Conf.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,20 @@ let name =
mk ~default
Arg.(value & opt (some string) default & info ["name"] ~doc ~docs ~docv)

let numeric =
let doc =
"Instead of re-indenting the file, output one integer per line \
representing the indentation value, printing as many values as lines \
in the range between lines X and Y (included)."
in
let default = None in
let docv = "X-Y" in
mk ~default
Arg.(
value
& opt (some (pair ~sep:'-' int int)) default
& info ["numeric"] ~doc ~docs ~docv)

let ocp_indent_options =
let unsupported ocp_indent = (ocp_indent, ([], "")) in
let alias ocp_indent ocamlformat =
Expand Down Expand Up @@ -2267,7 +2281,11 @@ let make_action ~enable_outside_detected_project ~root action inputs =
Ok (Check (List.map files ~f))
| `Check, `Stdin (name, kind) -> Ok (Check [make_stdin ?name kind])
type opts = {debug: bool; margin_check: bool; format_invalid_files: bool}
type opts =
{ debug: bool
; margin_check: bool
; format_invalid_files: bool
; numeric: (int * int) option }
let validate () =
let root =
Expand Down Expand Up @@ -2295,7 +2313,10 @@ let validate () =
match !format_invalid_files with Some `Auto -> true | _ -> false
in
let opts =
{debug= !debug; margin_check= !margin_check; format_invalid_files}
{ debug= !debug
; margin_check= !margin_check
; format_invalid_files
; numeric= !numeric }
in
`Ok (action, opts)
Expand Down
3 changes: 2 additions & 1 deletion lib/Conf.mli
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ type opts =
{ debug: bool (** Generate debugging output if true. *)
; margin_check: bool
(** Check whether the formatted output exceeds the margin. *)
; format_invalid_files: bool }
; format_invalid_files: bool
; numeric: (int * int) option }

val action : unit -> (action * opts) Cmdliner.Term.result
(** Formatting action: input type and source, and output destination. *)
Expand Down
105 changes: 105 additions & 0 deletions lib/Source.ml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,111 @@ type t = string

let create s = s

(* 0 <= line_nb < List.length lines *)
let rec aux ?(check_prev = true) ?(check_next = true) t ~pattern ~line_nb
~lines =
let idx_result idx =
if String.is_empty pattern then
if String.is_empty (List.nth_exn lines line_nb) then Some (idx, 0)
else None
else Some (idx, String.length pattern)
in
let prev_index l x =
let line_nb = l - 1 in
let pattern = List.nth_exn lines line_nb in
let check_next = false in
match aux t ~check_prev ~check_next ~pattern ~line_nb ~lines with
| Some (p, _) -> if p < x then idx_result x else None
| None -> None
in
let next_index l x =
let line_nb = l + 1 in
let pattern = List.nth_exn lines line_nb in
let check_prev = false in
match aux t ~check_prev ~check_next ~pattern ~line_nb ~lines with
| Some (n, _) -> if n > x then idx_result x else None
| None -> None
in
if String.length pattern > String.length t then None
else
match String.substr_index_all ~may_overlap:true t ~pattern with
| [] ->
(* pattern has not been found, we try to find a substring of it,
heuristic: cutting right (might consider another) *)
if String.is_empty pattern then None
else
let pattern =
Caml.String.sub pattern 0 (String.length pattern - 1)
in
aux ~check_prev ~check_next t ~pattern ~line_nb ~lines
| [idx] ->
Stdio.print_endline ("single match= " ^ Int.to_string idx);
idx_result idx
| h :: t -> (
Stdio.print_endline "many matches";
List.iter (h :: t) ~f:(fun x -> Stdio.print_endline (Int.to_string x));
(* multiple candidates found, we have to pick the right one *)
match
(check_prev && line_nb > 0, check_next && line_nb < List.length lines - 1)
with
| false, false -> None
| true, false ->
List.fold_left t ~init:(prev_index line_nb h) ~f:(fun acc x ->
match (prev_index line_nb x, acc) with
| Some (idx, n), Some (idx', n') ->
if n > n' then Some (idx, n) else Some (idx', n')
| Some (idx, n), None -> Some (idx, n)
| None, _ -> acc)
| false, true ->
List.fold_left t ~init:(next_index line_nb h) ~f:(fun acc x ->
match (next_index line_nb x, acc) with
| Some (idx, n), Some (idx', n') ->
if n > n' then Some (idx, n) else Some (idx', n')
| Some (idx, n), None -> Some (idx, n)
| None, _ -> acc)
| true, true ->
let matches x =
match (prev_index line_nb x, next_index line_nb x) with
| Some (_, n), Some (_, n') -> Some (x, n + n')
| _ -> None
in
List.fold_left t ~init:(matches h) ~f:(fun acc x ->
match (matches x, acc) with
| Some (idx, n), Some (idx', n') ->
if n > n' then Some (idx, n) else Some (idx', n')
| Some (idx, n), None -> Some (idx, n)
| None, _ -> acc) )

let substr_smaller_index t ~pattern ~line_nb ~lines =
match aux t ~pattern ~line_nb ~lines with
| Some (i, _) -> Ok i
| None ->
Error
(`Msg (Format.sprintf "Could not find index of pattern %S." pattern))

let indentation_of_index_opt t idx =
if idx < 0 then None
else
match String.split_lines t with
| [] -> None
| lines ->
let rec aux wc_acc = function
| [] -> None
| h :: t ->
if idx < wc_acc + String.length h then
Some (String.length h - String.(length (lstrip h)))
else aux (wc_acc + String.length h) t
in
aux 0 lines

let indentation_of_index t idx =
match indentation_of_index_opt t idx with
| Some i -> Ok i
| None ->
Error
(`Msg
(Format.sprintf "Could not compute indentation of index %i." idx))

let position_before t (pos : Lexing.position) =
let is_closing c =
Char.equal c ')' || Char.equal c ']' || Char.equal c '}'
Expand Down
15 changes: 15 additions & 0 deletions lib/Source.mli
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ type t

val create : string -> t

val substr_smaller_index :
t
-> pattern:string
-> line_nb:int
-> lines:string list
-> (int, [`Msg of string]) Result.t
(** [substr_smaller_index t ~pattern ~line_nb ~lines] returns the smaller
index in [t] where the bigger substring of [t] (line number [line_nb])
starts. [lines] are used to disambiguate the result when multiple
occurrences are found in [t]. *)

val indentation_of_index : t -> int -> (int, [`Msg of string]) Result.t
(** [indentation_of_index t idx] returns the indentation of the line
containing the character of index [idx] in [t]. *)

val empty_line_between : t -> Lexing.position -> Lexing.position -> bool
(** [empty_line_between t p1 p2] is [true] if there is an empty line between
[p1] and [p2]. The lines containing [p1] and [p2] are not considered
Expand Down
5 changes: 5 additions & 0 deletions ocamlformat-help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,11 @@ OPTIONS
Do not check that the version matches the one specified in
.ocamlformat.

--numeric=X-Y
Instead of re-indenting the file, output one integer per line
representing the indentation value, printing as many values as
lines in the range between lines X and Y (included).

-o DST, --output=DST
Output file. Mutually exclusive with --inplace. Write to stdout if
omitted.
Expand Down
26 changes: 26 additions & 0 deletions test/cli/dune
Original file line number Diff line number Diff line change
Expand Up @@ -520,3 +520,29 @@
(alias runtest)
(action
(diff env_unknown_value.expected env_unknown_value.output)))

(rule
(targets line_indent_1.output)
(deps .ocamlformat)
(action
(with-outputs-to
%{targets}
(system "%{bin:ocamlformat} %{dep:sample/c.ml} --numeric 1-1"))))

(alias
(name runtest)
(action
(diff line_indent_1.expected line_indent_1.output)))

(rule
(targets line_indent_3_7.output)
(deps .ocamlformat)
(action
(with-outputs-to
%{targets}
(system "%{bin:ocamlformat} %{dep:sample/c.ml} --numeric 3-7"))))

(alias
(name runtest)
(action
(diff line_indent_3_7.expected line_indent_3_7.output)))
1 change: 1 addition & 0 deletions test/cli/line_indent_1.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0
5 changes: 5 additions & 0 deletions test/cli/line_indent_3_7.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
0
4
4
2
2
7 changes: 7 additions & 0 deletions test/cli/sample/c.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
let foooooooooooo = let foooooooo = foooooooooooo in foooooooooooo

let foooooooooooo = let foooooooooooooo =
let woooooooooooooooo = koooooooooooooooo in
baaaaaar
in
hooohoooo
Loading

0 comments on commit dd4f310

Please sign in to comment.