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 Oct 12, 2020
1 parent 42135bf commit a59e3c6
Show file tree
Hide file tree
Showing 15 changed files with 549 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@

#### New features

+ Line/region indentation feature (emacs integration) (#1207, @gpetiot)

### 0.15.0 (2020-08-06)

#### Changes
Expand Down
12 changes: 12 additions & 0 deletions bin/ocamlformat.ml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ let run_action action opts =
| Error e -> Error (fun () -> print_error conf opts ~input_name e)
in
Result.combine_errors_unit (List.map inputs ~f)
| In_out ({kind= Conf.Kind k; file; name= input_name; conf}, None)
when Option.is_some opts.numeric -> (
let source = source_from_file file in
let range = Option.value_exn opts.numeric in
match
Translation_unit.indentation k ~input_name ~source ~range conf opts
with
| Ok indents ->
List.iter indents ~f:(fun i ->
Stdio.print_endline (Int.to_string i)) ;
Ok ()
| Error e -> Error [(fun () -> print_error conf opts ~input_name e)] )
| 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
Expand Down
88 changes: 88 additions & 0 deletions emacs/ocamlformat.el
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,94 @@ function."
(delete-file bufferfile)
(delete-file outputfile)))

(defun ocamlformat-args (name start-line end-line)
(let*
((margin-args
(cond
((equal ocamlformat-margin-mode 'window)
(list "--margin" (number-to-string (window-body-width))))
((equal ocamlformat-margin-mode 'fill)
(list "--margin" (number-to-string fill-column)))
(t
'())))
(enable-args
(cond
((equal ocamlformat-enable 'disable)
(list "--disable"))
((equal ocamlformat-enable 'enable-outside-detected-project)
(list "--enable-outside-detected-project"))
(t
'())))
(extension-args
(cond
((eq ocamlformat-file-kind 'implementation)
(list "--impl"))
((eq ocamlformat-file-kind 'interface)
(list "--intf")))))
(append margin-args enable-args extension-args
(list
"-"
"--name" name
"--numeric" (format "%d-%d" start-line end-line)))))

(defun ocamlformat-region (start end)
(interactive "r")
(let*
((ext (file-name-extension buffer-file-name t))
(bufferfile (file-truename (make-temp-file "ocamlformat" nil ext)))
(errorfile (file-truename (make-temp-file "ocamlformat" nil ext)))
(errbuf
(cond
((eq ocamlformat-show-errors 'buffer)
(get-buffer-create "*compilation*"))
((eq ocamlformat-show-errors 'echo)
(get-buffer-create "*OCamlFormat stderr*"))))
(start-line (line-number-at-pos start))
(end-line (line-number-at-pos end))
(indents-str
(with-output-to-string
(if (/= 0
(apply 'call-process-region
(point-min) (point-max) ocamlformat-command nil
(list standard-output errorfile) nil
(ocamlformat-args buffer-file-name start-line end-line)))
(progn
(if errbuf
(progn
(with-current-buffer errbuf
(setq buffer-read-only nil)
(erase-buffer))
(ocamlformat--process-errors
(buffer-file-name) bufferfile errorfile errbuf)))
(message "Could not apply ocamlformat")))))
(indents (mapcar 'string-to-number (split-string indents-str))))
(save-excursion
(goto-char start)
(mapcar
#'(lambda (indent) (indent-line-to indent) (forward-line))
indents))
(delete-file errorfile)
(delete-file bufferfile)))

(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
31 changes: 29 additions & 2 deletions lib/Conf.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,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 @@ -2193,7 +2207,17 @@ 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 default_opts =
{ debug= false
; margin_check= false
; format_invalid_files= false
; numeric= None }
let validate () =
let root =
Expand Down Expand Up @@ -2221,7 +2245,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
11 changes: 10 additions & 1 deletion lib/Conf.mli
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,21 @@ type action =
(** Check whether the input files already are formatted. *)
| Print_config of t (** Print the configuration and exit. *)

val conventional_profile : t

val ocamlformat_profile : t

val janestreet_profile : t

(** Options changing the tool's behavior *)
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 default_opts : opts

val action : unit -> (action * opts) Cmdliner.Term.result
(** Formatting action: input type and source, and output destination. *)
Expand Down
124 changes: 124 additions & 0 deletions lib/Indent.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
(**************************************************************************)
(* *)
(* OCamlFormat *)
(* *)
(* Copyright (c) Facebook, Inc. and its affiliates. *)
(* *)
(* This source code is licensed under the MIT license found in *)
(* the LICENSE file in the root directory of this source tree. *)
(* *)
(**************************************************************************)

open Migrate_ast
open Result.Monad_infix

let rec loc_of_line loctree locs line =
match locs with
| [] -> None
| (h : Location.t) :: t ->
if h.loc_start.pos_lnum = line then Some h
else if h.loc_start.pos_lnum <= line && line <= h.loc_end.pos_lnum then
match Loc_tree.children loctree h with
| [] -> Some h
| children -> (
match loc_of_line loctree children line with
| Some loc -> Some loc
| None -> Some h )
else loc_of_line loctree t line

let matching_loc loc locs locs' =
match List.zip locs locs' with
| Ok assoc -> (
let equal x y = Location.compare x y = 0 in
match List.Assoc.find assoc ~equal loc with
| Some loc -> Ok loc
| None ->
Error (`Msg "Cannot find matching location in formatted output.") )
| Unequal_lengths ->
Error (`Msg "Cannot match pre-post formatting locations.")

let indentation_of_line l = String.(length l - length (lstrip l))

let indentation_1_line ?prev (loctree, locs) (_, locs') formatted_src nlines
~line =
if line = nlines + 1 then Ok 0
else
match loc_of_line loctree locs line with
| Some loc -> (
matching_loc loc locs locs'
>>= fun (loc' : Location.t) ->
let line_nb = loc'.loc_start.pos_lnum - 1 in
let line' =
List.nth_exn (String.split_lines formatted_src) line_nb
in
let indent = indentation_of_line line' in
match prev with
| Some (prev_indent, prev_line)
when indent = prev_indent || indent = 0 -> (
(* in case this is a line that is split but could fit on a single
line, consecutive lines will have the same indentation, we try
to infer some artificial indentation here, even though it will
be squeezed together and fit on a single line when the whole
file is reformatted *)
match
Source.infer_indent_from_prev_line (Source.create prev_line)
with
| Some i -> Ok (prev_indent + i)
| None -> Ok indent )
| _ -> Ok indent )
| None -> Ok 0

let indentation_1_line_fallback ?prev src nlines ~line =
if line = nlines + 1 then Ok 0
else
let line_nb = line - 1 in
let line' = List.nth_exn (String.split_lines src) line_nb in
let indent = indentation_of_line line' in
match prev with
| Some (prev_indent, prev_line) -> (
match Source.infer_indent_from_prev_line (Source.create prev_line) with
| Some i -> Ok (prev_indent + i)
| None -> Ok indent )
| _ -> Ok indent

type 'a parsed = {ast: 'a; source: string}

let indent_from_locs fragment ~unformatted:{ast; source}
~formatted:{ast= formatted_ast; source= formatted_source} ~lines
~range:(low, high) =
let nlines = List.length lines in
let prev =
Option.map
(List.nth lines (low - 2))
~f:(fun line -> (indentation_of_line line, line))
in
let locs = Loc_tree.of_ast fragment ast (Source.create source) in
let locs' =
Loc_tree.of_ast fragment formatted_ast (Source.create formatted_source)
in
let rec aux ?prev acc i =
if i > high then Ok (List.rev acc)
else
indentation_1_line ?prev locs locs' formatted_source nlines ~line:i
>>= fun indent ->
let line = Option.value (List.nth lines (i - 1)) ~default:"" in
aux ~prev:(indent, line) (indent :: acc) (i + 1)
in
aux ?prev [] low

let indent_from_lines ~source ~lines ~range:(low, high) =
let nlines = List.length lines in
let prev =
Option.map
(List.nth lines (low - 2))
~f:(fun line -> (indentation_of_line line, line))
in
let rec aux ?prev acc i =
if i > high then Ok (List.rev acc)
else
indentation_1_line_fallback ?prev source nlines ~line:i
>>= fun indent ->
let line = Option.value (List.nth lines (i - 1)) ~default:"" in
aux ~prev:(indent, line) (indent :: acc) (i + 1)
in
aux ?prev [] low
26 changes: 26 additions & 0 deletions lib/Indent.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
(**************************************************************************)
(* *)
(* OCamlFormat *)
(* *)
(* Copyright (c) Facebook, Inc. and its affiliates. *)
(* *)
(* This source code is licensed under the MIT license found in *)
(* the LICENSE file in the root directory of this source tree. *)
(* *)
(**************************************************************************)

type 'a parsed = {ast: 'a; source: string}

val indent_from_locs :
'a Migrate_ast.Mapper.fragment
-> unformatted:'a parsed
-> formatted:'a parsed
-> lines:string list
-> range:int * int
-> (int list, [`Msg of string]) Result.t

val indent_from_lines :
source:string
-> lines:string list
-> range:int * int
-> (int list, [`Msg of string]) Result.t
39 changes: 39 additions & 0 deletions lib/Source.ml
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,42 @@ let loc_of_first_token_at t loc kwd =
match tokens_at t loc ~filter:(Poly.( = ) kwd) with
| [] -> None
| (_, loc) :: _ -> Some loc

let indent_after_token (tok : Parser.token) =
match tok with
(* indent further *)
| AMPERAMPER | AMPERSAND | AND | AS | BAR | BARBAR | BARRBRACKET | BEGIN
|CLASS | COLON | COLONCOLON | COLONEQUAL | COLONGREATER | CONSTRAINT
|DO | DOT | DOTDOT | DOWNTO | ELSE | EQUAL | EXCEPTION | EXTERNAL | FOR
|FUN | FUNCTION | FUNCTOR | GREATER | IF | INCLUDE | INFIXOP0 _
|INFIXOP1 _ | INFIXOP2 _ | INFIXOP3 _ | INFIXOP4 _ | DOTOP _ | LETOP _
|ANDOP _ | INHERIT | INITIALIZER | LAZY | LBRACE | LBRACELESS | LBRACKET
|LBRACKETBAR | LBRACKETLESS | LBRACKETGREATER | LBRACKETPERCENT
|LBRACKETPERCENTPERCENT | LESS | LESSMINUS | LET | LPAREN | LBRACKETAT
|LBRACKETATAT | LBRACKETATATAT | MATCH | METHOD | MINUS | MINUSDOT
|MINUSGREATER | MODULE | MUTABLE | NEW | NONREC | OBJECT | OF | OPEN
|OR | PERCENT | PLUS | PLUSDOT | PLUSEQ | PREFIXOP _ | PRIVATE
|QUESTION | REC | HASH | HASHOP _ | SIG | STAR | STRUCT | THEN | TILDE
|TO | TRY | TYPE | VAL | VIRTUAL | WHEN | WHILE | WITH ->
Some 2
(* same indent *)
| IN | DONE | END | SEMI | COMMA -> Some 0
(* cannot tell *)
| ASSERT | BACKQUOTE | BANG | CHAR _ | EOF | EOL | FALSE | FLOAT _
|GREATERRBRACE | GREATERRBRACKET | INT _ | LABEL _ | LIDENT _
|OPTLABEL _ | QUOTE | RBRACE | RBRACKET | RPAREN | SEMISEMI | STRING _
|TRUE | UIDENT _ | UNDERSCORE | COMMENT _ | DOCSTRING _
|QUOTED_STRING_ITEM _ | QUOTED_STRING_EXPR _ ->
None

let infer_indent_from_prev_line t =
let toks =
let lexbuf = Lexing.from_string t in
let rec loop acc =
match Lexer.token lexbuf with
| Parser.EOF -> acc
| tok -> loop ((tok, Location.curr lexbuf) :: acc)
in
loop []
in
match toks with [] -> None | (tok, _) :: _ -> indent_after_token tok
2 changes: 2 additions & 0 deletions lib/Source.mli
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,5 @@ val is_quoted_string : t -> Location.t -> bool

val loc_of_first_token_at :
t -> Location.t -> Parser.token -> Location.t option

val infer_indent_from_prev_line : t -> int option
Loading

0 comments on commit a59e3c6

Please sign in to comment.