diff --git a/lib/compass/commands/watch_project.rb b/lib/compass/commands/watch_project.rb index 97eb2fe99f..ee0528d8ff 100644 --- a/lib/compass/commands/watch_project.rb +++ b/lib/compass/commands/watch_project.rb @@ -14,17 +14,30 @@ def perform puts "" exit 0 end + + recompile + puts ">>> Compass is watching for changes. Press Ctrl-C to Stop." - loop do - # TODO: Make this efficient by using filesystem monitoring. - compiler = new_compiler_instance(:quiet => true) - remove_obsolete_css(compiler) - recompile(compiler) - sleep 1 + + require File.join(Compass.lib_directory, 'vendor', 'fssm') + + FSSM.monitor do |monitor| + Compass.configuration.sass_load_paths.each do |load_path| + monitor.path load_path do |path| + path.glob '**/*.sass' + + path.update &method(:recompile) + path.delete {|base, relative| remove_obsolete_css(base,relative); recompile(base, relative)} + path.create &method(:recompile) + end + end + end + end - def remove_obsolete_css(compiler) + def remove_obsolete_css(base = nil, relative = nil) + compiler = new_compiler_instance(:quiet => true) sass_files = compiler.sass_files deleted_sass_files = (last_sass_files || []) - sass_files deleted_sass_files.each do |deleted_sass_file| @@ -34,7 +47,8 @@ def remove_obsolete_css(compiler) self.last_sass_files = sass_files end - def recompile(compiler) + def recompile(base = nil, relative = nil) + compiler = new_compiler_instance(:quiet => true) if file = compiler.out_of_date? begin puts ">>> Change detected to: #{file}" diff --git a/lib/vendor/fssm.rb b/lib/vendor/fssm.rb new file mode 100644 index 0000000000..d04b32aad2 --- /dev/null +++ b/lib/vendor/fssm.rb @@ -0,0 +1,30 @@ +module FSSM + FileNotFoundError = Class.new(StandardError) + CallbackError = Class.new(StandardError) + + class << self + def monitor(*args, &block) + monitor = FSSM::Monitor.new + context = args.empty? ? monitor : monitor.path(*args) + if block && block.arity == 0 + context.instance_eval(&block) + elsif block && block.arity == 1 + block.call(context) + end + monitor.run + end + end +end + +$:.unshift(File.dirname(__FILE__)) +require 'pathname' +require 'fssm/ext' +require 'fssm/support' +require 'fssm/path' +require 'fssm/state' +require 'fssm/monitor' + +require "fssm/backends/#{FSSM::Support.backend.downcase}" +FSSM::Backends::Default = FSSM::Backends.const_get(FSSM::Support.backend) +$:.shift + diff --git a/lib/vendor/fssm/backends/fsevents.rb b/lib/vendor/fssm/backends/fsevents.rb new file mode 100644 index 0000000000..0790a92266 --- /dev/null +++ b/lib/vendor/fssm/backends/fsevents.rb @@ -0,0 +1,78 @@ +module FSSM::Backends + class FSEvents + def initialize(options={}) + @streams = [] + @handlers = {} + @allocator = options[:allocator] || OSX::KCFAllocatorDefault + @context = options[:context] || nil + @since = options[:since] || OSX::KFSEventStreamEventIdSinceNow + @latency = options[:latency] || 0.0 + @flags = options[:flags] || 0 + end + + def add_path(path, preload=true) + @handlers["#{path}"] = FSSM::State.new(path, preload) + + cb = lambda do |stream, context, number, paths, flags, ids| + paths.regard_as('*') + watched = OSX.FSEventStreamCopyPathsBeingWatched(stream).first + @handlers["#{watched}"].refresh + # TODO: support this level of granularity + # number.times do |n| + # @handlers["#{watched}"].refresh_path(paths[n]) + # end + end + + @streams << create_stream(cb, "#{path}") + end + + def run + @streams.each do |stream| + schedule_stream(stream) + start_stream(stream) + end + + begin + OSX.CFRunLoopRun + rescue Interrupt + @streams.each do |stream| + stop_stream(stream) + invalidate_stream(stream) + release_stream(stream) + end + end + + end + + private + + def create_stream(callback, paths) + paths = [paths] unless paths.is_a?(Array) + OSX.FSEventStreamCreate(@allocator, callback, @context, paths, @since, @latency, @flags) + end + + def schedule_stream(stream, options={}) + run_loop = options[:run_loop] || OSX.CFRunLoopGetCurrent + loop_mode = options[:loop_mode] || OSX::KCFRunLoopDefaultMode + + OSX.FSEventStreamScheduleWithRunLoop(stream, run_loop, loop_mode) + end + + def start_stream(stream) + OSX.FSEventStreamStart(stream) + end + + def stop_stream(stream) + OSX.FSEventStreamStop(stream) + end + + def invalidate_stream(stream) + OSX.FSEventStreamInvalidate(stream) + end + + def release_stream(stream) + OSX.FSEventStreamRelease(stream) + end + + end +end diff --git a/lib/vendor/fssm/backends/polling.rb b/lib/vendor/fssm/backends/polling.rb new file mode 100644 index 0000000000..0879775705 --- /dev/null +++ b/lib/vendor/fssm/backends/polling.rb @@ -0,0 +1,24 @@ +module FSSM::Backends + class Polling + def initialize(options={}) + @handlers = [] + @latency = options[:latency] || 1 + end + + def add_path(path, preload=true) + @handlers << FSSM::State.new(path, preload) + end + + def run + begin + loop do + start = Time.now.to_f + @handlers.each {|handler| handler.refresh} + nap_time = @latency - (Time.now.to_f - start) + sleep nap_time if nap_time > 0 + end + rescue Interrupt + end + end +end +end diff --git a/lib/vendor/fssm/ext.rb b/lib/vendor/fssm/ext.rb new file mode 100644 index 0000000000..5014617482 --- /dev/null +++ b/lib/vendor/fssm/ext.rb @@ -0,0 +1,7 @@ +class Pathname + class << self + def for(path) + path.is_a?(Pathname) ? path : new(path) + end + end +end diff --git a/lib/vendor/fssm/monitor.rb b/lib/vendor/fssm/monitor.rb new file mode 100644 index 0000000000..bda386d032 --- /dev/null +++ b/lib/vendor/fssm/monitor.rb @@ -0,0 +1,21 @@ +class FSSM::Monitor + def initialize(options={}) + @options = options + @backend = FSSM::Backends::Default.new + end + + def path(*args, &block) + path = FSSM::Path.new(*args) + if block && block.arity == 0 + path.instance_eval(&block) + elsif block && block.arity == 1 + block.call(path) + end + @backend.add_path(path) + path + end + + def run + @backend.run + end +end diff --git a/lib/vendor/fssm/path.rb b/lib/vendor/fssm/path.rb new file mode 100644 index 0000000000..8b78133877 --- /dev/null +++ b/lib/vendor/fssm/path.rb @@ -0,0 +1,88 @@ +class FSSM::Path + def initialize(path=nil, glob=nil, &block) + set_path(path || '.') + set_glob(glob || '**/*') + init_callbacks + if block && block.arity == 0 + self.instance_eval(&block) + elsif block && block.arity == 1 + block.call(self) + end + end + + def to_s + @path.to_s + end + + def to_pathname + @path + end + + def glob(value=nil) + return @glob if value.nil? + set_glob(value) + end + + def create(callback_or_path=nil, &block) + callback_action(:create, (block_given? ? block : callback_or_path)) + end + + def update(callback_or_path=nil, &block) + callback_action(:update, (block_given? ? block : callback_or_path)) + end + + def delete(callback_or_path=nil, &block) + callback_action(:delete, (block_given? ? block : callback_or_path)) + end + + private + + def init_callbacks + do_nothing = lambda {|base, relative|} + @callbacks = Hash.new(do_nothing) + end + + def callback_action(type, arg=nil) + if arg.is_a?(Proc) + set_callback(type, arg) + elsif arg.nil? + get_callback(type) + else + run_callback(type, arg) + end + end + + def set_callback(type, arg) + raise ArgumentError, "Proc expected" unless arg.is_a?(Proc) + @callbacks[type] = arg + end + + def get_callback(type) + @callbacks[type] + end + + def run_callback(type, arg) + base, relative = split_path(arg) + + begin + @callbacks[type].call(base, relative) + rescue Exception => e + raise FSSM::CallbackError, "#{type} - #{base.join(relative)}: #{e.message}", e.backtrace + end + end + + def split_path(path) + path = Pathname.for(path) + [@path, (path.relative? ? path : path.relative_path_from(@path))] + end + + def set_path(path) + path = Pathname.for(path) + raise FSSM::FileNotFoundError, "#{path}" unless path.exist? + @path = path.realpath + end + + def set_glob(glob) + @glob = glob.is_a?(Array) ? glob : [glob] + end +end diff --git a/lib/vendor/fssm/state.rb b/lib/vendor/fssm/state.rb new file mode 100644 index 0000000000..fe514c3f1b --- /dev/null +++ b/lib/vendor/fssm/state.rb @@ -0,0 +1,46 @@ +class FSSM::State + def initialize(path, preload=true) + @path = path + @snapshot = {} + snapshot if preload + end + + def refresh + previous = @snapshot + current = snapshot + + deleted(previous, current) + created(previous, current) + modified(previous, current) + end + + private + + def created(previous, current) + (current.keys - previous.keys).each {|created| @path.create(created)} + end + + def deleted(previous, current) + (previous.keys - current.keys).each {|deleted| @path.delete(deleted)} + end + + def modified(previous, current) + (current.keys & previous.keys).each do |file| + @path.update(file) if (current[file] <=> previous[file]) != 0 + end + end + + def snapshot + snap = {} + @path.glob.each {|glob| add_glob(snap, glob)} + @snapshot = snap + end + + def add_glob(snap, glob) + Pathname.glob(@path.to_pathname.join(glob)).each do |fn| + next unless fn.file? + snap["#{fn}"] = fn.mtime + end + end + +end diff --git a/lib/vendor/fssm/support.rb b/lib/vendor/fssm/support.rb new file mode 100644 index 0000000000..fe6aa8969a --- /dev/null +++ b/lib/vendor/fssm/support.rb @@ -0,0 +1,26 @@ +module FSSM::Support + class << self + # def backend + # (mac? && carbon_core?) ? 'FSEvents' : 'Polling' + # end + + def backend + 'Polling' + end + + def mac? + @@mac ||= RUBY_PLATFORM =~ /darwin/i + end + + def carbon_core? + @@carbon_core ||= begin + require 'osx/foundation' + OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework' + true + rescue LoadError + false + end + end + + end +end