Wolfmans Howlings

A programmers Blog about Ruby, Rails and a few other issues

JEdit Ruby/Rails Snippets or superabbrevs

Posted by Jim Morris Sat, 25 Nov 2006 03:05:00 GMT

I imported the rest of the Textmate ruby and rails snippets I had to combine them into the one ruby file though.

I have added about 80 new ones to the existing ones by Scott Becker that I found here

The list of snippets I now have is at the end of this post, note I did change the abbreviation for some of the assertions they all now start with as so I can search for them faster with my select_superabbrevs macro described in the previous post.

download the new snippets from here unzip and replace the ruby file in the .jedit/SuperAbbrevs directory with the one in this zip archive.

You need the Beta version of SuperAbbrevs to use this from here

Super Abbreviations for the ruby mode

--------------------------------------- : --------------------------------------
:${1:key} => ${2:"value"}$end

-------------------------------------- all -------------------------------------
all? { |${1:e}| $end }

-------------------------------------- am --------------------------------------
alias_method :${1:new_name}, :${2:old_name}

-------------------------------------- any -------------------------------------
any? { |${1:e}| $end }

-------------------------------------- app -------------------------------------
if __FILE__ == \$PROGRAM_NAME
    $end
end

-------------------------------------- ase -------------------------------------
assert_equal ${1:value}, @${2:thing}.${3:attr}$end

------------------------------------- asid -------------------------------------
assert_in_delta(${1:expected_float}, ${2:actual_float}, ${3:2 ** -20})

------------------------------------- asio -------------------------------------
assert_instance_of(${1:ExpectedClass}, ${2:actual_instance})$end

------------------------------------- asko -------------------------------------
assert_kind_of ${1:Class}, @${2:thing}$end

-------------------------------------- asm -------------------------------------
assert_match(/${1:expected_pattern}/, ${2:actual_string})

-------------------------------------- asn -------------------------------------
assert_nil(${1:instance})$end

------------------------------------- asne -------------------------------------
assert_not_equal(${1:unexpected}, ${2:actual})

------------------------------------- asnm -------------------------------------
assert_no_match(/${1:unexpected_pattern}/, ${2:actual_string})

------------------------------------- asnn -------------------------------------
assert_not_nil ${1:true}$end

------------------------------------- asnr -------------------------------------
assert_nothing_raised(${1:Exception}) { $end }

------------------------------------- asns -------------------------------------
assert_not_same(${1:unexpected}, ${2:actual})$end

------------------------------------- asnt -------------------------------------
assert_nothing_thrown { $end }

-------------------------------------- aso -------------------------------------
assert_operator(${1:left}, :${2:operator}, ${3:right})

-------------------------------------- asr -------------------------------------
assert_raise(${1:ActiveRecord::RecordNotFound}) { ${2:Class}.find(@${3:thing}.${4:id}) }$end

------------------------------------- asrar ------------------------------------
assert_raise(${1:ActiveRecord::RecordNotFound}) { ${2:Class}.find(@${3:thing}.${4:id}) }

------------------------------------- asre -------------------------------------
assert_response :${1:success}$end

------------------------------------- asrt -------------------------------------
assert_redirected_to :action => "${1:index}"$end

-------------------------------------- ass -------------------------------------
assert(${1:test}, "${2:Failure message.}")

------------------------------------- assm -------------------------------------
assert_same(${1:expected}, ${2:actual})$end

-------------------------------------- ast -------------------------------------
assert_throws(:${1:expected}) { $end }

-------------------------------------- bt --------------------------------------
belongs_to :${1:object}, :class_name => "${2:ClassName}", :foreign_key => "${3:foreign_key}_id"$end

------------------------------------- case -------------------------------------
case ${1:object}
when ${2:condition}
    $end
end

-------------------------------------- cl --------------------------------------
classify { |${1:e}| $end }

------------------------------------- clafn ------------------------------------
split("::").inject(Object) { |par, const| par.const_get(const) }

------------------------------------- class ------------------------------------
class ${1:ClassName}
    $end
end

-------------------------------------- col -------------------------------------
collect { |${1:e}| $end }

------------------------------------ collect -----------------------------------
collect { |${1:element}| ${1:element}.$2 }$end

------------------------------------- Comp -------------------------------------
include Comparable

def <=>(other)
    $end
end

-------------------------------------- dee -------------------------------------
Marshal.load(Marshal.dump(${1:obj_to_copy}))

-------------------------------------- def -------------------------------------
def ${1:method_name}
  $end
end

------------------------------------- defd -------------------------------------
def_delegator :${1:@del_obj}, :${2:del_meth}, :${3:new_name}

------------------------------------- defds ------------------------------------
def_delegators :${1:@del_obj}, :${2:del_methods}

------------------------------------- defs -------------------------------------
def self.${1:class_method_name}
    $end
end

------------------------------------- deft -------------------------------------
def test_${1:case_name}
    $end
end

-------------------------------------- det -------------------------------------
detect { |${1:e}| $end }

-------------------------------------- do --------------------------------------
do
    $end
end

-------------------------------------- doo -------------------------------------
do |${1:object}|
    $end
end

-------------------------------------- ea --------------------------------------
each { |${1:e}| $end }

-------------------------------------- eab -------------------------------------
each_byte { |${1:byte}| $end }

-------------------------------------- eac -------------------------------------
each_char { |${1:chr}| $end }

------------------------------------- each -------------------------------------
each { |${1:element}| ${1:element}.$end }

-------------------------------- each_with_index -------------------------------
each_with_index { |${1:element}, ${2:idx}| ${1:element}.$end }

-------------------------------------- eai -------------------------------------
each_index { |${1:i}| $end }

-------------------------------------- eak -------------------------------------
each_key { |${1:key}| $end }

-------------------------------------- eal -------------------------------------
each_line$1 { |${2:line}| $end }

-------------------------------------- eap -------------------------------------
each_pair { |${1:name}, ${2:val}| $end }

------------------------------------- easl -------------------------------------
each_slice(${1:2}) { |${2:group}| $end }

-------------------------------------- eav -------------------------------------
each_value { |${1:val}| $end }

------------------------------------- eawi -------------------------------------
each_with_index { |${1:e}, ${2:i}| $end }

------------------------------------- Enum -------------------------------------
include Enumerable

def each(&block)
    $end
end

-------------------------------------- fin -------------------------------------
find { |${1:e}| $end }

------------------------------------- fina -------------------------------------
find_all { |${1:e}| $end }

-------------------------------------- fl --------------------------------------
flunk("${1:Failure message.}")

------------------------------------- flao -------------------------------------
inject(Array.new) { |${1:arr}, ${2:a}| ${1:arr}.push(*${2:a}) }

------------------------------------- flash ------------------------------------
flash[:${1:notice}] = "${2:Successfully created...}"$end

------------------------------------- forin ------------------------------------
for ${1:element} in ${2:collection}
    ${1:element}.$3
end$end

------------------------------------- Forw -------------------------------------
extend Forwardable

-------------------------------------- gre -------------------------------------
grep(${1:/${2:pattern}/}) { |${3:match}| $end }

------------------------------------- Hash -------------------------------------
Hash.new { |${1:hash}, ${2:key}| ${1:hash}[${2:key}] = $end }

-------------------------------------- hm --------------------------------------
has_many :${1:objects}, :class_name => "${2:ClassName}", :foreign_key => "${3:foreign_key}_id"$end

-------------------------------------- ho --------------------------------------
has_one :${1:object}, :class_name => "${2:ClassName}", :foreign_key => "${3:foreign_key}_id"$end

-------------------------------------- if --------------------------------------
if ${1:condition}
    $end
end

-------------------------------------- ife -------------------------------------
if ${1:condition}
    $2
else
    $3
end

-------------------------------------- inj -------------------------------------
inject(${1:init}) { |${2:mem}, ${3:var}| $end }

------------------------------------ inject ------------------------------------
inject(${1:object}) { |${2:injection}, ${3:element}| $4 }$end

------------------------------------- logi -------------------------------------
logger.info "${1:Current value is...}"$end

-------------------------------------- mac -------------------------------------
add_column :${1:table_name}, :${2:column_name}, :${3:string}$end

-------------------------------------- mai -------------------------------------
add_index :${1:table_name},[:${2:column_name}], :name => "${2:column_name}_index"$end

-------------------------------------- map -------------------------------------
map { |${1:e}| $end }

------------------------------------- mapwi ------------------------------------
enum_with_index.map { |${1:e}, ${2:i}| $end }

-------------------------------------- max -------------------------------------
max { |a, b| $end }

-------------------------------------- mcc -------------------------------------
change_column :${1:table_name}, :${2:column_name}, :${3:string}, ${4:default => 1}$end

-------------------------------------- mct -------------------------------------
create_table :${1:table_name} do |t|
  $2
end

-------------------------------------- Md --------------------------------------
File.open(${1:"${2:path/to/file}.dump"}, "w") { |${3:file}| Marshal.dump(${4:obj}, ${3:file}) }

-------------------------------------- mdt -------------------------------------
drop_table :${1:table_name}$end

-------------------------------------- mex -------------------------------------
execute "$1"$end

-------------------------------------- min -------------------------------------
min { |a, b| $end }

-------------------------------------- mm --------------------------------------
def method_missing(meth, *args, &block)
    $end
end

-------------------------------------- mnc -------------------------------------
rename_column :${1:table_name}, :${2:column_name}, :${3:new_column}$end

-------------------------------------- mod -------------------------------------
module ${1:ModuleName}
    module_function

    $end
end

-------------------------------------- mrc -------------------------------------
remove_column :${1:table_name}, :${2:column_name}$end

-------------------------------------- mri -------------------------------------
remove_index :${1:table_name}, :${2:column_name}$end

-------------------------------------- mtc -------------------------------------
t.column :${1:column_name}, :${2:string}$end

--------------------------------------- p --------------------------------------
params[:${1:id}]$end

-------------------------------------- par -------------------------------------
partition { |${1:e}| $end }

--------------------------------------- r --------------------------------------
attr_reader :${1:attr_names}

-------------------------------------- ra --------------------------------------
render :action => "${1:action}"$end

-------------------------------------- ral -------------------------------------
render :action => "${1:action}", :layout => "${2:layoutname}"$end

-------------------------------------- ran -------------------------------------
sort_by { rand }

-------------------------------------- rb --------------------------------------
#!/usr/bin/env ruby -w



------------------------------------- rcea -------------------------------------
render_component :action => "${1:index}"$end

------------------------------------- rcec -------------------------------------
render_component :controller => "${1:items}"$end

------------------------------------- rceca ------------------------------------
render_component :controller => "${1:items}", :action => "${2:index}"$end

-------------------------------------- rea -------------------------------------
redirect_to :action => "${1:index}"$end

------------------------------------- reai -------------------------------------
redirect_to :action => "${1:show}", :id => ${2:@item}$end

-------------------------------------- rec -------------------------------------
redirect_to :controller => "${1:items}"$end

------------------------------------- reca -------------------------------------
redirect_to :controller => "${1:items}", :action => "${2:list}"$end

------------------------------------- recai ------------------------------------
redirect_to :controller => "${1:items}", :action => "${2:show}", :id => ${3:@item}$end

-------------------------------------- rej -------------------------------------
reject { |${1:e}| $end }

------------------------------------ reject ------------------------------------
reject { |${1:element}| ${1:element}.$end }

-------------------------------------- req -------------------------------------
require "$end"

------------------------------------- reve -------------------------------------
reverse_each { |${1:e}| $end }

-------------------------------------- rf --------------------------------------
render :file => "${1:filepath}"$end

-------------------------------------- rfu -------------------------------------
render :file => "${1:filepath}", :use_full_path => ${2:false}$end

-------------------------------------- ri --------------------------------------
render :inline => "${1:<%= 'hello' %>}"$end

-------------------------------------- ril -------------------------------------
render :inline => "${1:<%= 'hello' %>}", :locals => { ${2::name} => "${3:value}"$4 }$end

-------------------------------------- rit -------------------------------------
render :inline => "${1:<%= 'hello' %>}", :type => ${2::rxml}$end

-------------------------------------- rl --------------------------------------
render :layout => "${1:layoutname}"$end

-------------------------------------- rn --------------------------------------
render :nothing => ${1:true}$end

-------------------------------------- rns -------------------------------------
render :nothing => ${1:true}, :status => ${2:401}$end

-------------------------------------- rp --------------------------------------
render :partial => "${1:item}"$end

-------------------------------------- rpc -------------------------------------
render :partial => "${1:item}", :collection => ${2:items}$end

-------------------------------------- rpl -------------------------------------
render :partial => "${1:item}", :locals => { :${2:name} => "${3:value}"$4 }$end

-------------------------------------- rpo -------------------------------------
render :partial => "${1:item}", :object => ${2:object}$end

-------------------------------------- rps -------------------------------------
render :partial => "${1:item}", :status => ${2:500}$end

-------------------------------------- rt --------------------------------------
render :text => "${1:text to render...}"$end

-------------------------------------- rtl -------------------------------------
render :text => "${1:text to render...}", :layout => "${2:layoutname}"$end

------------------------------------- rtlt -------------------------------------
render :text => "${1:text to render...}", :layout => ${2:true}$end

-------------------------------------- rts -------------------------------------
render :text => "${1:text to render...}", :status => ${2:401}$end

-------------------------------------- rw --------------------------------------
attr_accessor :${1:attr_names}

--------------------------------------- s --------------------------------------
session[:${1:user}]$end

-------------------------------------- sca -------------------------------------
scan(/${1:pattern}/) { |${2:match}| $end }

-------------------------------------- sel -------------------------------------
select { |${1:e}| $end }

------------------------------------ select ------------------------------------
select { |${1:element}| ${1:element}.$2 }$end

-------------------------------------- sor -------------------------------------
sort { |a, b| $end }

------------------------------------- sorb -------------------------------------
sort_by { |${1:e}| $end }

-------------------------------------- tc --------------------------------------
require "test/unit"

require "${1:library_file_name}"

deli    delete_if { |${1:e}| $end }

-------------------------------------- ts --------------------------------------
require "test/unit"

require "tc_${1:test_case_file}"
require "tc_${2:test_case_file}"

-------------------------------------- uni -------------------------------------
ARGF.each_line$1 do |${2:line}|
    $end
end

------------------------------------ unless ------------------------------------
unless ${1:condition}
    $end
end

------------------------------------- usai -------------------------------------
if ARGV.$1
  puts "Usage:  #{\$PROGRAM_NAME} ${2:ARGS_GO_HERE}"
  exit
end

------------------------------------- usau -------------------------------------
unless ARGV.$1
  puts "Usage:  #{\$PROGRAM_NAME} ${2:ARGS_GO_HERE}"
  exit
end

-------------------------------------- va --------------------------------------
validates_associated :${1:attribute}, :on => "${2:create}"$end

------------------------------------- vaif -------------------------------------
validates_associated :${1:attribute}, :on => "${2:create}", :if => proc { |obj| ${3:obj.condition?} }$end

-------------------------------------- vc --------------------------------------
validates_confirmation_of :${1:attribute}, :on => "${2:create}", :message => "${3:should match confirmation}"$end

------------------------------------- vcif -------------------------------------
validates_confirmation_of :${1:attribute}, :on => "${2:create}", :message => "${3:should match confirmation}", :if => proc { |obj| ${4:obj.condition?} }$end

-------------------------------------- ve --------------------------------------
validates_exclusion_of :${1:attribute}, :in => ${2:enumerable}, :on => "${3:create}", :message => "${4:is not allowed}"$end

------------------------------------- veif -------------------------------------
validates_exclusion_of :${1:attribute}, :in => ${2:enumerable}, :on => "${3:create}", :message => "${4:is not allowed}", :if => proc { |obj| ${5:obj.condition?} }$end

------------------------------------ verify ------------------------------------
verify :only => [:$1], :method => :post, :render => {:status => 500, :text => "use HTTP-POST"}$end

------------------------------------ verifyr -----------------------------------
verify :only => [:$1], :session => :user, :params => :id, :redirect_to => {:action => '${2:index}'}$end

------------------------------------- vlen -------------------------------------
validates_length_of ${1::name}, :maximum=>${2:10}, :message=>"${3:less than %d if you don't mind}"

-------------------------------------- vp --------------------------------------
validates_presence_of :${1:attribute}, :on => "${2:create}", :message => "${3:must be present}"$end

------------------------------------- vpif -------------------------------------
validates_presence_of :${1:attribute}, :on => "${2:create}", :message => "${3:must be present}", :if => proc { |obj| ${4:obj.condition?} }$end

-------------------------------------- vu --------------------------------------
validates_uniqueness_of :${1:attribute}, :on => "${2:create}", :message => "${3:must be unique}"$end

------------------------------------- vuif -------------------------------------
validates_uniqueness_of :${1:attribute}, :on => "${2:create}", :message => "${3:must be unique}", :if => proc { |obj| ${4:obj.condition?} }$end

--------------------------------------- w --------------------------------------
attr_writer :${1:attr_names}

------------------------------------- when -------------------------------------
when ${1:condition}
    $end

------------------------------------- ydump ------------------------------------
File.open(${1:"${2:path/to/file}.yaml"}, "w") { |${3:file}| YAML.dump(${4:obj}, ${3:file}) }

------------------------------------- yload ------------------------------------
File.open(${1:"${2:path/to/file}.yaml"}) { |${3:file}| YAML.load(${3:file}) }

-------------------------------------- zip -------------------------------------
zip(${1:enums}) { |${2:row}| $end }

Posted in , ,  | Tags , , , ,  | 8 comments | no trackbacks

JEdit - Textmate for the rest of us?

Posted by Jim Morris Wed, 22 Nov 2006 07:20:07 GMT

OK after expounding on how much I like Epsilon as an editor, I finally hit a wall with it. I wanted tabs of the currently open files, and it simply does not provide that facility yet. (I know all modern editors do!). On X11 (IE Linux and OS/X) Epsilon is not really a graphical app, it does open an X11 window, but within it most dialogs and lists (like buffer lists etc) are text, a little like a curses app. The advantage is it opens really fast, and loads huge files pretty fast. The downside is you don't get all those nice windowing facilities like movable pop-up windows, dialogs, trees and tabs!

So after hearing so much about JEdit, I bit the bullet and gave it a whirl. Following the instructions from a number of blogs (too numerous to mention) I installed it on my KUbuntu Linux box, with all the plugins people recommend, and of course the Ruby plugin and SupperAbbrevs (The beta version), so I have my snippets!

Being used to an Emacs like editor I had to fuss around remapping all my keys (or at least some of them), then map the new plugins to reasonable key-bindings (called shortcuts in JEdit lingo).

I have to say I am pretty impressed, it does everything I want (or almost). It has a plugin for tabs, it has a project manager, and HTML/XML modes. The Ruby mode is pretty good, modeled after Textmate.

So the things that were missing that I had become accustomed to were.

  • Expanding #{} in a string when you type # - Wrote a macro to do that
  • Being able to select a snippet (called abbreviation in the lingo) from a list rather than having to remember the abbreviation. - wrote a macro to do it
  • Some Emacs like behavior like control-K etc. - found it in the macros manager
  • Toggling between view and controller (and model) in a rails project - Wrote one, but found a better one in macros
  • Being able to use snippets (OK abbreviations!) from a different language mode. eg I'm in rhtml but I want an HTML abbreviation, or vice versa. - wrote a macro to do it
  • Running ruby scripts into a local buffer - the console plugin solved this
  • Running Focused Ruby Unit tests - wrote a macro to do it

So at first I thought I would have to hack the various plugins, but then I discovered the beanShell macros are extremely powerful, basically they are java syntax, but have full access to the JEdit internals and most of the plugins internals too. As Java is my second favorite language, this was a nice surprise. (You should guess by now Ruby is my current favorite language).

So I went about writing macros to fill in the gaps, to see if I could switch to JEdit.

As a side note I will not buy a Mac so I can use Textmate! as awesome as Textmate is, it is not enough to get me to switch, so I will get JEdit as close to it as I can, maybe even further. (I worked on a MacBook Pro Intel for 3+ months, and was not sold).

Yes JEdit actually has intellisense-like completion for Ruby. A previous blog entry of mine explained how I tried Komodo to get this, but it was not very good, however the JEdit version found in the latest RubyPlugin, works pretty well, I think it uses parsing from the JRuby project, so no surprise it works.

In addition it also has a pretty good ruby and rails help, that is linked to the autocomplete, it is missing a few rails entries, but is very handy. Features of this clever plugin are found here.

All this and JEdit is free!

Back to the macro writing.

The first hack, er I mean Macro, was to expand # to #{} when it is in a string. This was relatively easy, as shown below. I just has to make sure I was actually in a string, and take into account escaped quotes and escaped #. Anyway the macro works quite well, I think it would be better off in the RubyPlugin, but never mind.

It took me a while to think of beanshell as Java, I kept wanting to leave off ; and other scripting language things, but it really is Java, just scripted, very impressive language, I have no idea how they do it, if they compile on the fly or interpret, but it works, and you have full access to Swing, and as I have written many thousands of lines of Java/Swing code over the years, I was back in business. (It did take me a while to remember that stuff though as my Swing Feng was a few years old).

// Expand # to #{} if in quotes
boolean escaped() {
  pos= textArea.getCaretPosition();
  str= textArea.getText(pos-1, 1);
  return(str.equals("\\"));
}

boolean inQuotes() {
  cl= textArea.getCaretLine();
  str= textArea.getLineText(cl);
  pos= (textArea.getCaretPosition() - textArea.getLineStartOffset(cl))-1;
  cnt= 0;
  do{
    off= str.lastIndexOf("\"", pos);
    if(off < 0)
      break;
    else if(off == 0 || str.charAt(off-1) != '\\')
      cnt++;
    pos= off-1;
  }while(pos >= 0);
  return (cnt & 1) != 0;
}

if((buffer.getMode().getName().equals("ruby") || buffer.getMode().getName().equals("rhtml")) && inQuotes() && !escaped()){
  textArea.setSelectedText("#{}");
  textArea.goToPrevCharacter(false);
}else{
    textArea.setSelectedText("#");
}

The next one was recreating my Epsilon Hack that toggles between view and controller. I found a great starting macro which toggled between model and controller, so I just modified that a bit, using the same basic technique, and within a few hours I had my view/controller toggle. It may not be as robust as my Epsilon version, as it does a pretty naive search to find what method it is in, in the controller, but it seems to work. Of course, as with Textmates version, it only works if the rails naming conventions are followed. Download from here

UPDATE I have found a much better implementation with all the bells and whistles of the Textmate version here.

The big one was getting a list up to show the SupperAbbrevs snippets for any given mode, and selecting the one you want. This took a good part of a day, I had to study the SupperAbbrevs code, and I found a good starting point with a macro that lists abbreviations from the built-in JEdit abbreviations. I use a good part of that, and just changed it so you can select one of the abbreviations and use that to insert the snippet. I ran into a few quirks with JTable and selection, but that was because I was pretty rusty, I have solved those problems before, I had just forgotten how to.

So now I can type a portion or just one letter of an abbreviation, hit Alt-Space and I get a pop-up with all the abbreviations of the current mode, and the first one that matches what I have typed so far already selected. Hitting enter will insert that one, or I can select the one I want, and hit enter or click Expand, and it does its magic. I need this as I can never remember all those abbreviations, for all the rails and ruby snippets. (Not even Textmate can do that!) Of course I can still type in the whole abbreviation and hit TAB to get the quick expansion as before.

Now something that Textmate does that I like is you can click on an icon and you get a list of all the modes then you can select a snippet from any mode and insert it. I don't think macros will let you do exactly that, (I think that would require a plugin), but the next best thing is hitting Alt-Space with nothing in front of it, and I get a pop up. I can select the mode I want then the snippet from the table, and click insert, and it does the same thing. Just a few extra mouse clicks than Textmate, but close enough for me.

Now I just have to figure out how to contribute these macros to the Textmate community, the Wiki is closed to registered users only, and I got no reply when I tried to register.

I'll put all my macros here for now, until they register me. Then you will be able to download these macros from the nice MacroManager plugin they have in JEdit. So far there seems to be a plugin or macro for almost everything. The other macros are all available via the macroManager plugin, including the toggle between model and controller one. The Emacs macros are also available from the same utility.

I should say I am using the 4.2stable version of JEdit, I played with 4.3pre8 but it was throwing a lot of exceptions on the screen, although none fatal, but it did seem to be a little slower, I'll wait for it become more stable before trying again.

The RubyPlugin is available from the plugin manager, the latest version of superAbbrevs is not however, I got it from this link explained here

These were the most useful howtos I found ruby-rails-jedit, and jedit-for-ruby-rails-development.

but there are literally hundreds around.

Another idea taken from Textmate is a macro that runs the current Ruby Unit Test case, download from here. It will run a focused test only on the testcase the cursor is currently in.

Here is the download link for the biggest Macro I have done to date, it is the one that allows you to select a SuperAbbrev snippet.

Posted in , ,  | Tags , , ,  | 5 comments | no trackbacks

Allow a different local and remote subversion repository path for Capistrano

Posted by Jim Morris Wed, 15 Nov 2006 22:04:28 GMT

One of the things that bugs me about Capistrano is the requirement that access to the remote subversion repository have the same path from the local machine and the remote machine. This is never the case in my experience.

I have been getting around it by setting the path to svn://localhost/... and running ssh locally to port forward the SVN port to the remote host. This sucks as I usually forget to run ssh in another window first.

So I added an optional configuration variable to be set in deploy.rb called local_repository_path, so now you set the repository path as normal, which is the path the remote server uses to access the SVN repository, and you set the path that the local machine (your workstation) uses to access the same repository...

set :repository, "svn://localhost/#{application}/trunk"
set :local_repository_path, "svn+ssh://myremotehost.com/path/to/repostitory/#{application}/trunk"

Ideally one would patch the Capistrano distribution to achieve this, but thanks to the magic of ruby you can patch it from your own setup so I put the following into lib/tasks/patch_capistrano.rb


# Patch the svn scm to allow a local svn repository path as well as
# the remote one as most systems I use have a different path depending
# on whether you access svn from the local machine or remote machine

  module Capistrano

    # override the two scm methods that access svn from the local machine
    module SCM

      class Subversion
        # Return an integer identifying the last known revision in the svn
        # repository. (This integer is currently the revision number.)
        def latest_revision
          @latest_revision ||= begin
            configuration.logger.debug "querying latest revision..."
            match = svn_log(configuration[:local_repository_path]).scan(/r(\d+)/).first or
            raise "Could not determine latest revision"
            match.first
          end
        end

        # Return a string containing the diff between the two revisions. +from+
        # and +to+ may be in any format that svn recognizes as a valid revision
        # identifier. If +from+ is +nil+, it defaults to the last deployed
        # revision. If +to+ is +nil+, it defaults to HEAD.
        def diff(actor, from=nil, to=nil)
          from ||= current_revision(actor)
          to ||= "HEAD"
          `svn diff #{configuration[:local_repository_path]}@#{from} #{configuration[:local_repository_path]}@#{to}`
        end

      end
    end

  end

Then simply add this line to your config/deploy.rb...

require 'lib/tasks/patch_capistrano'

and everything works fine now.

Note there are only two scm methods that access SVN from the local machine, latest_revision and diff. I have patched both.

I'll tidy this up, make the default for local_repository_path be repository, and submit the patch to the Capistrano folks, who will hopefully commit it to the source code, as it won't affect existing users, but will make the rest of us a little happier.

Posted in  | Tags , ,  | 2 comments | no trackbacks

How to protect a form from accidentally losing data

Posted by Jim Morris Wed, 15 Nov 2006 06:47:44 GMT

Here is a good one, your users are complaining that they have spent 5 minutes filling in a web form, then lose it all by clicking on a link away from the form, and back arrow doesn't restore the fields that were entered. (At least on IE).

I saw an interesting design in the Rails Recipes book for saving form data at regular intervals, but I didn't like the overhead of that one.

So I found a snippet of javascript that detects when you are about to switch away from a page, I coupled that with a simple observer that sets a dirty flag when any field is modified in anyway, and you get something that pops up a dialog asking if they really want to leave this page and lose their input data, they get the option to not go to the new page and submit the form instead.

This is not as clean as I'd like, so anyone with ideas of making it cleaner please let me know.

This would go in app/helpers/application.rb...

  # if the given form has changed at all then pop-up a dialog if they
  # try to move away from the form before saving
  # You have to add :html => {:id => 'new_mission_form'} to the form
  # and  :onclick => "dirty_page= false;" to the submit button
  def catch_page_change_if_changed(form, obj= nil)
    <<-END
      <script language="javascript">
          <!--
            dirty_page = #{obj.nil? || obj.errors.empty? ? "false" : "true"};
            new Form.EventObserver(#{form}, function(element, value) {dirty_page= true; });
            window.onbeforeunload = function (evt) {
              if (dirty_page) {
                var message = 'Leaving this page will lose all entered values';
                if (typeof evt == 'undefined') {
                  evt = window.event;
                }
                if (evt) {
                  evt.returnValue = message;
                }
                return message;
              }
            }
         //-->
      </script>
    END
  end

In the view you give the form an id, and you also need to add an :onclick to the submit button so it allows you to submit the form without popping up the dialog...

<% form_for :obj, @obj,  :html => {:id => 'new_form'}, :url => { :action => 'create' } do |f| %>
...
<%= submit_tag "Submit", :onclick => "dirty_page= false;" %>

then call this somewhere in that view...

catch_page_change_if_changed('new_form', @obj)

Basically I just set a javascript variable called dirty_page to false, and have the observer set it to true if anything changes. The optional object being passed in to the helper is used to detect if we are back on the form due to input errors, in which case we set the dirty_flag to true, so we don't lose the fields that were not in error.

Oh this only works if javascript is enabled (of course!)

Posted in  | Tags ,  | 2 comments | no trackbacks