middleman/middleman-core/lib/middleman-core/core_extensions/file_watcher.rb

168 lines
4.5 KiB
Ruby
Raw Normal View History

require "pathname"
require "set"
# API for watching file change events
module Middleman
module CoreExtensions
module FileWatcher
IGNORE_LIST = [
/^\.bundle\//,
/^\.sass-cache\//,
/^\.git\//,
/^\.gitignore$/,
/^\.DS_Store$/,
/^build\//,
/^\.rbenv-.*$/,
/^Gemfile$/,
/^Gemfile\.lock$/,
2012-07-09 04:01:06 +02:00
/~$/,
/(^|\/)\.?#/
]
# Setup extension
class << self
# Once registered
def registered(app)
app.send :include, InstanceMethods
# Before parsing config, load the data/ directory
app.before_configuration do
files.reload_path(data_dir)
end
# After config, load everything else
app.ready do
files.reload_path('.')
end
end
alias :included :registered
end
# Instance methods
module InstanceMethods
# Access the file api
# @return [Middleman::CoreExtensions::FileWatcher::API]
def files
@_files_api ||= API.new(self)
end
end
# Core File Change API class
class API
attr_reader :app
delegate :logger, :to => :app
# Initialize api and internal path cache
def initialize(app)
@app = app
@known_paths = Set.new
@_changed = []
@_deleted = []
end
# Add callback to be run on file change
#
# @param [nil,Regexp] matcher A Regexp to match the change path against
# @return [Array<Proc>]
def changed(matcher=nil, &block)
@_changed << [block, matcher] if block_given?
@_changed
end
# Add callback to be run on file deletion
#
# @param [nil,Regexp] matcher A Regexp to match the deleted path against
# @return [Array<Proc>]
def deleted(matcher=nil, &block)
@_deleted << [block, matcher] if block_given?
@_deleted
end
# Notify callbacks that a file changed
#
# @param [Pathname] path The file that changed
# @return [void]
def did_change(path)
return if ignored?(path)
logger.debug "== File Change: #{path}"
@known_paths << path
self.run_callbacks(path, :changed)
end
# Notify callbacks that a file was deleted
#
# @param [Pathname] path The file that was deleted
# @return [void]
def did_delete(path)
return if ignored?(path)
logger.debug "== File Deletion: #{path}"
@known_paths.delete(path)
self.run_callbacks(path, :deleted)
end
# Manually trigger update events
#
# @param [Pathname] path The path to reload
# @param [Boolean] only_new Whether we only look for new files
# @return [void]
def reload_path(path, only_new=false)
# chdir into the root directory so Pathname can work with relative paths
Dir.chdir @app.root_path do
path = Pathname(path)
return unless path.exist?
glob = (path + "**/*").to_s
subset = @known_paths.select { |p| p.fnmatch(glob) }
::Middleman::Util.all_files_under(path).each do |filepath|
if only_new
next if subset.include?(filepath)
else
subset.delete(filepath)
end
self.did_change(filepath)
end
subset.each(&method(:did_delete)) unless only_new
end
end
# Like reload_path, but only triggers events on new files
#
# @param [Pathname] path The path to reload
# @return [void]
def find_new_files(path)
reload_path(path, true)
end
protected
# Whether this path is ignored
# @param [Pathname] path
# @return [Boolean]
def ignored?(path)
IGNORE_LIST.any? { |r| path.to_s.match(r) }
end
# Notify callbacks for a file given an array of callbacks
#
# @param [Pathname] path The file that was changed
# @param [Symbol] callbacks_name The name of the callbacks method
# @return [void]
def run_callbacks(path, callbacks_name)
path = path.to_s
self.send(callbacks_name).each do |callback, matcher|
next unless matcher.nil? || path.match(matcher)
@app.instance_exec(path, &callback)
end
end
end
end
end
2012-07-09 04:01:06 +02:00
end