diff options
25 files changed, 376 insertions, 280 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index d59c0d720..24b4df09b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ * [Process] `Process.spawn/1`, `Process.spawn/3`, `Process.spawn_link/1`, `Process.spawn_link/3`, `Process.spawn_monitor/1`, `Process.spawn_monitor/3`, `Process.send/2` and `Process.self/0` are deprecated in favor of the ones in `Kernel` * Deprecations + * [IEx] IEx.Options is deprecated in favor of `IEx.configure/1` and `IEx.configuration/0` * [Kernel] `lc` and `bc` comprehensions are deprecated in favor of `for` * [Macro] `Macro.safe_terms/1` is deprecated * [Process] `Process.delete/0` is deprecated @@ -40,6 +41,7 @@ * [String] Deprecate `:global` option in `String.split/3` in favor of `parts: :infinity` * Backwards incompatible changes + * [IEx] IEx no longer loads an `.iex.exs` file at the current path. Instead, IEx should be configured via the new Mix config * [ExUnit] `ExUnit.Test` and `ExUnit.TestCase` has been converted to structs * [ExUnit] The test and callback context has been converted to maps * [Kernel] `File.Stat`, `HashDict`, `HashSet`, `Inspect.Opts`, `Macro.Env`, `Range`, `Regex` and `Version.Requirement` have been converted to structs. This means `is_record/2` checks will no longer work, instead, you can pattern match on them using `%Range{}` and similar @@ -18,7 +18,7 @@ if [ $# -gt 0 ] && ([ "$1" = "--help" ] || [ "$1" = "-h" ]); then --detached Starts the Erlang VM detached from console --gen-debug Turns on default debugging for all GenServers --remsh \"name\" Connects to a node using a remote shell - --dot-iex \"path\" Overrides default .iex.exs file and uses path instead; + --dot-iex \"path\" Overrides default ~/.iex.exs file and uses path instead; path can be empty, then no file will be loaded ** Options marked with (*) can be given more than once diff --git a/lib/elixir/lib/application.ex b/lib/elixir/lib/application.ex index 610c9c653..d62b5ced1 100644 --- a/lib/elixir/lib/application.ex +++ b/lib/elixir/lib/application.ex @@ -112,6 +112,14 @@ defmodule Application do @type start_type :: :permanent | :transient | :temporary @doc """ + Returns all key-value pairs for `app`. + """ + @spec get_all_env(app) :: [{key,value}] + def get_all_env(app) do + :application.get_all_env(app) + end + + @doc """ Returns the value for `key` in `app`'s environment. If the specified application is not loaded, or the configuration parameter diff --git a/lib/elixir/lib/io/ansi.ex b/lib/elixir/lib/io/ansi.ex index 3db0ee3d3..d8582cbbf 100644 --- a/lib/elixir/lib/io/ansi.ex +++ b/lib/elixir/lib/io/ansi.ex @@ -7,8 +7,8 @@ defmodule IO.ANSI.Sequence do "\e[#{unquote(code)}#{unquote(terminator)}" end - defp escape_sequence(<< unquote(atom_to_binary(name)), rest :: binary >>) do - {"\e[#{unquote(code)}#{unquote(terminator)}", rest} + defp escape_sequence(unquote(atom_to_list(name))) do + unquote(name)() end end end @@ -130,15 +130,8 @@ defmodule IO.ANSI do @doc "Clear screen" defsequence :clear, "2", "J" - - # Catch spaces between codes - defp escape_sequence(<< ?\s, rest :: binary >>) do - escape_sequence(rest) - end - defp escape_sequence(other) do - [spec|_] = String.split(other, ~r/(,|\})/) - raise ArgumentError, message: "invalid ANSI sequence specification: #{spec}" + raise ArgumentError, message: "invalid ANSI sequence specification: #{other}" end @doc ~S""" @@ -163,8 +156,8 @@ defmodule IO.ANSI do """ @spec escape(String.t, emit :: boolean) :: String.t def escape(string, emit \\ terminal?) do - {rendered, emitted} = do_escape(string, false, emit, false, []) - if emitted and emit do + {rendered, emitted} = do_escape(string, emit, false, nil, []) + if emitted do rendered <> reset else rendered @@ -193,34 +186,43 @@ defmodule IO.ANSI do """ @spec escape_fragment(String.t, emit :: boolean) :: String.t def escape_fragment(string, emit \\ terminal?) do - {rendered, _emitted} = do_escape(string, false, emit, false, []) - rendered + {escaped, _emitted} = do_escape(string, emit, false, nil, []) + escaped end - defp do_escape(<< ?%, ?{, rest :: binary >>, false, emit, _emitted, acc) do - do_escape_sequence(rest, emit, acc) - end - defp do_escape(<< ?,, rest :: binary >>, true, emit, _emitted, acc) do - do_escape_sequence(rest, emit, acc) + defp do_escape(<<?}, t :: binary>>, emit, emitted, buffer, acc) when is_list(buffer) do + sequences = + buffer + |> Enum.reverse() + |> :string.tokens(',') + |> Enum.map(&(&1 |> :string.strip |> escape_sequence)) + |> Enum.reverse() + + if emit and sequences != [] do + do_escape(t, emit, true, nil, sequences ++ acc) + else + do_escape(t, emit, emitted, nil, acc) + end end - defp do_escape(<< ?\s, rest :: binary >>, true, emit, emitted, acc) do - do_escape(rest, true, emit, emitted, acc) + + defp do_escape(<<h, t :: binary>>, emit, emitted, buffer, acc) when is_list(buffer) do + do_escape(t, emit, emitted, [h|buffer], acc) end - defp do_escape(<< ?}, rest :: binary >>, true, emit, emitted, acc) do - do_escape(rest, false, emit, emitted, acc) + + defp do_escape(<<>>, _emit, _emitted, buffer, _acc) when is_list(buffer) do + buffer = iodata_to_binary Enum.reverse(buffer) + raise ArgumentError, message: "missing } for escape fragment #{buffer}" end - defp do_escape(<< x :: [binary, size(1)], rest :: binary>>, false, emit, emitted, acc) do - do_escape(rest, false, emit, emitted, [x|acc]) + + defp do_escape(<<?%, ?{, t :: binary>>, emit, emitted, nil, acc) do + do_escape(t, emit, emitted, [], acc) end - defp do_escape("", false, _emit, emitted, acc) do - {iodata_to_binary(Enum.reverse(acc)), emitted} + + defp do_escape(<<h, t :: binary>>, emit, emitted, nil, acc) do + do_escape(t, emit, emitted, nil, [h|acc]) end - defp do_escape_sequence(rest, emit, acc) do - {code, rest} = escape_sequence(rest) - if emit do - acc = [code|acc] - end - do_escape(rest, true, emit, true, acc) + defp do_escape(<<>>, _emit, emitted, nil, acc) do + {iodata_to_binary(Enum.reverse(acc)), emitted} end end diff --git a/lib/elixir/test/elixir/application_test.exs b/lib/elixir/test/elixir/application_test.exs index d655842b9..20258320f 100644 --- a/lib/elixir/test/elixir/application_test.exs +++ b/lib/elixir/test/elixir/application_test.exs @@ -11,6 +11,7 @@ defmodule ApplicationTest do assert Application.put_env(:elixir, :unknown, :known) == :ok assert Application.fetch_env(:elixir, :unknown) == {:ok, :known} assert Application.get_env(:elixir, :unknown, :default) == :known + assert {:unknown, :known} in Application.get_all_env(:elixir) assert Application.delete_env(:elixir, :unknown) == :ok assert Application.get_env(:elixir, :unknown, :default) == :default diff --git a/lib/elixir/test/elixir/io/ansi_test.exs b/lib/elixir/test/elixir/io/ansi_test.exs index 5239d4354..f05a06799 100644 --- a/lib/elixir/test/elixir/io/ansi_test.exs +++ b/lib/elixir/test/elixir/io/ansi_test.exs @@ -27,6 +27,9 @@ defmodule IO.ANSITest do end test :no_emit do + assert IO.ANSI.escape("Hello, %{}world!", false) == + "Hello, world!" + assert IO.ANSI.escape("Hello, %{red,bright}world!", false) == "Hello, world!" end diff --git a/lib/ex_unit/lib/ex_unit.ex b/lib/ex_unit/lib/ex_unit.ex index 57e3b851b..242a8eda9 100644 --- a/lib/ex_unit/lib/ex_unit.ex +++ b/lib/ex_unit/lib/ex_unit.ex @@ -163,7 +163,7 @@ defmodule ExUnit do """ def configure(options) do Enum.each options, fn {k, v} -> - :application.set_env(:ex_unit, k, v) + Application.put_env(:ex_unit, k, v) end end @@ -171,7 +171,7 @@ defmodule ExUnit do Returns ExUnit configuration. """ def configuration do - :application.get_all_env(:ex_unit) + Application.get_all_env(:ex_unit) end @doc """ diff --git a/lib/iex/lib/iex.ex b/lib/iex/lib/iex.ex index d175a5ed2..d7b07a59d 100644 --- a/lib/iex/lib/iex.ex +++ b/lib/iex/lib/iex.ex @@ -120,16 +120,15 @@ defmodule IEx do Connecting an Elixir shell to a remote node without Elixir is **not** supported. - ## The .iex.exs file + ## The ~/.iex.exs file - When starting IEx, it will look for a local `.iex.exs` file (located in the current - working directory), then a global one (located at `~/.iex.exs`) and will load the - first one it finds (if any). The code in the chosen .iex file will be - evaluated in the shell's context. So, for instance, any modules that are - loaded or variables that are bound in the .iex file will be available in the - shell after it has booted. + When starting IEx, it will look for a global configuration file + (located at `~/.iex.exs`) and load it if available. The code in the + chosen .iex file will be evaluated in the shell's context. So, for + instance, any modules that are loaded or variables that are bound + in the .iex file will be available in the shell after it has booted. - Sample contents of a local .iex file: + Sample contents of a .iex file: # source another `.iex` file import_file "~/.iex.exs" @@ -151,23 +150,19 @@ defmodule IEx do iex(1)> value 13 - It is possible to override the default loading sequence for `.iex.exs` file by - supplying the `--dot-iex` option to iex. See `iex --help`. + It is possible to load another file by supplying the `--dot-iex` + option to iex. See `iex --help`. ## Configuring the shell There are a number of customization options provided by the shell. Take a look - at the docs for the `IEx.Options` module by typing `h IEx.Options`. + at the docs for the `IEx.configure/1` function by typing `h IEx.configure/1`. - The main functions there are `IEx.Options.get/1` and `IEx.Options.set/2`. One - can also use `IEx.Options.list/0` to get the list of all supported options. - `IEx.Options.print_help/1` will print documentation for the given option. - - In particular, it might be convenient to customize those options inside your - `.iex.exs` file like this: + Those options can be configured in your project configuration file or globally + by calling `IEx.configure/1` from your `~/.iex.exs` file like this: # .iex - IEx.Options.set :inspect, limit: 3 + IEx.configure(inspect: [limit: 3]) ### now run the shell ### @@ -214,17 +209,109 @@ defmodule IEx do """ @doc """ + Configures IEx. + + The supported options are: `:colors`, `:inspect`, + `:default_prompt`, `:alive_prompt` and `:history_size`. + + ## Colors + + A keyword list that encapsulates all color settings used by the + shell. See documentation for the `IO.ANSI` module for the list of + supported colors and attributes. + + The value is a keyword list. List of supported keys: + + * `:enabled` - boolean value that allows for switching the coloring on and off + * `:eval_result` - color for an expression's resulting value + * `:eval_info` - … various informational messages + * `:eval_error` - … error messages + * `:stack_app` - … the app in stack traces + * `:stack_info` - … the remaining info in stacktraces + * `:ls_directory` - … for directory entries (ls helper) + * `:ls_device` - … device entries (ls helper) + + When printing documentation, IEx will convert the markdown + documentation to ANSI as well. Those can be configured via: + + * `:doc_code` — the attributes for code blocks (cyan, bright) + * `:doc_inline_code` - inline code (cyan) + * `:doc_headings` - h1 and h2 (yellow, bright) + * `:doc_title` — the overall heading for the output (reverse,yellow,bright) + * `:doc_bold` - (bright) + * `:doc_underline` - (underline) + + ## Inspect + + A keyword list containing inspect options used by the shell + when printing results of expression evaluation. Defailt to + pretty formatting with a limit of 50 entries. + + See `Inspect.Opts` for the full list of options. + + ## History size + + Number of expressions and their results to keep in the history. + The value is an integer. When it is negative, the history is unlimited. + + ## Prompt + + This is an option determining the prompt displayed to the user + when awaiting input. + + The value is a keyword list. Two prompt types: + + * `:default_prompt` - used when `Node.alive?` returns false + * `:alive_prompt` - used when `Node.alive?` returns true + + The part of the listed in the following of the prompt string is replaced. + + * `%counter` - the index of the history + * `%prefix` - a prefix given by `IEx.Server` + * `%node` - the name of the local node + + """ + def configure(options) do + Enum.each options, fn {k, v} -> + Application.put_env(:iex, k, configure(k, v)) + end + end + + defp configure(k, v) when k in [:colors, :inspect] and is_list(v) do + Keyword.merge(Application.get_env(:iex, k), v) + end + + defp configure(:history_size, v) when is_integer(v) do + v + end + + defp configure(k, v) when k in [:default_prompt, :alive_prompt] and is_binary(v) do + v + end + + defp configure(k, v) do + raise ArgumentError, message: "invalid value #{inspect v} for configuration #{inspect k}" + end + + @doc """ + Returns IEx configuration. + """ + def configuration do + Application.get_all_env(:iex) + end + + @doc """ Registers a function to be invoked after the IEx process is spawned. """ def after_spawn(fun) when is_function(fun) do - :application.set_env(:iex, :after_spawn, [fun|after_spawn]) + Application.put_env(:iex, :after_spawn, [fun|after_spawn]) end @doc """ Returns registered `after_spawn` callbacks. """ def after_spawn do - {:ok, list} = :application.get_env(:iex, :after_spawn) + {:ok, list} = Application.fetch_env(:iex, :after_spawn) list end @@ -232,7 +319,7 @@ defmodule IEx do Returns `true` if IEx was properly started. """ def started? do - :application.get_env(:iex, :started) == {:ok, true} + Application.get_env(:iex, :started, false) end @doc """ @@ -240,7 +327,7 @@ defmodule IEx do ANSI escapes in `string` are not processed in any way. """ def color(color_name, string) do - colors = IEx.Options.get(:colors) + colors = Application.get_env(:iex, :colors) enabled = colors[:enabled] IO.ANSI.escape_fragment("%{#{colors[color_name]}}", enabled) <> string <> IO.ANSI.escape_fragment("%{reset}", enabled) @@ -345,7 +432,7 @@ defmodule IEx do _ -> :init.wait_until_started() end - start_iex() + callback = start_iex(callback) set_expand_fun() run_after_spawn() IEx.Server.start(opts, callback) @@ -355,14 +442,61 @@ defmodule IEx do @doc false def dont_display_result, do: :"do not show this result in output" + @doc false + def default_colors do + [# Used by default on evaluation cycle + eval_interrupt: "yellow", + eval_result: "yellow", + eval_error: "red", + eval_info: "normal", + stack_app: "red,bright", + stack_info: "red", + + # Used by ls + ls_directory: "blue", + ls_device: "green", + + # Used by ansi docs + doc_bold: "bright", + doc_code: "cyan,bright", + doc_headings: "yellow,bright", + doc_inline_code: "cyan", + doc_underline: "underline", + doc_title: "reverse,yellow,bright"] + end + + @doc false + def default_inspect do + [structs: true, binaries: :infer, + char_lists: :infer, limit: 50, pretty: true] + end + ## Helpers - defp start_iex do - unless started? do - :application.start(:elixir) - :application.start(:iex) - :application.set_env(:iex, :started, true) - IEx.Options.set :colors, enabled: IO.ANSI.terminal? + defp start_iex(callback) do + if started? do + callback + else + Application.start(:elixir) + Application.start(:iex) + Application.put_env(:iex, :started, true) + + fn -> + # The callback may actually configure IEx (for example, + # if it is a Mix project), so we wrap the original callback + # so we can normalize options afterwards. + callback.() + + colors = default_colors + |> Keyword.merge(Application.get_env(:iex, :colors)) + |> Keyword.put_new(:enabled, IO.ANSI.terminal?) + + inspect = default_inspect + |> Keyword.merge(Application.get_env(:iex, :inspect)) + + Application.put_env(:iex, :colors, colors) + Application.put_env(:iex, :inspect, inspect) + end end end diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 92b1a575d..204b9be20 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -39,18 +39,12 @@ defmodule IEx.Evaluator do Returns the new config. """ def load_dot_iex(config, path \\ nil) do - candidates = if path do - [path] - else - Enum.map [".iex.exs", "~/.iex.exs"], &Path.expand/1 - end + path = path || "~/.iex.exs" - path = Enum.find candidates, &File.regular?/1 - - if nil?(path) do - config - else + if File.regular?(path) do eval_dot_iex(config, path) + else + config end end @@ -132,7 +126,8 @@ defmodule IEx.Evaluator do end defp update_history(counter, cache, result) do - IEx.History.append({counter, cache, result}, counter, IEx.Options.get(:history_size)) + IEx.History.append({counter, cache, result}, counter, + Application.get_env(:iex, :history_size)) end defp io_put(result) do @@ -144,7 +139,7 @@ defmodule IEx.Evaluator do end defp inspect_opts do - [width: IEx.width] ++ IEx.Options.get(:inspect) + [width: IEx.width] ++ Application.get_env(:iex, :inspect) end ## Error handling diff --git a/lib/iex/lib/iex/helpers.ex b/lib/iex/lib/iex/helpers.ex index 49a8be519..518c4400f 100644 --- a/lib/iex/lib/iex/helpers.ex +++ b/lib/iex/lib/iex/helpers.ex @@ -236,7 +236,7 @@ defmodule IEx.Helpers do their results. """ def v do - inspect_opts = IEx.Options.get(:inspect) + inspect_opts = Application.get_env(:iex, :inspect) IEx.History.each(&print_history_entry(&1, inspect_opts)) end @@ -299,7 +299,7 @@ defmodule IEx.Helpers do Flushes all messages sent to the shell and prints them out. """ def flush do - inspect_opts = IEx.Options.get(:inspect) + inspect_opts = Application.get_env(:iex, :inspect) do_flush(inspect_opts) end diff --git a/lib/iex/lib/iex/introspection.ex b/lib/iex/lib/iex/introspection.ex index 649f85d6e..44fab3a16 100644 --- a/lib/iex/lib/iex/introspection.ex +++ b/lib/iex/lib/iex/introspection.ex @@ -178,7 +178,7 @@ defmodule IEx.Introspection do end defp docs_options() do - [width: IEx.width] ++ IEx.Options.get(:colors) + [width: IEx.width] ++ Application.get_env(:iex, :colors) end @doc """ diff --git a/lib/iex/lib/iex/options.ex b/lib/iex/lib/iex/options.ex index b78a18d2f..1487dd59a 100644 --- a/lib/iex/lib/iex/options.ex +++ b/lib/iex/lib/iex/options.ex @@ -1,37 +1,5 @@ defmodule IEx.Options do - @moduledoc """ - Provides an interface for adjusting options of the running IEx session. - - Changing options is usually done inside an IEx session or in your .iex.exs file. - See `h(IEx)` for more info on the latter. - - If the value of an option is a keyword list, only those keys that are - mentioned will be changed. The rest of the sub-options will keep their - current values. Any extraneous keys are filtered out, i.e. not used. - - To get the list of all supported options, use `IEx.Options.list/0`. - You can also get an option's description using `IEx.Options.print_help/1`. - - ## Examples - - iex(1)> ArgumentError[] - ArgumentError[message: "argument error"] - - iex(2)> IEx.Options.set :inspect, structs: false - [limit: 50, structs: true] - - iex(3)> ArgumentError[] - {ArgumentError,:__exception__,"argument error"} - - iex(4)> IEx.Options.list - [:colors,:inspect] - - iex(5)> IEx.Options.print_help :colors - This is an aggregate option that encapsulates all color settings used - by the shell. - ... # omitted content - - """ + @moduledoc false @supported_options ~w(colors inspect history_size prompt)a @@ -40,6 +8,7 @@ defmodule IEx.Options do list. """ def get do + IO.write :stderr, "IEx.Options.get/0 is deprecated, please use IEx.configuration/0\n#{Exception.format_stacktrace}" Enum.map list(), fn name -> {name, get(name)} end @@ -53,6 +22,7 @@ defmodule IEx.Options do for key <- @supported_options do def get(unquote(key)) do + IO.write :stderr, "IEx.Options.get/1 is deprecated, please use IEx.configuration/0\n#{Exception.format_stacktrace}" {:ok, value} = Application.fetch_env(:iex, unquote(key)) value end @@ -69,6 +39,7 @@ defmodule IEx.Options do Returns a keyword list of old option values. """ def set(opts) do + IO.write :stderr, "IEx.Options.set/1 is deprecated, please use IEx.configure/1\n#{Exception.format_stacktrace}" Enum.map opts, fn {name, val} -> {name, set(name, val)} end @@ -85,6 +56,7 @@ defmodule IEx.Options do def set(name, value) def set(:colors, colors) when is_list(colors) do + IO.write :stderr, "IEx.Options.set/2 is deprecated, please use IEx.configure/1\n#{Exception.format_stacktrace}" filter_and_merge(:colors, colors) end @@ -93,6 +65,7 @@ defmodule IEx.Options do end def set(:inspect, opts) when is_list(opts) do + IO.write :stderr, "IEx.Options.set/2 is deprecated, please use IEx.configure/1\n#{Exception.format_stacktrace}" filter_and_merge(:inspect, opts) end @@ -101,6 +74,7 @@ defmodule IEx.Options do end def set(:history_size, size) when is_integer(size) do + IO.write :stderr, "IEx.Options.set/2 is deprecated, please use IEx.configure/1\n#{Exception.format_stacktrace}" old_size = get(:history_size) Application.put_env(:iex, :history_size, size) old_size @@ -111,6 +85,7 @@ defmodule IEx.Options do end def set(:prompt, prompts) when is_list(prompts) do + IO.write :stderr, "IEx.Options.set/2 is deprecated, please use IEx.configure/1\n#{Exception.format_stacktrace}" filter_and_merge(:prompt, prompts) end @@ -193,6 +168,7 @@ defmodule IEx.Options do Same as `help/1` but instead of returning a string, prints it. """ def print_help(name) do + IO.write :stderr, "IEx.Options is deprecated\n#{Exception.format_stacktrace}" IO.ANSI.Docs.print help(name) end @@ -200,6 +176,7 @@ defmodule IEx.Options do Returns all supported options as a list of names. """ def list() do + IO.write :stderr, "IEx.Options is deprecated\n#{Exception.format_stacktrace}" @supported_options end @@ -211,23 +188,9 @@ defmodule IEx.Options do raise ArgumentError, message: "Expected the value to be #{type}" end - defp raise_key(option_name, name) do - raise ArgumentError, message: "Unsupported key '#{name}' for option '#{option_name}'" - end - defp filter_and_merge(opt, values) when is_list(values) do old_values = get(opt) - filtered_values = filtered_kw(opt, old_values, values) - :application.set_env(:iex, opt, Keyword.merge(old_values, filtered_values)) + :application.set_env(:iex, opt, Keyword.merge(old_values, values)) old_values end - - defp filtered_kw(opt, reference_kw, user_kw) do - Enum.filter user_kw, fn {name, _} -> - if not Keyword.has_key?(reference_kw, name) do - raise_key(opt, name) - end - true - end - end end diff --git a/lib/iex/lib/iex/server.ex b/lib/iex/lib/iex/server.ex index 0e2467fde..2eff0c549 100644 --- a/lib/iex/lib/iex/server.ex +++ b/lib/iex/lib/iex/server.ex @@ -269,7 +269,7 @@ defmodule IEx.Server do {:default, prefix || "iex"} end - prompt = IEx.Options.get(:prompt)[mode] + prompt = Application.get_env(:iex, :"#{mode}_prompt") |> String.replace("%counter", to_string(counter)) |> String.replace("%prefix", to_string(prefix)) |> String.replace("%node", to_string(node)) diff --git a/lib/iex/mix.exs b/lib/iex/mix.exs index 1a966fa26..ce58ab129 100644 --- a/lib/iex/mix.exs +++ b/lib/iex/mix.exs @@ -8,34 +8,10 @@ defmodule IEx.Mixfile do def application do [env: [ after_spawn: [], - colors: colors, - inspect: [structs: true, binaries: :infer, - char_lists: :infer, limit: 50, pretty: true], + colors: IEx.default_colors, + inspect: IEx.default_inspect, history_size: 20, - prompt: [default: "%prefix(%counter)>", alive: "%prefix(%node)%counter>" ]]] - end - - defp colors do - [enabled: true, - - # Used by default on evaluation cycle - eval_interrupt: "yellow", - eval_result: "yellow", - eval_error: "red", - eval_info: "normal", - stack_app: "red,bright", - stack_info: "red", - - # Used by ls - ls_directory: "blue", - ls_device: "green", - - # Used by ansi docs - doc_bold: "bright", - doc_code: "cyan,bright", - doc_headings: "yellow,bright", - doc_inline_code: "cyan", - doc_underline: "underline", - doc_title: "reverse,yellow,bright"] + default_prompt: "%prefix(%counter)>", + alive_prompt: "%prefix(%node)%counter>"]] end end diff --git a/lib/iex/test/iex/evaluator_test.exs b/lib/iex/test/iex/evaluator_test.exs index 098fc783f..0860956e4 100644 --- a/lib/iex/test/iex/evaluator_test.exs +++ b/lib/iex/test/iex/evaluator_test.exs @@ -12,7 +12,6 @@ defmodule IEx.EvaluatorTest do {IEx, :three, 3, [file: "longer", line: 1234]}, {List, :four, 4, [file: "loc", line: 1]}, ] - IEx.Options.set :colors, enabled: false expected = """ (elixir) loc:1: List.one/1 diff --git a/lib/iex/test/iex/interaction_test.exs b/lib/iex/test/iex/interaction_test.exs index 6b424df12..577f6cc52 100644 --- a/lib/iex/test/iex/interaction_test.exs +++ b/lib/iex/test/iex/interaction_test.exs @@ -3,14 +3,18 @@ Code.require_file "../test_helper.exs", __DIR__ defmodule IEx.InteractionTest do use IEx.Case + @doc """ + Hello, I have %{red}ANSI%{reset} escapes. + """ + def ansi_escapes, do: :ok + ## Basic interaction test "whole output" do - IEx.Options.set :colors, enabled: false - assert capture_io("IO.puts \"Hello world\"", fn -> IEx.Server.start([dot_iex_path: ""], fn -> end) - end) =~ "Interactive Elixir (#{System.version}) - press Ctrl+C to exit (type h() ENTER for help)\niex(1)> Hello world\n:ok\niex(2)>" + end) =~ "Interactive Elixir (#{System.version}) - press Ctrl+C to exit (type h() ENTER for help)" <> + "\niex(1)> Hello world\n:ok\niex(2)>" end test "empty input" do @@ -88,6 +92,51 @@ defmodule IEx.InteractionTest do :code.delete(Sample) end + test "prompt" do + opts = [default_prompt: "prompt(%counter)>"] + assert capture_iex("1\n", opts, [], true) == "prompt(1)> 1\nprompt(2)>" + end + + unless match?({:win32,_}, :os.type) do + test "color" do + opts = [colors: [enabled: true, eval_result: "red"]] + assert capture_iex("1 + 2", opts) == "\e[31m3\e[0m" + + # Sanity checks + assert capture_iex("IO.ANSI.escape(\"%{blue}hello\", true)", opts) + == "\e[31m\"\\e[34mhello\\e[0m\"\e[0m" + assert capture_iex("IO.puts IO.ANSI.escape(\"%{blue}hello\", true)", opts) + == "\e[34mhello\e[0m\n\e[31m:ok\e[0m" + assert capture_iex("IO.puts IO.ANSI.escape(\"%{blue}hello\", true)", [colors: [enabled: false]]) + == "\e[34mhello\e[0m\n:ok" + + # Test that ANSI escapes in the docs are left alone + opts = [colors: [enabled: true]] + assert capture_iex("h IEx.InteractionTest.ansi_escapes", opts) + == "* def ansi_escapes()\n\nHello, I have %{red}ANSI%{reset} escapes." + + # Test that ANSI escapes in iex output are left alone + opts = [colors: [enabled: true, eval_result: "red", eval_info: "red"]] + assert capture_iex("\"%{red} %{blue}\"", opts) == "\e[31m\"%{red} %{blue}\"\e[0m" + assert capture_iex("IO.puts IEx.color(:eval_info, \"%{red} %{blue}\")", opts) + == "\e[31m%{red} %{blue}\e[0m\n\e[31m:ok\e[0m" + end + end + + test "inspect opts" do + opts = [inspect: [binaries: :as_binaries, char_lists: :as_lists, structs: false, limit: 4]] + assert capture_iex("<<45,46,47>>\n[45,46,47]\n%IO.Stream{}", opts) == + "<<45, 46, 47>>\n[45, 46, 47]\n%{__struct__: IO.Stream, device: nil, line_or_bytes: :line, raw: true}" + end + + test "history size" do + opts = [history_size: 3] + assert capture_iex("1\n2\n3\nv(1)", opts) == "1\n2\n3\n1" + assert "1\n2\n3\n4\n** (RuntimeError) v(1) is out of bounds" <> _ = capture_iex("1\n2\n3\n4\nv(1)", opts) + assert "1\n2\n3\n4\n** (RuntimeError) v(-4) is out of bounds" <> _ = capture_iex("1\n2\n3\n4\nv(-4)", opts) + assert "1\n2\n3\n4\n2\n** (RuntimeError) v(2) is out of bounds" <> _ = capture_iex("1\n2\n3\n4\nv(2)\nv(2)", opts) + end + ## .iex file loading test "no .iex" do @@ -98,7 +147,7 @@ defmodule IEx.InteractionTest do File.write!("dot-iex", "my_variable = 144") assert capture_iex("my_variable", [], [dot_iex_path: "dot-iex"]) == "144" after - File.rm!("dot-iex") + File.rm("dot-iex") end test "nested .iex" do @@ -109,23 +158,28 @@ defmodule IEx.InteractionTest do assert capture_iex(input, [], [dot_iex_path: "dot-iex"]) == "13\n14\nhello\n:ok" after File.rm("dot-iex-1") - File.rm!("dot-iex") + File.rm("dot-iex") end test "receive exit" do - assert capture_iex("spawn_link(fn -> exit(:bye) end)") =~ ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) :bye" - assert capture_iex("spawn_link(fn -> exit({:bye, [:world]}) end)") =~ ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) {:bye, \[:world\]}" + assert capture_iex("spawn_link(fn -> exit(:bye) end)") =~ + ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) :bye" + assert capture_iex("spawn_link(fn -> exit({:bye, [:world]}) end)") =~ + ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) {:bye, \[:world\]}" end test "receive exit from exception" do - # use exit/1 to fake an error so that an error message is not sent to the - # error logger. - assert capture_iex("spawn_link(fn -> exit({ArgumentError[], - [{:not_a_real_module, :function, 0, []}]}) end)") =~ ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) an exception was raised:\n\s{4}\*\* \(ArgumentError\) argument error\n\s{8}:not_a_real_module\.function/0" + # use exit/1 to fake an error so that an error message + # is not sent to the error logger. + content = capture_iex("spawn_link(fn -> exit({ArgumentError[], + [{:not_a_real_module, :function, 0, []}]}) end)") + assert content =~ ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) an exception was raised:\n" + assert content =~ ~r"\s{4}\*\* \(ArgumentError\) argument error\n" + assert content =~ ~r"\s{8}:not_a_real_module\.function/0" end test "exit due to failed call" do - assert capture_iex("exit({:bye, {:gen_server, :call, [self(), :hello]}})") =~ ~r"\*\* \(exit\) exited in: :gen_server\.call\(#PID<\d+\.\d+\.\d+>, :hello\)\n\s{4}\*\* \(EXIT\) :bye" + assert capture_iex("exit({:bye, {:gen_server, :call, [self(), :hello]}})") =~ + ~r"\*\* \(exit\) exited in: :gen_server\.call\(#PID<\d+\.\d+\.\d+>, :hello\)\n\s{4}\*\* \(EXIT\) :bye" end - end diff --git a/lib/iex/test/iex/options_test.exs b/lib/iex/test/iex/options_test.exs deleted file mode 100644 index 74c317cd7..000000000 --- a/lib/iex/test/iex/options_test.exs +++ /dev/null @@ -1,67 +0,0 @@ -Code.require_file "../test_helper.exs", __DIR__ - -defmodule IEx.OptionsTest do - use IEx.Case - - @doc """ - Hello, I have %{red}ANSI%{reset} escapes. - """ - def ansi_escapes, do: :ok - - unless match?({:win32,_}, :os.type) do - test "color" do - opts = [colors: [enabled: true, eval_result: "red"]] - assert capture_iex("1 + 2", opts) == "\e[31m3\e[0m" - - # Sanity checks - assert capture_iex("IO.ANSI.escape(\"%{blue}hello\", true)", opts) - == "\e[31m\"\\e[34mhello\\e[0m\"\e[0m" - assert capture_iex("IO.puts IO.ANSI.escape(\"%{blue}hello\", true)", opts) - == "\e[34mhello\e[0m\n\e[31m:ok\e[0m" - assert capture_iex("IO.puts IO.ANSI.escape(\"%{blue}hello\", true)", [colors: [enabled: false]]) - == "\e[34mhello\e[0m\n:ok" - - # Test that ANSI escapes in the docs are left alone - opts = [colors: [enabled: true]] - assert capture_iex("h IEx.OptionsTest.ansi_escapes", opts) - == "* def ansi_escapes()\n\nHello, I have %{red}ANSI%{reset} escapes." - - # Test that ANSI escapes in iex output are left alone - opts = [colors: [enabled: true, eval_result: "red", eval_info: "red"]] - assert capture_iex("\"%{red} %{blue}\"", opts) == "\e[31m\"%{red} %{blue}\"\e[0m" - assert capture_iex("IO.puts IEx.color(:eval_info, \"%{red} %{blue}\")", opts) - == "\e[31m%{red} %{blue}\e[0m\n\e[31m:ok\e[0m" - end - end - - test "inspect opts" do - opts = [inspect: [binaries: :as_binaries, char_lists: :as_lists, structs: false, limit: 4]] - assert capture_iex("<<45,46,47>>\n[45,46,47]\n%IO.Stream{}", opts) == - "<<45, 46, 47>>\n[45, 46, 47]\n%{__struct__: IO.Stream, device: nil, line_or_bytes: :line, raw: true}" - end - - test "history size" do - opts = [history_size: 3] - assert capture_iex("1\n2\n3\nv(1)", opts) == "1\n2\n3\n1" - assert "1\n2\n3\n4\n** (RuntimeError) v(1) is out of bounds" <> _ = capture_iex("1\n2\n3\n4\nv(1)", opts) - assert "1\n2\n3\n4\n** (RuntimeError) v(-4) is out of bounds" <> _ = capture_iex("1\n2\n3\n4\nv(-4)", opts) - assert "1\n2\n3\n4\n2\n** (RuntimeError) v(2) is out of bounds" <> _ = capture_iex("1\n2\n3\n4\nv(2)\nv(2)", opts) - end - - test "prompt" do - opts = [prompt: [default: "prompt(%counter)>", alive: "prompt(%counter)"]] - assert capture_iex("1\n", opts, [], true) == "prompt(1)> 1\nprompt(2)>" - end - - test "bad option" do - assert_raise ArgumentError, fn -> - IEx.Options.set :nonexistent_option, nil - end - end - - test "bad key" do - assert_raise ArgumentError, fn -> - IEx.Options.set :colors, nonexistent_color_name: "red" - end - end -end diff --git a/lib/iex/test/iex/server_test.exs b/lib/iex/test/iex/server_test.exs index 17b460f80..ecbb65880 100644 --- a/lib/iex/test/iex/server_test.exs +++ b/lib/iex/test/iex/server_test.exs @@ -1,12 +1,7 @@ Code.require_file "../test_helper.exs", __DIR__ defmodule IEx.ServerTest do - use IEx.Case - - setup do - IEx.Options.set :colors, enabled: false - :ok - end + use IEx.Case, async: true # Options diff --git a/lib/iex/test/test_helper.exs b/lib/iex/test/test_helper.exs index eca77ffe3..38e6f57a2 100644 --- a/lib/iex/test/test_helper.exs +++ b/lib/iex/test/test_helper.exs @@ -1,10 +1,11 @@ -:application.start(:iex) +Application.start(:iex) +Application.put_env(:iex, :colors, [enabled: false]) ExUnit.start [trace: "--trace" in System.argv] defmodule IEx.Case do + use ExUnit.CaseTemplate @moduledoc false - # # Provides convenience functions for testing IEx-related functionality. # Use this module inside your test module like this: # @@ -20,27 +21,25 @@ defmodule IEx.Case do # session, except colors are disabled by default and .iex files are not # loaded. # - # You can provide your own IEx.Options and a path to a .iex file as + # You can provide your own IEx configuration and a path to a .iex file as # additional arguments to the capture_iex function. - # - defmacro __using__(_) do + using do quote do - use ExUnit.Case, async: false import ExUnit.CaptureIO import unquote(__MODULE__) + end + end - setup do - opts = IEx.Options.get - IEx.Options.set :colors, [enabled: false] - {:ok, [iex_opts: opts]} - end + setup do + opts = IEx.configuration |> + Keyword.take([:default_prompt, :alive_prompt, :inspect, :colors, :history_size]) + {:ok, [iex_opts: opts]} + end - teardown context do - IEx.Options.set context[:iex_opts] - :ok - end - end + teardown context do + IEx.configure context[:iex_opts] + :ok end @doc """ @@ -54,9 +53,7 @@ defmodule IEx.Case do IEx.Server.start to be used in the normal .iex loading process. """ def capture_iex(input, options \\ [], server_options \\ [], capture_prompt \\ false) do - Enum.each options, fn {opt, value} -> - IEx.Options.set(opt, value) - end + IEx.configure(options) ExUnit.CaptureIO.capture_io([input: input, capture_prompt: capture_prompt], fn -> server_options = Keyword.put_new(server_options, :dot_iex_path, "") diff --git a/lib/mix/lib/mix/cli.ex b/lib/mix/lib/mix/cli.ex index 42f8f86e1..e44d4bf22 100644 --- a/lib/mix/lib/mix/cli.ex +++ b/lib/mix/lib/mix/cli.ex @@ -51,6 +51,7 @@ defmodule Mix.CLI do defp run_task(name, args) do try do if Mix.Project.get do + Mix.Task.run "loadconfig" Mix.Task.run "deps.loadpaths", ["--no-deps-check"] Mix.Task.run "loadpaths", ["--no-elixir-version-check"] Mix.Task.reenable "deps.loadpaths" @@ -87,7 +88,8 @@ defmodule Mix.CLI do end defp change_env(task) do - if nil?(System.get_env("MIX_ENV")) && (env = Mix.Project.config[:preferred_cli_env][task]) do + if nil?(System.get_env("MIX_ENV")) && + (env = Mix.Project.config[:preferred_cli_env][task]) do Mix.env(env) if project = Mix.Project.pop do {project, _config, file} = project diff --git a/lib/mix/lib/mix/project.ex b/lib/mix/lib/mix/project.ex index a1a0c34e8..971fcdd35 100644 --- a/lib/mix/lib/mix/project.ex +++ b/lib/mix/lib/mix/project.ex @@ -44,7 +44,10 @@ defmodule Mix.Project do # Push a project onto the project stack. # Only the top of the stack can be accessed. @doc false - def push(atom, file \\ "nofile") when is_atom(atom) do + def push(atom, file \\ nil) when is_atom(atom) do + file = file || + (atom && String.from_char_data!(atom.__info__(:compile)[:source])) + config = default_config |> Keyword.merge(get_project_config(atom)) |> Keyword.drop(@private_config) @@ -67,9 +70,9 @@ defmodule Mix.Project do # The configuration that is pushed down to dependencies. @doc false def deps_config(config \\ config()) do - [ build_path: build_path(config), - build_per_environment: config[:build_per_environment], - deps_path: deps_path(config) ] + [build_path: build_path(config), + build_per_environment: config[:build_per_environment], + deps_path: deps_path(config)] end @doc """ @@ -133,14 +136,17 @@ defmodule Mix.Project do By default it includes the mix.exs file and the lock manifest. """ def config_files do - project = get - opts = [Mix.Dep.Lock.manifest] - - if project && (source = project.__info__(:compile)[:source]) do - opts = [String.from_char_data!(source)|opts] - end - - opts + [Mix.Dep.Lock.manifest] ++ + case Mix.ProjectStack.peek do + {name, config, file} -> + configs = config[:config_path] || "config/config.exs" + |> Path.dirname + |> Path.join("*.exs") + |> Path.wildcard + [file|configs] + _ -> + [] + end end @doc """ diff --git a/lib/mix/lib/mix/tasks/loadconfig.ex b/lib/mix/lib/mix/tasks/loadconfig.ex index 9e5f2d0e9..ee8b133d3 100644 --- a/lib/mix/lib/mix/tasks/loadconfig.ex +++ b/lib/mix/lib/mix/tasks/loadconfig.ex @@ -71,7 +71,7 @@ defmodule Mix.Tasks.Loadconfig do raise Mix.Error, message: "umbrella child #{inspect dep.app} has set the configuration for " <> "key #{inspect k} in app #{inspect app} to #{inspect v2} but another umbrella child has " <> "already set it to #{inspect v1}. You need to remove the configuration or resolve " <> - "the conflict by setting a value in the umbrella config" + "the conflict by defining a config file and setting a value in your umbrella project" end end) end diff --git a/lib/mix/lib/mix/tasks/new.ex b/lib/mix/lib/mix/tasks/new.ex index dc5c18541..a6f168866 100644 --- a/lib/mix/lib/mix/tasks/new.ex +++ b/lib/mix/lib/mix/tasks/new.ex @@ -79,6 +79,9 @@ defmodule Mix.Tasks.New do create_file "lib/#{app}.ex", lib_app_template(assigns) create_directory "lib/#{app}" create_file "lib/#{app}/supervisor.ex", lib_supervisor_template(assigns) + + create_directory "config" + create_file "config/config.exs", config_template(assigns) end create_directory "test" @@ -259,6 +262,29 @@ defmodule Mix.Tasks.New do end """ + embed_template :config, """ + # This file is responsible for configuring your application and + # its dependencies. It must return a keyword list containing the + # application name and another keyword list with the application + # key-value pairs. + + # Note this configuration is loaded before any dependency and is + # restricted to this project. If another project depends on this + # project, this file won't be loaded nor affect the parent project. + + # You can customize the configuration path by setting :config_path + # in your mix.exs file. For example, you can emulate configuration + # per environment by setting: + # + # config_path: "config/\#{Mix.env}.exs" + # + # Changing any file inside the config directory causes the whole + # project to be recompiled. + + [dep1: [key: :value], + dep2: [key: :value]] + """ + embed_template :lib, """ defmodule <%= @mod %> do end diff --git a/lib/mix/test/mix/project_test.exs b/lib/mix/test/mix/project_test.exs index 157901749..1a0aaed5b 100644 --- a/lib/mix/test/mix/project_test.exs +++ b/lib/mix/test/mix/project_test.exs @@ -5,7 +5,7 @@ defmodule Mix.ProjectTest do defmodule SampleProject do def project do - [ hello: "world" ] + [hello: "world"] end end diff --git a/lib/mix/test/mix/tasks/loadconfig_test.exs b/lib/mix/test/mix/tasks/loadconfig_test.exs index ed7284c76..fb0b3dc1d 100644 --- a/lib/mix/test/mix/tasks/loadconfig_test.exs +++ b/lib/mix/test/mix/tasks/loadconfig_test.exs @@ -7,8 +7,8 @@ defmodule Mix.Tasks.LoadconfigTest do teardown do Enum.each @apps, fn app -> - Enum.each :application.get_all_env(app), fn {key, _} -> - :application.unset_env(app, key, persist: true) + Enum.each Application.get_all_env(app), fn {key, _} -> + Application.delete_env(app, key, persist: true) end end :ok |