From 4f95f3bd8abdb8fd3cce346f26da3d2fcdc74030 Mon Sep 17 00:00:00 2001 From: Robin Daugherty Date: Sat, 31 Oct 2020 11:21:30 -0400 Subject: [PATCH 1/8] Refactor to Editor module and call pattern Adding support for BETTER_ERRORS_EDITOR env var. --- lib/better_errors.rb | 49 +++++++------------- lib/better_errors/editor.rb | 81 +++++++++++++++++++++++++++++++++ lib/better_errors/error_page.rb | 2 +- spec/better_errors_spec.rb | 30 ++++++------ spec/spec_helper.rb | 1 + 5 files changed, 114 insertions(+), 49 deletions(-) create mode 100644 lib/better_errors/editor.rb diff --git a/lib/better_errors.rb b/lib/better_errors.rb index 8cd6b7ec..169c1171 100644 --- a/lib/better_errors.rb +++ b/lib/better_errors.rb @@ -11,20 +11,9 @@ require "better_errors/raised_exception" require "better_errors/repl" require "better_errors/stack_frame" +require "better_errors/editor" module BetterErrors - POSSIBLE_EDITOR_PRESETS = [ - { symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" }, - { symbols: [:macvim, :mvim], sniff: /vim/i, url: proc { |file, line| "mvim://open?url=file://#{file}&line=#{line}" } }, - { symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" }, - { symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" }, - { symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" }, - { symbols: [:rubymine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" }, - { symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}" }, - { symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" }, - { symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" }, - ] - class << self # The path to the root of the application. Better Errors uses this property # to determine if a file in a backtrace should be considered an application @@ -64,17 +53,18 @@ class << self @maximum_variable_inspect_size = 100_000 @ignored_classes = ['ActionDispatch::Request', 'ActionDispatch::Response'] - # Returns a proc, which when called with a filename and line number argument, + # Returns an object which responds to #url, which when called with + # a filename and line number argument, # returns a URL to open the filename and line in the selected editor. # # Generates TextMate URLs by default. # - # BetterErrors.editor["/some/file", 123] + # BetterErrors.editor.url("/some/file", 123) # # => txmt://open?url=file:///some/file&line=123 # # @return [Proc] def self.editor - @editor + @editor ||= default_editor end # Configures how Better Errors generates open-in-editor URLs. @@ -115,20 +105,17 @@ def self.editor # @param [Proc] proc # def self.editor=(editor) - POSSIBLE_EDITOR_PRESETS.each do |config| - if config[:symbols].include?(editor) - return self.editor = config[:url] - end - end - - if editor.is_a? String - self.editor = proc { |file, line| editor % { file: URI.encode_www_form_component(file), line: line } } + if editor.respond_to? :url + @editor = editor + elsif editor.is_a? Symbol + @editor = Editor.for_symbol(editor) + raise(ArgumentError, "Symbol #{editor} is not a symbol in the list of supported errors.") unless editor + elsif editor.is_a? String + @editor = Editor.for_formatting_string(editor) + elsif editor.respond_to? :call + @editor = Editor.for_proc(editor) else - if editor.respond_to? :call - @editor = editor - else - raise TypeError, "Expected editor to be a valid editor key, a format string or a callable." - end + raise ArgumentError, "Expected editor to be a valid editor key, a format string or a callable." end end @@ -145,12 +132,8 @@ def self.use_pry! # # @return [Symbol] def self.default_editor - POSSIBLE_EDITOR_PRESETS.detect(-> { {} }) { |config| - ENV["EDITOR"] =~ config[:sniff] - }[:url] || :textmate + Editor.default_editor end - - BetterErrors.editor = default_editor end begin diff --git a/lib/better_errors/editor.rb b/lib/better_errors/editor.rb new file mode 100644 index 00000000..c6022322 --- /dev/null +++ b/lib/better_errors/editor.rb @@ -0,0 +1,81 @@ +require "uri" + +module BetterErrors + module Editor + KNOWN_EDITORS = [ + { symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" }, + { symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" }, + { symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" }, + { symbols: [:macvim, :mvim], sniff: /vim/i, url: "mvim://open?url=file://%{file_unencoded}&line=%{line}" }, + { symbols: [:rubymine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" }, + { symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" }, + { symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" }, + { symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}" }, + { symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" }, + ] + + class UsingFormattingString + def initialize(url_formatting_string) + @url_formatting_string = url_formatting_string + end + + def url(file, line) + url_formatting_string % { file: URI.encode_www_form_component(file), file_unencoded: file, line: line } + end + + private + + attr_reader :url_formatting_string + end + + class UsingProc + def initialize(url_proc) + @url_proc = url_proc + end + + def url(file, line) + url_proc.call(file, line) + end + + private + + attr_reader :url_proc + end + + def self.for_formatting_string(formatting_string) + UsingFormattingString.new(formatting_string) + end + + def self.for_proc(url_proc) + UsingProc.new(url_proc) + end + + def self.for_symbol(symbol) + KNOWN_EDITORS.each do |preset| + return for_formatting_string(preset[:url]) if preset[:symbols].include?(symbol) + end + end + + # Automatically sniffs a default editor preset based on the EDITOR + # environment variable. + # + # @return [Symbol] + def self.default_editor + editor_command = ENV["EDITOR"] || ENV["BETTER_ERRORS_EDITOR"] + if editor_command + editor = editor_from_command(editor_command) + return editor if editor + + puts "Since EDITOR or BETTER_ERRORS_EDITOR environment variable are not recognized, using Textmate by default." + else + puts "Since there is no EDITOR or BETTER_ERRORS_EDITOR environment variable, using Textmate by default." + end + for_symbol(:textmate) + end + + def self.editor_from_command(editor_command) + env_preset = KNOWN_EDITORS.find { |preset| editor_command =~ preset[:sniff] } + for_formatting_string(env_preset[:url]) if env_preset + end + end +end diff --git a/lib/better_errors/error_page.rb b/lib/better_errors/error_page.rb index 0eb1081d..1e44bd6e 100644 --- a/lib/better_errors/error_page.rb +++ b/lib/better_errors/error_page.rb @@ -94,7 +94,7 @@ def first_frame private def editor_url(frame) - BetterErrors.editor[frame.filename, frame.line] + BetterErrors.editor.url(frame.filename, frame.line) end def rack_session diff --git a/spec/better_errors_spec.rb b/spec/better_errors_spec.rb index 9d796105..2ab2a486 100644 --- a/spec/better_errors_spec.rb +++ b/spec/better_errors_spec.rb @@ -3,45 +3,45 @@ describe BetterErrors do context ".editor" do it "defaults to textmate" do - expect(subject.editor["foo.rb", 123]).to eq("txmt://open?url=file://foo.rb&line=123") + expect(subject.editor.url("foo.rb", 123)).to eq("txmt://open?url=file://foo.rb&line=123") end it "url escapes the filename" do - expect(subject.editor["&.rb", 0]).to eq("txmt://open?url=file://%26.rb&line=0") + expect(subject.editor.url("&.rb", 0)).to eq("txmt://open?url=file://%26.rb&line=0") end [:emacs, :emacsclient].each do |editor| it "uses emacs:// scheme when set to #{editor.inspect}" do subject.editor = editor - expect(subject.editor[]).to start_with "emacs://" + expect(subject.editor.url("file", 42)).to start_with "emacs://" end end [:macvim, :mvim].each do |editor| it "uses mvim:// scheme when set to #{editor.inspect}" do subject.editor = editor - expect(subject.editor[]).to start_with "mvim://" + expect(subject.editor.url("file", 42)).to start_with "mvim://" end end [:sublime, :subl, :st].each do |editor| it "uses subl:// scheme when set to #{editor.inspect}" do subject.editor = editor - expect(subject.editor[]).to start_with "subl://" + expect(subject.editor.url("file", 42)).to start_with "subl://" end end [:textmate, :txmt, :tm].each do |editor| it "uses txmt:// scheme when set to #{editor.inspect}" do subject.editor = editor - expect(subject.editor[]).to start_with "txmt://" + expect(subject.editor.url("file", 42)).to start_with "txmt://" end end [:atom].each do |editor| it "uses atom:// scheme when set to #{editor.inspect}" do subject.editor = editor - expect(subject.editor[]).to start_with "atom://" + expect(subject.editor.url("file", 42)).to start_with "atom://" end end @@ -49,7 +49,7 @@ it "uses emacs:// scheme when EDITOR=#{editor}" do ENV["EDITOR"] = editor subject.editor = subject.default_editor - expect(subject.editor[]).to start_with "emacs://" + expect(subject.editor.url("file", 42)).to start_with "emacs://" end end @@ -57,7 +57,7 @@ it "uses mvim:// scheme when EDITOR=#{editor}" do ENV["EDITOR"] = editor subject.editor = subject.default_editor - expect(subject.editor[]).to start_with "mvim://" + expect(subject.editor.url("file", 42)).to start_with "mvim://" end end @@ -65,7 +65,7 @@ it "uses subl:// scheme when EDITOR=#{editor}" do ENV["EDITOR"] = editor subject.editor = subject.default_editor - expect(subject.editor[]).to start_with "subl://" + expect(subject.editor.url("file", 42)).to start_with "subl://" end end @@ -73,7 +73,7 @@ it "uses txmt:// scheme when EDITOR=#{editor}" do ENV["EDITOR"] = editor subject.editor = subject.default_editor - expect(subject.editor[]).to start_with "txmt://" + expect(subject.editor.url("file", 42)).to start_with "txmt://" end end @@ -82,7 +82,7 @@ it "uses atom:// scheme when EDITOR=#{editor}" do ENV["EDITOR"] = editor subject.editor = subject.default_editor - expect(subject.editor[]).to start_with "atom://" + expect(subject.editor.url("file", 42)).to start_with "atom://" end end @@ -90,7 +90,7 @@ it "uses x-mine:// scheme when EDITOR=#{editor}" do ENV["EDITOR"] = editor subject.editor = subject.default_editor - expect(subject.editor[]).to start_with "x-mine://" + expect(subject.editor.url("file", 42)).to start_with "x-mine://" end end @@ -98,7 +98,7 @@ it "uses idea:// scheme when EDITOR=#{editor}" do ENV["EDITOR"] = editor subject.editor = subject.default_editor - expect(subject.editor[]).to start_with "idea://" + expect(subject.editor.url("file", 42)).to start_with "idea://" end end @@ -106,7 +106,7 @@ it "uses vscode:// scheme when EDITOR=#{editor}" do ENV["EDITOR"] = editor subject.editor = subject.default_editor - expect(subject.editor[]).to start_with "vscode://" + expect(subject.editor.url("file", 42)).to start_with "vscode://" end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9828c3f3..31d351ab 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ $: << File.expand_path("../../lib", __FILE__) ENV["EDITOR"] = nil +ENV["BETTER_ERRORS"] = nil require 'simplecov' require 'simplecov-lcov' From c907709a723791f3d5fc129666d51c406afde4dc Mon Sep 17 00:00:00 2001 From: Robin Daugherty Date: Wed, 4 Nov 2020 14:14:14 -0500 Subject: [PATCH 2/8] Support for BETTER_ERRORS_EDITOR_URL --- lib/better_errors/editor.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/better_errors/editor.rb b/lib/better_errors/editor.rb index c6022322..30ce6050 100644 --- a/lib/better_errors/editor.rb +++ b/lib/better_errors/editor.rb @@ -61,6 +61,12 @@ def self.for_symbol(symbol) # # @return [Symbol] def self.default_editor + editor_from_environment_formatting_string || + editor_from_environment_editor || + for_symbol(:textmate) + end + + def self.editor_from_environment_editor editor_command = ENV["EDITOR"] || ENV["BETTER_ERRORS_EDITOR"] if editor_command editor = editor_from_command(editor_command) @@ -70,12 +76,17 @@ def self.default_editor else puts "Since there is no EDITOR or BETTER_ERRORS_EDITOR environment variable, using Textmate by default." end - for_symbol(:textmate) end def self.editor_from_command(editor_command) env_preset = KNOWN_EDITORS.find { |preset| editor_command =~ preset[:sniff] } for_formatting_string(env_preset[:url]) if env_preset end + + def self.editor_from_environment_formatting_string + return unless ENV['BETTER_ERRORS_EDITOR_URL'] + + for_formatting_string(ENV['BETTER_ERRORS_EDITOR_URL']) + end end end From 2b520a0fa5564b69a28c7415a7deacd50553c2d3 Mon Sep 17 00:00:00 2001 From: Robin Daugherty Date: Wed, 4 Nov 2020 15:10:01 -0500 Subject: [PATCH 3/8] Remove new undocumented behavior of passing an object that responds to #url. --- lib/better_errors.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/better_errors.rb b/lib/better_errors.rb index 169c1171..af46bdd5 100644 --- a/lib/better_errors.rb +++ b/lib/better_errors.rb @@ -105,9 +105,7 @@ def self.editor # @param [Proc] proc # def self.editor=(editor) - if editor.respond_to? :url - @editor = editor - elsif editor.is_a? Symbol + if editor.is_a? Symbol @editor = Editor.for_symbol(editor) raise(ArgumentError, "Symbol #{editor} is not a symbol in the list of supported errors.") unless editor elsif editor.is_a? String From 5931137088b0d688745462ca1a9c1553233ef4c6 Mon Sep 17 00:00:00 2001 From: Robin Daugherty Date: Wed, 4 Nov 2020 15:10:32 -0500 Subject: [PATCH 4/8] Specs for Editor module --- lib/better_errors/editor.rb | 17 ++- spec/better_errors/editor_spec.rb | 233 ++++++++++++++++++++++++++++++ spec/better_errors_spec.rb | 129 +++++------------ 3 files changed, 276 insertions(+), 103 deletions(-) create mode 100644 spec/better_errors/editor_spec.rb diff --git a/lib/better_errors/editor.rb b/lib/better_errors/editor.rb index 30ce6050..0e998637 100644 --- a/lib/better_errors/editor.rb +++ b/lib/better_errors/editor.rb @@ -56,8 +56,8 @@ def self.for_symbol(symbol) end end - # Automatically sniffs a default editor preset based on the EDITOR - # environment variable. + # Automatically sniffs a default editor preset based on + # environment variables. # # @return [Symbol] def self.default_editor @@ -67,12 +67,15 @@ def self.default_editor end def self.editor_from_environment_editor - editor_command = ENV["EDITOR"] || ENV["BETTER_ERRORS_EDITOR"] - if editor_command - editor = editor_from_command(editor_command) + if ENV["BETTER_ERRORS_EDITOR"] + editor = editor_from_command(ENV["BETTER_ERRORS_EDITOR"]) return editor if editor - - puts "Since EDITOR or BETTER_ERRORS_EDITOR environment variable are not recognized, using Textmate by default." + puts "BETTER_ERRORS_EDITOR environment variable is not recognized as a supported Better Errors editor." + end + if ENV["EDITOR"] + editor = editor_from_command(ENV["EDITOR"]) + return editor if editor + puts "EDITOR environment variable is not recognized as a supported Better Errors editor. Using TextMate by default." else puts "Since there is no EDITOR or BETTER_ERRORS_EDITOR environment variable, using Textmate by default." end diff --git a/spec/better_errors/editor_spec.rb b/spec/better_errors/editor_spec.rb new file mode 100644 index 00000000..efc73608 --- /dev/null +++ b/spec/better_errors/editor_spec.rb @@ -0,0 +1,233 @@ +require "spec_helper" + +RSpec.describe BetterErrors::Editor do + describe ".for_formatting_string" do + it "returns an object that reponds to #url" do + editor = described_class.for_formatting_string("custom://%{file}:%{file_unencoded}:%{line}") + expect(editor.url("/path&file", 42)).to eq("custom://%2Fpath%26file:/path&file:42") + end + end + + describe ".for_proc" do + it "returns an object that responds to #url, which calls the proc" do + editor = described_class.for_proc(proc { |file, line| "result" } ) + expect(editor.url("foo", 42)).to eq("result") + end + end + + describe ".for_symbol" do + subject { described_class.for_symbol(symbol) } + + [:atom].each do |symbol| + context "when symbol is '#{symbol}'" do + let(:symbol) { symbol } + + it "uses atom:// scheme" do + expect(subject.url("file", 42)).to start_with("atom://") + end + end + end + + [:emacs, :emacsclient].each do |symbol| + context "when symbol is '#{symbol}'" do + let(:symbol) { symbol } + it "uses emacs:// scheme" do + expect(subject.url("file", 42)).to start_with("emacs://") + end + end + end + + [:macvim, :mvim].each do |symbol| + context "when symbol is '#{symbol}'" do + let(:symbol) { symbol } + + it "uses mvim:// scheme" do + expect(subject.url("file", 42)).to start_with("mvim://") + end + end + end + + [:sublime, :subl, :st].each do |symbol| + context "when symbol is '#{symbol}'" do + let(:symbol) { symbol } + + it "uses subl:// scheme" do + expect(subject.url("file", 42)).to start_with("subl://") + end + end + end + + [:textmate, :txmt, :tm].each do |symbol| + context "when symbol is '#{symbol}'" do + let(:symbol) { symbol } + + it "uses txmt:// scheme" do + expect(subject.url("file", 42)).to start_with("txmt://") + end + end + end + end + + describe ".default_editor" do + subject(:default_editor) { described_class.default_editor } + before do + ENV['BETTER_ERRORS_EDITOR_URL'] = nil + ENV['BETTER_ERRORS_EDITOR'] = nil + ENV['EDITOR'] = nil + end + + it "returns an object that responds to #url" do + expect(default_editor.url("foo", 123)).to match(/foo/) + end + + context "when $BETTER_ERRORS_EDITOR_URL is set" do + before do + ENV['BETTER_ERRORS_EDITOR_URL'] = "custom://%{file}:%{file_unencoded}:%{line}" + end + + it "uses the value as a formatting string to build the editor URL" do + expect(default_editor.url("/path&file", 42)).to eq("custom://%2Fpath%26file:/path&file:42") + end + end + + context "when $BETTER_ERRORS_EDITOR is set to one of the preset commands" do + before do + ENV['BETTER_ERRORS_EDITOR'] = "subl" + end + + it "returns an object that builds URLs for the corresponding editor" do + expect(default_editor.url("foo", 123)).to start_with('subl://') + end + end + + context "when $EDITOR is set to one of the preset commands" do + before do + ENV['EDITOR'] = "subl" + end + + it "returns an object that builds URLs for the corresponding editor" do + expect(default_editor.url("foo", 123)).to start_with('subl://') + end + + context "when $BETTER_ERRORS_EDITOR is set to one of the preset commands" do + before do + ENV['BETTER_ERRORS_EDITOR'] = "emacs" + end + + it "returns an object that builds URLs for that editor instead" do + expect(default_editor.url("foo", 123)).to start_with('emacs://') + end + end + + context "when $BETTER_ERRORS_EDITOR is set to an unrecognized command" do + before do + ENV['BETTER_ERRORS_EDITOR'] = "fubarcmd" + end + + it "returns an object that builds URLs for the $EDITOR instead" do + expect(default_editor.url("foo", 123)).to start_with('subl://') + end + end + end + + context "when $EDITOR is set to an unrecognized command" do + before do + ENV['EDITOR'] = "fubarcmd" + end + + it "returns an object that builds URLs for TextMate" do + expect(default_editor.url("foo", 123)).to start_with('txmt://') + end + end + + context "when $EDITOR and $BETTER_ERRORS_EDITOR are not set" do + it "returns an object that builds URLs for TextMate" do + expect(default_editor.url("foo", 123)).to start_with('txmt://') + end + end + end + + describe ".editor_from_command" do + subject { described_class.editor_from_command(command_line) } + + ["atom -w", "/usr/bin/atom -w"].each do |command| + context "when editor command is '#{command}'" do + let(:command_line) { command } + + it "uses atom:// scheme" do + expect(subject.url("file", 42)).to start_with("atom://") + end + end + end + + ["emacsclient", "/usr/local/bin/emacsclient"].each do |command| + context "when editor command is '#{command}'" do + let(:command_line) { command } + + it "uses emacs:// scheme" do + expect(subject.url("file", 42)).to start_with("emacs://") + end + end + end + + ["idea"].each do |command| + context "when editor command is '#{command}'" do + let(:command_line) { command } + + it "uses idea:// scheme" do + expect(subject.url("file", 42)).to start_with("idea://") + end + end + end + + ["mate -w", "/usr/bin/mate -w"].each do |command| + context "when editor command is '#{command}'" do + let(:command_line) { command } + + it "uses txmt:// scheme" do + expect(subject.url("file", 42)).to start_with("txmt://") + end + end + end + + ["mine"].each do |command| + context "when editor command is '#{command}'" do + let(:command_line) { command } + + it "uses x-mine:// scheme" do + expect(subject.url("file", 42)).to start_with("x-mine://") + end + end + end + + ["mvim -f", "/usr/local/bin/mvim -f"].each do |command| + context "when editor command is '#{command}'" do + let(:command_line) { command } + + it "uses mvim:// scheme" do + expect(subject.url("file", 42)).to start_with("mvim://") + end + end + end + + ["subl -w", "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl"].each do |command| + context "when editor command is '#{command}'" do + let(:command_line) { command } + + it "uses subl:// scheme" do + expect(subject.url("file", 42)).to start_with("subl://") + end + end + end + + ["vscode", "code"].each do |command| + context "when editor command is '#{command}'" do + let(:command_line) { command } + + it "uses vscode:// scheme" do + expect(subject.url("file", 42)).to start_with("vscode://") + end + end + end + end +end diff --git a/spec/better_errors_spec.rb b/spec/better_errors_spec.rb index 2ab2a486..0b5b6604 100644 --- a/spec/better_errors_spec.rb +++ b/spec/better_errors_spec.rb @@ -1,112 +1,49 @@ require "spec_helper" -describe BetterErrors do - context ".editor" do - it "defaults to textmate" do - expect(subject.editor.url("foo.rb", 123)).to eq("txmt://open?url=file://foo.rb&line=123") - end - - it "url escapes the filename" do - expect(subject.editor.url("&.rb", 0)).to eq("txmt://open?url=file://%26.rb&line=0") - end - - [:emacs, :emacsclient].each do |editor| - it "uses emacs:// scheme when set to #{editor.inspect}" do - subject.editor = editor - expect(subject.editor.url("file", 42)).to start_with "emacs://" - end - end - - [:macvim, :mvim].each do |editor| - it "uses mvim:// scheme when set to #{editor.inspect}" do - subject.editor = editor - expect(subject.editor.url("file", 42)).to start_with "mvim://" - end - end - - [:sublime, :subl, :st].each do |editor| - it "uses subl:// scheme when set to #{editor.inspect}" do - subject.editor = editor - expect(subject.editor.url("file", 42)).to start_with "subl://" - end - end - - [:textmate, :txmt, :tm].each do |editor| - it "uses txmt:// scheme when set to #{editor.inspect}" do - subject.editor = editor - expect(subject.editor.url("file", 42)).to start_with "txmt://" - end - end - - [:atom].each do |editor| - it "uses atom:// scheme when set to #{editor.inspect}" do - subject.editor = editor - expect(subject.editor.url("file", 42)).to start_with "atom://" - end - end - - ["emacsclient", "/usr/local/bin/emacsclient"].each do |editor| - it "uses emacs:// scheme when EDITOR=#{editor}" do - ENV["EDITOR"] = editor - subject.editor = subject.default_editor - expect(subject.editor.url("file", 42)).to start_with "emacs://" +RSpec.describe BetterErrors do + describe ".editor" do + context "when set to a specific value" do + before do + allow(BetterErrors::Editor).to receive(:for_symbol).and_return(:editor_from_symbol) + allow(BetterErrors::Editor).to receive(:for_formatting_string).and_return(:editor_from_formatting_string) + allow(BetterErrors::Editor).to receive(:for_proc).and_return(:editor_from_proc) end - end - ["mvim -f", "/usr/local/bin/mvim -f"].each do |editor| - it "uses mvim:// scheme when EDITOR=#{editor}" do - ENV["EDITOR"] = editor - subject.editor = subject.default_editor - expect(subject.editor.url("file", 42)).to start_with "mvim://" + context "when the value is a string" do + it "uses BetterErrors::Editor.for_formatting_string to set the value" do + subject.editor = "thing://%{file}" + expect(BetterErrors::Editor).to have_received(:for_formatting_string).with("thing://%{file}") + expect(subject.editor).to eq(:editor_from_formatting_string) + end end - end - ["subl -w", "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl"].each do |editor| - it "uses subl:// scheme when EDITOR=#{editor}" do - ENV["EDITOR"] = editor - subject.editor = subject.default_editor - expect(subject.editor.url("file", 42)).to start_with "subl://" + context "when the value is a Proc" do + it "uses BetterErrors::Editor.for_proc to set the value" do + my_proc = proc { "thing" } + subject.editor = my_proc + expect(BetterErrors::Editor).to have_received(:for_proc).with(my_proc) + expect(subject.editor).to eq(:editor_from_proc) + end end - end - ["mate -w", "/usr/bin/mate -w"].each do |editor| - it "uses txmt:// scheme when EDITOR=#{editor}" do - ENV["EDITOR"] = editor - subject.editor = subject.default_editor - expect(subject.editor.url("file", 42)).to start_with "txmt://" + context "when the value is a symbol" do + it "uses BetterErrors::Editor.for_symbol to set the value" do + subject.editor = :subl + expect(BetterErrors::Editor).to have_received(:for_symbol).with(:subl) + expect(subject.editor).to eq(:editor_from_symbol) + end end end - - ["atom -w", "/usr/bin/atom -w"].each do |editor| - it "uses atom:// scheme when EDITOR=#{editor}" do - ENV["EDITOR"] = editor - subject.editor = subject.default_editor - expect(subject.editor.url("file", 42)).to start_with "atom://" + context "when no value has been set" do + before do + BetterErrors.instance_variable_set('@editor', nil) + allow(BetterErrors::Editor).to receive(:default_editor).and_return(:default_editor) end - end - - ["mine"].each do |editor| - it "uses x-mine:// scheme when EDITOR=#{editor}" do - ENV["EDITOR"] = editor - subject.editor = subject.default_editor - expect(subject.editor.url("file", 42)).to start_with "x-mine://" - end - end - - ["idea"].each do |editor| - it "uses idea:// scheme when EDITOR=#{editor}" do - ENV["EDITOR"] = editor - subject.editor = subject.default_editor - expect(subject.editor.url("file", 42)).to start_with "idea://" - end - end - ["vscode", "code"].each do |editor| - it "uses vscode:// scheme when EDITOR=#{editor}" do - ENV["EDITOR"] = editor - subject.editor = subject.default_editor - expect(subject.editor.url("file", 42)).to start_with "vscode://" + it "uses BetterErrors::Editor.default_editor to set the default value" do + expect(subject.editor).to eq(:default_editor) + expect(BetterErrors::Editor).to have_received(:default_editor) end end end From b95d3c334d749a94f162fba1b68c0b9e0f68aa4f Mon Sep 17 00:00:00 2001 From: Robin Daugherty Date: Wed, 4 Nov 2020 15:14:38 -0500 Subject: [PATCH 5/8] Create instances of Editor --- lib/better_errors/editor.rb | 48 +++++++++++++------------------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/lib/better_errors/editor.rb b/lib/better_errors/editor.rb index 0e998637..cc8bf383 100644 --- a/lib/better_errors/editor.rb +++ b/lib/better_errors/editor.rb @@ -1,7 +1,7 @@ require "uri" module BetterErrors - module Editor + class Editor KNOWN_EDITORS = [ { symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" }, { symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" }, @@ -14,40 +14,14 @@ module Editor { symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" }, ] - class UsingFormattingString - def initialize(url_formatting_string) - @url_formatting_string = url_formatting_string - end - - def url(file, line) - url_formatting_string % { file: URI.encode_www_form_component(file), file_unencoded: file, line: line } - end - - private - - attr_reader :url_formatting_string - end - - class UsingProc - def initialize(url_proc) - @url_proc = url_proc - end - - def url(file, line) - url_proc.call(file, line) - end - - private - - attr_reader :url_proc - end - def self.for_formatting_string(formatting_string) - UsingFormattingString.new(formatting_string) + new proc { |file, line| + formatting_string % { file: URI.encode_www_form_component(file), file_unencoded: file, line: line } + } end def self.for_proc(url_proc) - UsingProc.new(url_proc) + new url_proc end def self.for_symbol(symbol) @@ -91,5 +65,17 @@ def self.editor_from_environment_formatting_string for_formatting_string(ENV['BETTER_ERRORS_EDITOR_URL']) end + + def initialize(url_proc) + @url_proc = url_proc + end + + def url(file, line) + url_proc.call(file, line) + end + + private + + attr_reader :url_proc end end From 6591cf998872940ae64086444aa59be30f1cbe9c Mon Sep 17 00:00:00 2001 From: Robin Daugherty Date: Wed, 4 Nov 2020 15:16:47 -0500 Subject: [PATCH 6/8] Rename method and reorder --- lib/better_errors/editor.rb | 14 ++-- spec/better_errors/editor_spec.rb | 106 +++++++++++++++--------------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/lib/better_errors/editor.rb b/lib/better_errors/editor.rb index cc8bf383..06df7f4d 100644 --- a/lib/better_errors/editor.rb +++ b/lib/better_errors/editor.rb @@ -24,12 +24,6 @@ def self.for_proc(url_proc) new url_proc end - def self.for_symbol(symbol) - KNOWN_EDITORS.each do |preset| - return for_formatting_string(preset[:url]) if preset[:symbols].include?(symbol) - end - end - # Automatically sniffs a default editor preset based on # environment variables. # @@ -37,7 +31,7 @@ def self.for_symbol(symbol) def self.default_editor editor_from_environment_formatting_string || editor_from_environment_editor || - for_symbol(:textmate) + editor_from_symbol(:textmate) end def self.editor_from_environment_editor @@ -66,6 +60,12 @@ def self.editor_from_environment_formatting_string for_formatting_string(ENV['BETTER_ERRORS_EDITOR_URL']) end + def self.editor_from_symbol(symbol) + KNOWN_EDITORS.each do |preset| + return for_formatting_string(preset[:url]) if preset[:symbols].include?(symbol) + end + end + def initialize(url_proc) @url_proc = url_proc end diff --git a/spec/better_errors/editor_spec.rb b/spec/better_errors/editor_spec.rb index efc73608..033c05d5 100644 --- a/spec/better_errors/editor_spec.rb +++ b/spec/better_errors/editor_spec.rb @@ -15,59 +15,6 @@ end end - describe ".for_symbol" do - subject { described_class.for_symbol(symbol) } - - [:atom].each do |symbol| - context "when symbol is '#{symbol}'" do - let(:symbol) { symbol } - - it "uses atom:// scheme" do - expect(subject.url("file", 42)).to start_with("atom://") - end - end - end - - [:emacs, :emacsclient].each do |symbol| - context "when symbol is '#{symbol}'" do - let(:symbol) { symbol } - it "uses emacs:// scheme" do - expect(subject.url("file", 42)).to start_with("emacs://") - end - end - end - - [:macvim, :mvim].each do |symbol| - context "when symbol is '#{symbol}'" do - let(:symbol) { symbol } - - it "uses mvim:// scheme" do - expect(subject.url("file", 42)).to start_with("mvim://") - end - end - end - - [:sublime, :subl, :st].each do |symbol| - context "when symbol is '#{symbol}'" do - let(:symbol) { symbol } - - it "uses subl:// scheme" do - expect(subject.url("file", 42)).to start_with("subl://") - end - end - end - - [:textmate, :txmt, :tm].each do |symbol| - context "when symbol is '#{symbol}'" do - let(:symbol) { symbol } - - it "uses txmt:// scheme" do - expect(subject.url("file", 42)).to start_with("txmt://") - end - end - end - end - describe ".default_editor" do subject(:default_editor) { described_class.default_editor } before do @@ -230,4 +177,57 @@ end end end + + describe ".editor_from_symbol" do + subject { described_class.editor_from_symbol(symbol) } + + [:atom].each do |symbol| + context "when symbol is '#{symbol}'" do + let(:symbol) { symbol } + + it "uses atom:// scheme" do + expect(subject.url("file", 42)).to start_with("atom://") + end + end + end + + [:emacs, :emacsclient].each do |symbol| + context "when symbol is '#{symbol}'" do + let(:symbol) { symbol } + it "uses emacs:// scheme" do + expect(subject.url("file", 42)).to start_with("emacs://") + end + end + end + + [:macvim, :mvim].each do |symbol| + context "when symbol is '#{symbol}'" do + let(:symbol) { symbol } + + it "uses mvim:// scheme" do + expect(subject.url("file", 42)).to start_with("mvim://") + end + end + end + + [:sublime, :subl, :st].each do |symbol| + context "when symbol is '#{symbol}'" do + let(:symbol) { symbol } + + it "uses subl:// scheme" do + expect(subject.url("file", 42)).to start_with("subl://") + end + end + end + + [:textmate, :txmt, :tm].each do |symbol| + context "when symbol is '#{symbol}'" do + let(:symbol) { symbol } + + it "uses txmt:// scheme" do + expect(subject.url("file", 42)).to start_with("txmt://") + end + end + end + end end From 45915e6a1c5e777f5fb975ef3094642a75fb6b66 Mon Sep 17 00:00:00 2001 From: Robin Daugherty Date: Wed, 4 Nov 2020 15:31:28 -0500 Subject: [PATCH 7/8] Support for virtual and host paths --- lib/better_errors/editor.rb | 20 ++++++++++++++++- spec/better_errors/editor_spec.rb | 37 +++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/better_errors/editor.rb b/lib/better_errors/editor.rb index 06df7f4d..b8de8fb7 100644 --- a/lib/better_errors/editor.rb +++ b/lib/better_errors/editor.rb @@ -70,12 +70,30 @@ def initialize(url_proc) @url_proc = url_proc end - def url(file, line) + def url(raw_path, line) + if virtual_path && raw_path.start_with?(virtual_path) + if host_path + file = raw_path.sub(%r{\A#{virtual_path}}, host_path) + else + file = raw_path.sub(%r{\A#{virtual_path}/}, '') + end + else + file = raw_path + end + url_proc.call(file, line) end private attr_reader :url_proc + + def virtual_path + @virtual_path ||= ENV['BETTER_ERRORS_VIRTUAL_PATH'] + end + + def host_path + @host_path ||= ENV['BETTER_ERRORS_HOST_PATH'] + end end end diff --git a/spec/better_errors/editor_spec.rb b/spec/better_errors/editor_spec.rb index 033c05d5..ca0127a3 100644 --- a/spec/better_errors/editor_spec.rb +++ b/spec/better_errors/editor_spec.rb @@ -230,4 +230,41 @@ end end end + + describe "#url" do + subject(:url) { described_instance.url("/full/path/to/lib/file.rb", 42) } + let(:described_instance) { described_class.for_formatting_string("%{file_unencoded}")} + before do + ENV['BETTER_ERRORS_VIRTUAL_PATH'] = virtual_path + ENV['BETTER_ERRORS_HOST_PATH'] = host_path + end + let(:virtual_path) { nil } + let(:host_path) { nil } + + context "when $BETTER_ERRORS_VIRTUAL_PATH is set" do + let(:virtual_path) { "/full/path/to" } + + context "when $BETTER_ERRORS_HOST_PATH is not set" do + let(:host_path) { nil } + + it "removes the VIRTUAL_PATH prefix, making the path relative" do + expect(url).to eq("lib/file.rb") + end + end + + context "when $BETTER_ERRORS_HOST_PATH is set" do + let(:host_path) { '/Users/myname/Code' } + + it "replaces the VIRTUAL_PATH prefix with the HOST_PATH" do + expect(url).to eq("/Users/myname/Code/lib/file.rb") + end + end + end + + context "when $BETTER_ERRORS_VIRTUAL_PATH is not set" do + it "does not alter file paths" do + expect(url).to eq("/full/path/to/lib/file.rb") + end + end + end end From 123f9b327e8dc9c960c87dfaf876a8d519be57a4 Mon Sep 17 00:00:00 2001 From: Robin Daugherty Date: Wed, 4 Nov 2020 15:33:22 -0500 Subject: [PATCH 8/8] Test invalid editor --- spec/better_errors_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/better_errors_spec.rb b/spec/better_errors_spec.rb index 0b5b6604..fbec6e7c 100644 --- a/spec/better_errors_spec.rb +++ b/spec/better_errors_spec.rb @@ -33,6 +33,12 @@ expect(subject.editor).to eq(:editor_from_symbol) end end + + context "when set to something else" do + it "raises an ArgumentError" do + expect { subject.editor = Class.new }.to raise_error(ArgumentError) + end + end end context "when no value has been set" do