File: queue_optimizer.rb

package info (click to toggle)
ruby-listen 3.9.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 544 kB
  • sloc: ruby: 5,033; makefile: 9
file content (129 lines) | stat: -rw-r--r-- 3,727 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# frozen_string_literal: true

module Listen
  class QueueOptimizer
    class Config
      def initialize(adapter_class, silencer)
        @adapter_class = adapter_class
        @silencer = silencer
      end

      def exist?(path)
        Pathname(path).exist?
      end

      def silenced?(path, type)
        @silencer.silenced?(path, type)
      end

      def debug(*args, &block)
        Listen.logger.debug(*args, &block)
      end
    end

    def smoosh_changes(changes)
      # TODO: adapter could be nil at this point (shutdown)
      cookies = changes.group_by do |_, _, _, _, options|
        (options || {})[:cookie]
      end
      _squash_changes(_reinterpret_related_changes(cookies))
    end

    def initialize(config)
      @config = config
    end

    private

    attr_reader :config

    # groups changes into the expected structure expected by
    # clients
    def _squash_changes(changes)
      # We combine here for backward compatibility
      # Newer clients should receive dir and path separately
      changes = changes.map { |change, dir, path| [change, dir + path] }

      actions = changes.group_by(&:last).map do |path, action_list|
        [_logical_action_for(path, action_list.map(&:first)), path.to_s]
      end

      config.debug("listen: raw changes: #{actions.inspect}")

      { modified: [], added: [], removed: [] }.tap do |squashed|
        actions.each do |type, path|
          squashed[type] << path unless type.nil?
        end
        config.debug("listen: final changes: #{squashed.inspect}")
      end
    end

    def _logical_action_for(path, actions)
      actions << :added if actions.delete(:moved_to)
      actions << :removed if actions.delete(:moved_from)

      modified = actions.find { |x| x == :modified }
      _calculate_add_remove_difference(actions, path, modified)
    end

    def _calculate_add_remove_difference(actions, path, default_if_exists)
      added = actions.count { |x| x == :added }
      removed = actions.count { |x| x == :removed }
      diff = added - removed

      # TODO: avoid checking if path exists and instead assume the events are
      # in order (if last is :removed, it doesn't exist, etc.)
      if config.exist?(path)
        if diff > 0
          :added
        elsif diff.zero? && added > 0
          :modified
        else
          default_if_exists
        end
      else
        diff < 0 ? :removed : nil
      end
    end

    # remove extraneous rb-inotify events, keeping them only if it's a possible
    # editor rename() call (e.g. Kate and Sublime)
    def _reinterpret_related_changes(cookies)
      table = { moved_to: :added, moved_from: :removed }
      cookies.flat_map do |_, changes|
        if (editor_modified = editor_modified?(changes))
          [[:modified, *editor_modified]]
        else
          not_silenced = changes.reject do |type, _, _, path, _|
            config.silenced?(Pathname(path), type)
          end
          not_silenced.map do |_, change, dir, path, _|
            [table.fetch(change, change), dir, path]
          end
        end
      end
    end

    def editor_modified?(changes)
      return unless changes.size == 2

      from_type = from = nil
      to_type = to_dir = to = nil

      changes.each do |data|
        case data[1]
        when :moved_from
          from_type, _from_change, _, from, = data
        when :moved_to
          to_type, _to_change, to_dir, to, = data
        end
      end

      # Expect an ignored moved_from and non-ignored moved_to
      # to qualify as an "editor modify"
      if from && to && config.silenced?(Pathname(from), from_type) && !config.silenced?(Pathname(to), to_type)
        [to_dir, to]
      end
    end
  end
end