Repository: ruby-syntax-tree/syntax_tree (Ruby)

Issue:
Make line width configurable on the CLI ?
We are evaluating this on discourse/discourse and all our plugins.

Things are looking good, but this jumped to me as weird:

https://github.com/discourse/discourse-chat/pull/1030/files#diff-001301097537175b30c546260016452d2ff11f02435e4acb6cbd4143d744b407R199

Is this because of line width? I see ternaries being removed in many places because of, I assume, line width. Can we get a cli option for line width? We have no immediate interest in forking nor using syntax_tree as a lib at the moment, so that would be great to help adoption.


Discussion:
Configurable line width would be a big help for me, too.

I have been focusing on the VS Code extension and not really on the core gem. The gem has a plugin architecture; if I were to do line width, I would see about making it a plugin (might need to adjust the core code to enable that).

The other factor is, how to specify the parameter (desired max width)? Your idea of using an environment variable was interesting. I know the philosophy of this gem is KISS / zero-config as much as possible -- introducing _any_ config is a bit of a bummer.

An alternate idea would be to introduce a way to specify parameters for plugins, on the CLI or at instantiation time, `--plugin=foo/bar:120` or something like that.

Or, perhaps the gem could ship with a plugin that expands line width to 120 / 160 / etc, and people can subclass that plugin and install their own subclass if they want another value.

Anyhow: random musings. Curious to hear @kddnewton's take on line length. It's not blocking adoption in my company but would be nice to have down the road.
Yeah I don't really want to add more configuration but print width is so fundamental I think I have to. I think I'm going to add `--print-width` as a command-line option to all of the commands that it makes sense.

That being said @xfalcox I really wouldn't suggest increasing it all of the way to 120. It's fundamentally different from something like rubocop's line length for instance. This doc here: https://prettier.io/docs/en/options.html#print-width goes into why and is a pretty good explanation. I've seen people have success with 100 occasionally, but leaving it at 80 is definitely a better way to go in the long run (in my opinion).

That being said, I am going to support this option.
Hahaha you are right on the money with the 120. I implemented both 120 and 100 as plugins [on my fork](https://github.com/ruby-syntax-tree/syntax_tree/compare/main...xfalcox:syntax_tree:line-width) and we found 120 removing too much if blocks into guard clauses.

We are now evaluating 100 as a middle ground!
@xfalcox `--print-width` is in the CLI for v3.1.0



Patch:
diff --git a/README.md b/README.md
index 4c472e37..9c33fd42 100644
--- a/README.md
+++ b/README.md
@@ -118,6 +118,12 @@ If there are files with unformatted code, you will receive:
 The listed files did not match the expected format.
 ```
 
+To change the print width that you are checking against, specify the `--print-width` option, as in:
+
+```sh
+stree check --print-width=100 path/to/file.rb
+```
+
 ### format
 
 This command will output the formatted version of each of the listed files. Importantly, it will not write that content back to the source files. It is meant to display the formatted version only.
@@ -132,6 +138,12 @@ For a file that contains `1 + 1`, you will receive:
 1 + 1
 ```
 
+To change the print width that you are formatting with, specify the `--print-width` option, as in:
+
+```sh
+stree format --print-width=100 path/to/file.rb
+```
+
 ### json
 
 This command will output a JSON representation of the syntax tree that is functionally equivalent to the input. This is mostly used in contexts where you need to access the tree from JavaScript or serialize it over a network.
@@ -213,6 +225,12 @@ This will list every file that is being formatted. It will output light gray if
 path/to/file.rb 0ms
 ```
 
+To change the print width that you are writing with, specify the `--print-width` option, as in:
+
+```sh
+stree write --print-width=100 path/to/file.rb
+```
+
 ## Library
 
 Syntax Tree can be used as a library to access the syntax tree underlying Ruby source code.
diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb
index 1dbd3ac8..5772b821 100644
--- a/lib/syntax_tree.rb
+++ b/lib/syntax_tree.rb
@@ -29,6 +29,11 @@ module SyntaxTree
   HANDLERS = {}
   HANDLERS.default = SyntaxTree
 
+  # This is the default print width when formatting. It can be overridden in the
+  # CLI by passing the --print-width option or here in the API by passing the
+  # optional second argument to ::format.
+  DEFAULT_PRINT_WIDTH = 80
+
   # This is a hook provided so that plugins can register themselves as the
   # handler for a particular file type.
   def self.register_handler(extension, handler)
@@ -43,7 +48,7 @@ def self.parse(source)
   end
 
   # Parses the given source and returns the formatted source.
-  def self.format(source, maxwidth = 80)
+  def self.format(source, maxwidth = DEFAULT_PRINT_WIDTH)
     formatter = Formatter.new(source, [], maxwidth)
     parse(source).format(formatter)
 
diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb
index c8e42831..2484a5b7 100644
--- a/lib/syntax_tree/cli.rb
+++ b/lib/syntax_tree/cli.rb
@@ -91,9 +91,17 @@ class Check < Action
       class UnformattedError < StandardError
       end
 
+      attr_reader :print_width
+
+      def initialize(print_width:)
+        @print_width = print_width
+      end
+
       def run(item)
         source = item.source
-        raise UnformattedError if source != item.handler.format(source)
+        if source != item.handler.format(source, print_width)
+          raise UnformattedError
+        end
       rescue StandardError
         warn("[#{Color.yellow("warn")}] #{item.filepath}")
         raise
@@ -114,13 +122,21 @@ class Debug < Action
       class NonIdempotentFormatError < StandardError
       end
 
+      attr_reader :print_width
+
+      def initialize(print_width:)
+        @print_width = print_width
+      end
+
       def run(item)
         handler = item.handler
 
         warning = "[#{Color.yellow("warn")}] #{item.filepath}"
-        formatted = handler.format(item.source)
+        formatted = handler.format(item.source, print_width)
 
-        raise NonIdempotentFormatError if formatted != handler.format(formatted)
+        if formatted != handler.format(formatted, print_width)
+          raise NonIdempotentFormatError
+        end
       rescue StandardError
         warn(warning)
         raise
@@ -148,8 +164,14 @@ def run(item)
 
     # An action of the CLI that formats the input source and prints it out.
     class Format < Action
+      attr_reader :print_width
+
+      def initialize(print_width:)
+        @print_width = print_width
+      end
+
       def run(item)
-        puts item.handler.format(item.source)
+        puts item.handler.format(item.source, print_width)
       end
     end
 
@@ -173,12 +195,18 @@ def run(item)
     # An action of the CLI that formats the input source and writes the
     # formatted output back to the file.
     class Write < Action
+      attr_reader :print_width
+
+      def initialize(print_width:)
+        @print_width = print_width
+      end
+
       def run(item)
         filepath = item.filepath
         start = Time.now
 
         source = item.source
-        formatted = item.handler.format(source)
+        formatted = item.handler.format(source, print_width)
         File.write(filepath, formatted) if filepath != :stdin
 
         color = source == formatted ? Color.gray(filepath) : filepath
@@ -194,43 +222,44 @@ def run(item)
     # The help message displayed if the input arguments are not correctly
     # ordered or formatted.
     HELP = <<~HELP
-      #{Color.bold("stree ast [OPTIONS] [FILE]")}
+      #{Color.bold("stree ast [--plugins=...] [--print-width=NUMBER] FILE")}
         Print out the AST corresponding to the given files
 
-      #{Color.bold("stree check [OPTIONS] [FILE]")}
+      #{Color.bold("stree check [--plugins=...] [--print-width=NUMBER] FILE")}
         Check that the given files are formatted as syntax tree would format them
 
-      #{Color.bold("stree debug [OPTIONS] [FILE]")}
+      #{Color.bold("stree debug [--plugins=...] [--print-width=NUMBER] FILE")}
         Check that the given files can be formatted idempotently
 
-      #{Color.bold("stree doc [OPTIONS] [FILE]")}
+      #{Color.bold("stree doc [--plugins=...] FILE")}
         Print out the doc tree that would be used to format the given files
 
-      #{Color.bold("stree format [OPTIONS] [FILE]")}
+      #{Color.bold("stree format [--plugins=...] [--print-width=NUMBER] FILE")}
         Print out the formatted version of the given files
 
-      #{Color.bold("stree json [OPTIONS] [FILE]")}
+      #{Color.bold("stree json [--plugins=...] FILE")}
         Print out the JSON representation of the given files
 
-      #{Color.bold("stree match [OPTIONS] [FILE]")}
+      #{Color.bold("stree match [--plugins=...] FILE")}
         Print out a pattern-matching Ruby expression that would match the given files
 
       #{Color.bold("stree help")}
         Display this help message
 
-      #{Color.bold("stree lsp [OPTIONS]")}
+      #{Color.bold("stree lsp [--plugins=...]")}
         Run syntax tree in language server mode
 
       #{Color.bold("stree version")}
         Output the current version of syntax tree
 
-      #{Color.bold("stree write [OPTIONS] [FILE]")}
+      #{Color.bold("stree write [--plugins=...] [--print-width=NUMBER] FILE")}
         Read, format, and write back the source of the given files
 
-      [OPTIONS]
-
       --plugins=...
         A comma-separated list of plugins to load.
+
+      --print-width=NUMBER
+        The maximum line width to use when formatting.
     HELP
 
     class << self
@@ -238,19 +267,31 @@ class << self
       # passed to the invocation.
       def run(argv)
         name, *arguments = argv
-
-        # If there are any plugins specified on the command line, then load them
-        # by requiring them here. We do this by transforming something like
-        #
-        #     stree format --plugins=haml template.haml
-        #
-        # into
-        #
-        #     require "syntax_tree/haml"
-        #
-        if arguments.first&.start_with?("--plugins=")
-          plugins = arguments.shift[/^--plugins=(.*)$/, 1]
-          plugins.split(",").each { |plugin| require "syntax_tree/#{plugin}" }
+        print_width = DEFAULT_PRINT_WIDTH
+
+        while arguments.first&.start_with?("--")
+          case (argument = arguments.shift)
+          when /^--plugins=(.+)$/
+            # If there are any plugins specified on the command line, then load
+            # them by requiring them here. We do this by transforming something
+            # like
+            #
+            #     stree format --plugins=haml template.haml
+            #
+            # into
+            #
+            #     require "syntax_tree/haml"
+            #
+            $1.split(",").each { |plugin| require "syntax_tree/#{plugin}" }
+          when /^--print-width=(\d+)$/
+            # If there is a print width specified on the command line, then
+            # parse that out here and use it when formatting.
+            print_width = Integer($1)
+          else
+            warn("Unknown CLI option: #{argument}")
+            warn(HELP)
+            return 1
+          end
         end
 
         case name
@@ -271,9 +312,9 @@ def run(argv)
           when "a", "ast"
             AST.new
           when "c", "check"
-            Check.new
+            Check.new(print_width: print_width)
           when "debug"
-            Debug.new
+            Debug.new(print_width: print_width)
           when "doc"
             Doc.new
           when "j", "json"
@@ -281,9 +322,9 @@ def run(argv)
           when "m", "match"
             Match.new
           when "f", "format"
-            Format.new
+            Format.new(print_width: print_width)
           when "w", "write"
-            Write.new
+            Write.new(print_width: print_width)
           else
             warn(HELP)
             return 1


Test patch:
diff --git a/test/cli_test.rb b/test/cli_test.rb
index 7f2bcd26..31e4b7e2 100644
--- a/test/cli_test.rb
+++ b/test/cli_test.rb
@@ -52,6 +52,14 @@ def test_check_unformatted
       assert_includes(result.stderr, "expected")
     end
 
+    def test_check_print_width
+      file = Tempfile.new(%w[test- .rb])
+      file.write("#{"a" * 40} + #{"b" * 40}\n")
+
+      result = run_cli("check", "--print-width=100", file: file)
+      assert_includes(result.stdio, "match")
+    end
+
     def test_debug
       result = run_cli("debug")
       assert_includes(result.stdio, "idempotently")