canopus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/docs/adr/001-runtime-gem-components.md +24 -0
- data/docs/adr/002-bounded-diff-engine.md +22 -0
- data/docs/adr/003-background-language-analysis.md +22 -0
- data/docs/adr/004-persistent-display-map.md +23 -0
- data/docs/adr/005-windows-runtime.md +27 -0
- data/docs/adr/README.md +36 -0
- data/docs/distribution.md +27 -0
- data/docs/lsp.md +38 -0
- data/docs/performance.md +40 -0
- data/docs/snippets.md +32 -0
- data/docs/vim.md +24 -0
- data/docs/workspace_edits.md +32 -0
- data/examples/native_smoke.rb +25 -0
- data/examples/plugins/word_count.rb +11 -0
- data/exe/canopus +19 -0
- data/lib/canopus/block_map.rb +19 -0
- data/lib/canopus/buffer.rb +293 -0
- data/lib/canopus/cli.rb +171 -0
- data/lib/canopus/controller.rb +507 -0
- data/lib/canopus/data_compat.rb +25 -0
- data/lib/canopus/display_map/line_builder.rb +54 -0
- data/lib/canopus/display_map/line_set.rb +8 -0
- data/lib/canopus/display_map/pending_line_set.rb +9 -0
- data/lib/canopus/display_map/summary.rb +23 -0
- data/lib/canopus/display_map/worker.rb +89 -0
- data/lib/canopus/display_map.rb +350 -0
- data/lib/canopus/display_point.rb +5 -0
- data/lib/canopus/editor/snippet_expandable.rb +281 -0
- data/lib/canopus/editor.rb +445 -0
- data/lib/canopus/error.rb +5 -0
- data/lib/canopus/fold_map.rb +87 -0
- data/lib/canopus/git/blame.rb +50 -0
- data/lib/canopus/git/commit.rb +7 -0
- data/lib/canopus/git/corrupt_object.rb +7 -0
- data/lib/canopus/git/diff.rb +131 -0
- data/lib/canopus/git/index.rb +94 -0
- data/lib/canopus/git/object_database.rb +45 -0
- data/lib/canopus/git/pack.rb +186 -0
- data/lib/canopus/git/repository.rb +313 -0
- data/lib/canopus/git/status.rb +74 -0
- data/lib/canopus/git/tree_entry.rb +7 -0
- data/lib/canopus/git.rb +15 -0
- data/lib/canopus/icon_theme.rb +43 -0
- data/lib/canopus/language/background_analysis/job.rb +12 -0
- data/lib/canopus/language/background_analysis/scheduler.rb +70 -0
- data/lib/canopus/language/background_analysis.rb +280 -0
- data/lib/canopus/language/definition.rb +7 -0
- data/lib/canopus/language/document.rb +143 -0
- data/lib/canopus/language/symbol.rb +7 -0
- data/lib/canopus/language/syntax_worker.rb +91 -0
- data/lib/canopus/language.rb +42 -0
- data/lib/canopus/lazy_rope.rb +218 -0
- data/lib/canopus/lsp/client.rb +299 -0
- data/lib/canopus/lsp/error.rb +7 -0
- data/lib/canopus/lsp/future/subscription.rb +9 -0
- data/lib/canopus/lsp/future.rb +87 -0
- data/lib/canopus/lsp/protocol.rb +83 -0
- data/lib/canopus/lsp/server_error.rb +13 -0
- data/lib/canopus/lsp/timeout.rb +7 -0
- data/lib/canopus/lsp/transport.rb +123 -0
- data/lib/canopus/lsp.rb +19 -0
- data/lib/canopus/markdown.rb +69 -0
- data/lib/canopus/match_data_compat.rb +17 -0
- data/lib/canopus/multi_buffer.rb +266 -0
- data/lib/canopus/pane.rb +61 -0
- data/lib/canopus/patch/composite.rb +12 -0
- data/lib/canopus/patch/reload.rb +18 -0
- data/lib/canopus/patch.rb +27 -0
- data/lib/canopus/performance_recorder.rb +207 -0
- data/lib/canopus/plugins/api.rb +50 -0
- data/lib/canopus/plugins/isolated_runtime.rb +167 -0
- data/lib/canopus/plugins/local_runtime.rb +25 -0
- data/lib/canopus/plugins/permission_denied.rb +7 -0
- data/lib/canopus/plugins/registry.rb +30 -0
- data/lib/canopus/plugins.rb +11 -0
- data/lib/canopus/project/ignore_matcher.rb +104 -0
- data/lib/canopus/project/search.rb +164 -0
- data/lib/canopus/project/search_worker/cancelled.rb +3 -0
- data/lib/canopus/project/search_worker/runner.rb +9 -0
- data/lib/canopus/project/search_worker.rb +108 -0
- data/lib/canopus/project/tree.rb +48 -0
- data/lib/canopus/project/watcher.rb +59 -0
- data/lib/canopus/project.rb +127 -0
- data/lib/canopus/regexp_compat.rb +30 -0
- data/lib/canopus/save_conflict.rb +5 -0
- data/lib/canopus/selection.rb +11 -0
- data/lib/canopus/settings.rb +118 -0
- data/lib/canopus/snippet/transform.rb +209 -0
- data/lib/canopus/snippet.rb +183 -0
- data/lib/canopus/tab_map.rb +30 -0
- data/lib/canopus/terminal/cell.rb +7 -0
- data/lib/canopus/terminal/grid.rb +321 -0
- data/lib/canopus/terminal/pty.rb +103 -0
- data/lib/canopus/terminal/scrollback.rb +38 -0
- data/lib/canopus/terminal/vt.rb +398 -0
- data/lib/canopus/terminal.rb +13 -0
- data/lib/canopus/theme.rb +43 -0
- data/lib/canopus/version.rb +5 -0
- data/lib/canopus/vim/commandable.rb +148 -0
- data/lib/canopus/vim/motionable.rb +321 -0
- data/lib/canopus/vim/operator_capable.rb +377 -0
- data/lib/canopus/vim/text_object_selectable.rb +141 -0
- data/lib/canopus/vim.rb +426 -0
- data/lib/canopus/workspace/edit/executable.rb +139 -0
- data/lib/canopus/workspace/edit/node.rb +8 -0
- data/lib/canopus/workspace/edit/plan.rb +170 -0
- data/lib/canopus/workspace/edit/resource_preparable.rb +98 -0
- data/lib/canopus/workspace/edit.rb +6 -0
- data/lib/canopus/workspace/file_change_aware.rb +40 -0
- data/lib/canopus/workspace/file_previewable.rb +31 -0
- data/lib/canopus/workspace/git_aware.rb +140 -0
- data/lib/canopus/workspace/language_aware.rb +413 -0
- data/lib/canopus/workspace/language_server_configurable.rb +181 -0
- data/lib/canopus/workspace/project_searchable.rb +208 -0
- data/lib/canopus/workspace/project_tree_editable.rb +82 -0
- data/lib/canopus/workspace/session_persistable.rb +153 -0
- data/lib/canopus/workspace/settings_aware.rb +91 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +185 -0
- data/lib/canopus/workspace/view.rb +643 -0
- data/lib/canopus/workspace.rb +525 -0
- data/lib/canopus/wrap_map.rb +108 -0
- data/lib/canopus.rb +24 -0
- data/sig/canopus.rbs +375 -0
- data/sig/controller.rbs +55 -0
- data/sig/display_stages.rbs +49 -0
- data/sig/git.rbs +140 -0
- data/sig/lsp.rbs +99 -0
- data/sig/markdown.rbs +11 -0
- data/sig/performance_recorder.rbs +23 -0
- data/sig/search_services.rbs +8 -0
- data/sig/services.rbs +81 -0
- data/sig/snippet.rbs +42 -0
- data/sig/terminal.rbs +125 -0
- data/sig/vim.rbs +27 -0
- data/sig/workspace_edits.rbs +11 -0
- data/sig/workspace_services.rbs +117 -0
- data/tools/certify_snippet_regex.rb +35 -0
- data/tools/check_dependencies.rb +80 -0
- data/tools/native_check.rb +70 -0
- data/tools/package.rb +113 -0
- data/tools/package_test.rb +32 -0
- metadata +314 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "workspace/view"
|
|
4
|
+
|
|
5
|
+
module Canopus
|
|
6
|
+
class Controller
|
|
7
|
+
SHORTCUTS = {"cmd-s" => "file.save", "ctrl-s" => "file.save", "cmd-n" => "file.new", "ctrl-n" => "file.new",
|
|
8
|
+
"cmd-w" => "file.close", "ctrl-w" => "file.close", "cmd-p" => "file.find", "ctrl-p" => "file.find",
|
|
9
|
+
"cmd-shift-p" => "command.palette", "ctrl-shift-p" => "command.palette", "cmd-z" => "edit.undo", "ctrl-z" => "edit.undo",
|
|
10
|
+
"cmd-shift-z" => "edit.redo", "ctrl-shift-z" => "edit.redo", "cmd-a" => "edit.select_all", "ctrl-a" => "edit.select_all",
|
|
11
|
+
"cmd-d" => "edit.select_next", "ctrl-d" => "edit.select_next", "cmd-f" => "search.buffer", "ctrl-f" => "search.buffer",
|
|
12
|
+
"cmd-shift-l" => "edit.select_all_occurrences", "ctrl-shift-l" => "edit.select_all_occurrences",
|
|
13
|
+
"alt-up" => "edit.move_line_up", "alt-down" => "edit.move_line_down",
|
|
14
|
+
"cmd-shift-f" => "search.project", "ctrl-shift-f" => "search.project", "cmd-alt-f" => "search.replace", "ctrl-h" => "search.replace",
|
|
15
|
+
"cmd-/" => "edit.toggle_comment", "ctrl-/" => "edit.toggle_comment", "cmd-b" => "view.project", "ctrl-b" => "view.project",
|
|
16
|
+
"ctrl-`" => "view.terminal", "cmd-\\" => "pane.split_right", "ctrl-\\" => "pane.split_right",
|
|
17
|
+
"ctrl-space" => "language.completion", "f12" => "language.definition", "shift-f12" => "language.references",
|
|
18
|
+
"f2" => "language.rename", "alt-enter" => "language.codeAction", "cmd-shift-o" => "language.outline",
|
|
19
|
+
"ctrl-shift-o" => "language.outline", "cmd-k" => "language.hover", "ctrl-k" => "language.hover"}.freeze
|
|
20
|
+
attr_reader :workspace, :view, :window, :keymap
|
|
21
|
+
def initialize(workspace, window)
|
|
22
|
+
@workspace, @window, @view = workspace, window, Workspace::View.new(workspace)
|
|
23
|
+
workspace.window = window
|
|
24
|
+
reload_keymap
|
|
25
|
+
window.draw { workspace.drain; @view }
|
|
26
|
+
window.on_input { |event| input(event) }
|
|
27
|
+
window.on_close { close_requested }
|
|
28
|
+
window.on_appearance { |_| workspace.apply_settings if workspace.settings["theme"] == "auto" } if window.respond_to?(:on_appearance)
|
|
29
|
+
window.on_tick do
|
|
30
|
+
data = workspace.terminal&.read
|
|
31
|
+
window.request_frame if data && !data.empty?
|
|
32
|
+
workspace.drain
|
|
33
|
+
workspace.poll_changes
|
|
34
|
+
workspace.poll_settings
|
|
35
|
+
reload_keymap
|
|
36
|
+
workspace.poll_git_changes
|
|
37
|
+
poll_display_maps
|
|
38
|
+
poll_language_documents
|
|
39
|
+
poll_scroll
|
|
40
|
+
window.request_frame if @view.tick
|
|
41
|
+
end
|
|
42
|
+
@clipboard, @terminal_focus = "", false
|
|
43
|
+
workspace.new_buffer unless workspace.editor
|
|
44
|
+
end
|
|
45
|
+
def input(event)
|
|
46
|
+
case event
|
|
47
|
+
when Zaniah::Input::KeyUp, Zaniah::Input::MouseDown, Zaniah::Input::FileDrop
|
|
48
|
+
@suppress_key_text = false
|
|
49
|
+
when Zaniah::Input::Composition
|
|
50
|
+
unless @workspace.palette
|
|
51
|
+
@composing = !event.text.empty?
|
|
52
|
+
@suppress_key_text = false if @composing
|
|
53
|
+
end
|
|
54
|
+
when Zaniah::Input::TextInput
|
|
55
|
+
@composing = false
|
|
56
|
+
suppressed, @suppress_key_text = @suppress_key_text, false
|
|
57
|
+
return if suppressed
|
|
58
|
+
end
|
|
59
|
+
cancel_scroll if event.is_a?(Zaniah::Input::KeyDown) || event.is_a?(Zaniah::Input::TextInput) || event.is_a?(Zaniah::Input::MouseDown)
|
|
60
|
+
@view.reset_blink unless event.is_a?(Zaniah::Input::MouseMove)
|
|
61
|
+
return if palette_input(event)
|
|
62
|
+
case event
|
|
63
|
+
when Zaniah::Input::FileDrop
|
|
64
|
+
event.paths.each { |path| @workspace.open(path) if File.file?(path) }
|
|
65
|
+
when Zaniah::Input::TextInput
|
|
66
|
+
input_text(event.text)
|
|
67
|
+
when Zaniah::Input::Composition
|
|
68
|
+
@workspace.editor.composition = event
|
|
69
|
+
when Zaniah::Input::KeyDown
|
|
70
|
+
key(event.keystroke)
|
|
71
|
+
when Zaniah::Input::MouseDown
|
|
72
|
+
mouse_down(event)
|
|
73
|
+
when Zaniah::Input::MouseMove
|
|
74
|
+
mouse_move(event) if @drag
|
|
75
|
+
resize_drag(event.position) if @resize_drag
|
|
76
|
+
scroll_drag(event.position) if @scroll_drag
|
|
77
|
+
if @terminal_drag
|
|
78
|
+
@terminal_drag == :selection ? @view.terminal_select(event.position, extend: true) : terminal_mouse(event, :move)
|
|
79
|
+
elsif !(@drag || @resize_drag || @scroll_drag || @drag_file || @drag_tab) &&
|
|
80
|
+
@workspace.terminal_visible && @workspace.terminal&.vt&.modes&.[](1003) && @view.hit(event.position)&.first == :terminal
|
|
81
|
+
terminal_mouse(event, :move)
|
|
82
|
+
end
|
|
83
|
+
when Zaniah::Input::MouseUp
|
|
84
|
+
@resize_drag = nil
|
|
85
|
+
@scroll_drag = nil
|
|
86
|
+
if @drag_file
|
|
87
|
+
path, origin = @drag_file
|
|
88
|
+
target = @view.hit(event.position)
|
|
89
|
+
if target&.first == :directory && (event.position.x - origin.x).abs + (event.position.y - origin.y).abs > 10
|
|
90
|
+
@workspace.rename_project_entry(path, File.join(target[1], File.basename(path)))
|
|
91
|
+
end
|
|
92
|
+
@drag_file = nil
|
|
93
|
+
end
|
|
94
|
+
terminal_mouse(event, :release) if @terminal_drag && @terminal_drag != :selection
|
|
95
|
+
@terminal_drag = nil
|
|
96
|
+
if @drag_tab
|
|
97
|
+
from, editor, origin = @drag_tab
|
|
98
|
+
if (event.position.x - origin.x).abs + (event.position.y - origin.y).abs > 10
|
|
99
|
+
target = @view.hit(event.position)
|
|
100
|
+
if target && [:editor, :tab, :pane].include?(target.first)
|
|
101
|
+
@workspace.move_tab(from: from, to: target[1], editor: editor, index: target.first == :tab ? target[1].editors.index(target[2]) : nil)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
@drag_tab = nil
|
|
105
|
+
end
|
|
106
|
+
@drag = nil
|
|
107
|
+
when Zaniah::Input::ScrollWheel
|
|
108
|
+
scroll(event)
|
|
109
|
+
end
|
|
110
|
+
@window.request_frame
|
|
111
|
+
rescue StandardError => error
|
|
112
|
+
@workspace.message = error.message
|
|
113
|
+
@window.request_frame
|
|
114
|
+
end
|
|
115
|
+
def input_text(text)
|
|
116
|
+
if @workspace.palette
|
|
117
|
+
return if [:workspace_edit, :confirm_close, :trash_file].include?(@workspace.palette[:kind])
|
|
118
|
+
@workspace.palette[:query] << text
|
|
119
|
+
@workspace.update_palette
|
|
120
|
+
elsif @terminal_focus && @workspace.terminal_visible
|
|
121
|
+
@workspace.terminal.write(text)
|
|
122
|
+
elsif @workspace.settings["vim_mode"]
|
|
123
|
+
text.each_grapheme_cluster { |character| @workspace.vim.feed(character) }
|
|
124
|
+
else
|
|
125
|
+
@workspace.editor.composition = nil
|
|
126
|
+
@workspace.editor.insert_text(text)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
def key(stroke)
|
|
130
|
+
@suppress_key_text = false
|
|
131
|
+
reload_keymap
|
|
132
|
+
stroke = Zaniah::Input::Keystroke.normalize(stroke)
|
|
133
|
+
return palette_key(stroke) if @workspace.palette
|
|
134
|
+
return if @composing && (stroke.split("-") & %w[ctrl cmd]).empty?
|
|
135
|
+
return @workspace.settings_completions if stroke == "ctrl-space" && @workspace.settings_document?(@workspace.editor.buffer)
|
|
136
|
+
if ["cmd-q", "ctrl-q"].include?(stroke)
|
|
137
|
+
return @window.close
|
|
138
|
+
end
|
|
139
|
+
if @terminal_focus && @workspace.terminal_visible && stroke != "ctrl-`"
|
|
140
|
+
if ["cmd-v", "ctrl-shift-v"].include?(stroke)
|
|
141
|
+
return @workspace.terminal.paste(@window.respond_to?(:clipboard) ? @window.clipboard.to_s : @clipboard)
|
|
142
|
+
elsif ["cmd-c", "ctrl-shift-c"].include?(stroke)
|
|
143
|
+
@clipboard = @view.terminal_selected_text
|
|
144
|
+
@window.clipboard = @clipboard if @window.respond_to?(:clipboard=)
|
|
145
|
+
return
|
|
146
|
+
end
|
|
147
|
+
parts = stroke.split("-")
|
|
148
|
+
name = parts.pop
|
|
149
|
+
return if (name.length == 1 || name == "space") && (parts & %w[ctrl alt cmd]).empty?
|
|
150
|
+
name = {"esc" => "escape", "pageup" => "page_up", "pagedown" => "page_down", "space" => " "}.fetch(name, name)
|
|
151
|
+
return @workspace.terminal.key(name, control: parts.include?("ctrl"), alt: parts.include?("alt"), shift: parts.include?("shift"))
|
|
152
|
+
end
|
|
153
|
+
mode = @workspace.settings["vim_mode"] ? @workspace.vim.mode.to_s : false
|
|
154
|
+
action = @keymap.dispatch(stroke, context: {"Editor" => true, "vim_mode" => mode})
|
|
155
|
+
if action
|
|
156
|
+
# Native adapters deliver text separately after printable key down.
|
|
157
|
+
# Cocoa also sends an empty Composition before that ordinary commit.
|
|
158
|
+
@suppress_key_text = stroke.match?(/\A(?:(?:alt|shift)-)*(?:space|[^\x00-\x1f\x7f])\z/)
|
|
159
|
+
return action == :pending ? nil : @workspace.call(action)
|
|
160
|
+
end
|
|
161
|
+
if ["cmd-c", "ctrl-c", "cmd-x", "ctrl-x", "cmd-v", "ctrl-v"].include?(stroke) && !(@workspace.settings["vim_mode"] && stroke == "ctrl-v")
|
|
162
|
+
return clipboard(stroke.split("-").last)
|
|
163
|
+
end
|
|
164
|
+
if ["tab", "shift-tab"].include?(stroke) && @workspace.editor.snippet_active?
|
|
165
|
+
stroke == "tab" ? @workspace.editor.next_snippet : @workspace.editor.previous_snippet
|
|
166
|
+
return @workspace.show_snippet_choices
|
|
167
|
+
end
|
|
168
|
+
parts = stroke.split("-")
|
|
169
|
+
name = parts.pop
|
|
170
|
+
if @workspace.settings["vim_mode"]
|
|
171
|
+
@workspace.vim.feed(stroke) if name.length > 1 || parts.include?("ctrl")
|
|
172
|
+
return
|
|
173
|
+
end
|
|
174
|
+
editor = @workspace.editor
|
|
175
|
+
shift = parts.include?("shift")
|
|
176
|
+
movement = {"left" => :left, "right" => :right, "up" => :up, "down" => :down,
|
|
177
|
+
"home" => :line_start, "end" => :line_end, "pageup" => :page_up, "pagedown" => :page_down}[name]
|
|
178
|
+
if movement
|
|
179
|
+
movement = {left: :word_left, right: :word_right}.fetch(movement, movement) if parts.include?("alt") || parts.include?("ctrl")
|
|
180
|
+
movement = {left: :line_start, right: :line_end, up: :file_start, down: :file_end}.fetch(movement, movement) if parts.include?("cmd")
|
|
181
|
+
editor.move(movement, extend: shift)
|
|
182
|
+
else
|
|
183
|
+
case name
|
|
184
|
+
when "backspace" then editor.delete_backward
|
|
185
|
+
when "delete" then editor.delete_forward
|
|
186
|
+
when "enter" then editor.insert_text("\n")
|
|
187
|
+
when "tab" then shift ? editor.indent(outdent: true) : (editor.next_snippet || editor.insert_text(editor.use_tabs ? "\t" : " " * editor.tab_size))
|
|
188
|
+
when "esc" then @workspace.dismiss_hover; editor.clear_snippet; editor.composition = nil; editor.select(editor.primary.head)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
def tick
|
|
193
|
+
reload_keymap
|
|
194
|
+
@window.tick
|
|
195
|
+
end
|
|
196
|
+
def cancel_scroll = @scroll_motion = nil
|
|
197
|
+
def poll_language_documents
|
|
198
|
+
visible = @workspace.panes.filter_map(&:active)
|
|
199
|
+
@language_viewports ||= {}
|
|
200
|
+
@language_viewports.delete_if { |editor, _| !visible.include?(editor) }
|
|
201
|
+
inactive = @workspace.panes.flat_map(&:editors).uniq - visible
|
|
202
|
+
@language_poll_index = (@language_poll_index || -1) + 1
|
|
203
|
+
candidates = visible + (inactive.empty? ? [] : [inactive[@language_poll_index % inactive.length]])
|
|
204
|
+
candidates.each do |editor|
|
|
205
|
+
document = editor.language_document
|
|
206
|
+
if visible.include?(editor)
|
|
207
|
+
map = editor.display_map
|
|
208
|
+
first = editor.scroll_y.floor.clamp(0, map.row_count - 1)
|
|
209
|
+
last = [first + editor.viewport_rows, first + 255, map.row_count - 1].min
|
|
210
|
+
cached = @language_viewports[editor]
|
|
211
|
+
# The persistent display tree changes on edits, folds, blocks and
|
|
212
|
+
# completed wrap batches. Idle polls need not walk every visible row.
|
|
213
|
+
unless cached && cached[0].equal?(document) && cached[1].equal?(map.tree) &&
|
|
214
|
+
cached[2] == editor.buffer.version && cached[3] == first && cached[4] == last
|
|
215
|
+
document.request(rows: (first..last).map { |row| map.source_row(row) }.uniq)
|
|
216
|
+
@language_viewports[editor] = [document, map.tree, editor.buffer.version, first, last]
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
changed = document.poll
|
|
220
|
+
@workspace.language_ready(editor, document)
|
|
221
|
+
@window.request_frame if changed
|
|
222
|
+
rescue StandardError => error
|
|
223
|
+
@workspace.message = "Language analysis: #{error.message}"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
def poll_scroll(now: Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
227
|
+
return false unless @scroll_motion
|
|
228
|
+
editor, vx, vy, time = @scroll_motion
|
|
229
|
+
friction = @workspace.settings["scroll_friction"]
|
|
230
|
+
if @workspace.palette || friction.zero? || !@workspace.panes.any? { |pane| pane.active.equal?(editor) }
|
|
231
|
+
cancel_scroll
|
|
232
|
+
return false
|
|
233
|
+
end
|
|
234
|
+
elapsed = now - time
|
|
235
|
+
return false unless elapsed.positive?
|
|
236
|
+
# Exact exponential integration makes motion independent of frame rate.
|
|
237
|
+
decay = Math.exp(-friction * elapsed)
|
|
238
|
+
x, y = editor.scroll_x, editor.scroll_y
|
|
239
|
+
editor.scroll(dx: vx * (1 - decay) / friction, dy: vy * (1 - decay) / friction)
|
|
240
|
+
vx = editor.scroll_x == x ? 0 : vx * decay
|
|
241
|
+
vy = editor.scroll_y == y ? 0 : vy * decay
|
|
242
|
+
@scroll_motion = vx.abs < 0.2 && vy.abs < 0.01 ? nil : [editor, vx, vy, now]
|
|
243
|
+
changed = editor.scroll_x != x || editor.scroll_y != y
|
|
244
|
+
@window.request_frame if changed
|
|
245
|
+
changed
|
|
246
|
+
end
|
|
247
|
+
def poll_display_maps
|
|
248
|
+
visible = @workspace.panes.filter_map(&:active)
|
|
249
|
+
inactive = @workspace.panes.flat_map(&:editors) - visible
|
|
250
|
+
targets = visible.map { |editor| [editor, [224 / [visible.length, 1].max, 1].max] }
|
|
251
|
+
unless inactive.empty?
|
|
252
|
+
@layout_poll_index = ((@layout_poll_index || -1) + 1) % inactive.length
|
|
253
|
+
targets << [inactive[@layout_poll_index], 32]
|
|
254
|
+
end
|
|
255
|
+
targets.each do |editor, limit|
|
|
256
|
+
map = editor.display_map
|
|
257
|
+
next unless map.pending?
|
|
258
|
+
top = editor.scroll_y
|
|
259
|
+
first = top.floor.clamp(0, map.row_count - 1)
|
|
260
|
+
anchor = map.to_buffer(DisplayPoint.new(first, 0))
|
|
261
|
+
cursor_was_visible = map.to_display(editor.primary.head).row.between?(first, first + editor.viewport_rows - 1)
|
|
262
|
+
next unless map.poll(max_lines: limit)
|
|
263
|
+
editor.scroll(dy: map.to_display(anchor).row + top % 1 - editor.scroll_y)
|
|
264
|
+
editor.reveal_cursor if @workspace.editor.equal?(editor) && cursor_was_visible
|
|
265
|
+
@window.request_frame
|
|
266
|
+
rescue StandardError => error
|
|
267
|
+
@workspace.message = "Background layout failed: #{error.message}"
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
def close_requested
|
|
271
|
+
if @discard_close || @workspace.buffers.values.none?(&:dirty?)
|
|
272
|
+
@workspace.palette = nil
|
|
273
|
+
return true
|
|
274
|
+
end
|
|
275
|
+
@workspace.palette = {kind: :confirm_close, query: "Save changes before closing?", index: 0, matches: ["Save all", "Discard changes", "Cancel"]}
|
|
276
|
+
@window.request_frame
|
|
277
|
+
false
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
private
|
|
281
|
+
def reload_keymap
|
|
282
|
+
language = @workspace.editor&.language_document&.definition&.name
|
|
283
|
+
groups = @workspace.settings["languages"].dig(language, "keymap") || @workspace.settings["keymap"]
|
|
284
|
+
return if @keymap_groups.equal?(groups)
|
|
285
|
+
unless @keymap_groups == groups
|
|
286
|
+
replacement = Zaniah::Input::Keymap.new
|
|
287
|
+
SHORTCUTS.each { |keys, action| replacement.bind(keys, action) }
|
|
288
|
+
groups.each do |group|
|
|
289
|
+
group.fetch("bindings").each { |keys, action| replacement.bind(keys, action, context: group.fetch("context")) }
|
|
290
|
+
end
|
|
291
|
+
@keymap = replacement
|
|
292
|
+
@view.keymap = replacement
|
|
293
|
+
end
|
|
294
|
+
@keymap_groups = groups
|
|
295
|
+
end
|
|
296
|
+
def palette_input(event)
|
|
297
|
+
return false unless @workspace.palette
|
|
298
|
+
@drag = @resize_drag = @scroll_drag = @terminal_drag = @drag_file = @drag_tab = nil
|
|
299
|
+
case event
|
|
300
|
+
when Zaniah::Input::KeyDown then key(event.keystroke)
|
|
301
|
+
when Zaniah::Input::TextInput then input_text(event.text)
|
|
302
|
+
when Zaniah::Input::MouseDown then mouse_down(event)
|
|
303
|
+
end
|
|
304
|
+
@window.request_frame
|
|
305
|
+
true
|
|
306
|
+
end
|
|
307
|
+
def clipboard(operation)
|
|
308
|
+
editor = @workspace.editor
|
|
309
|
+
if operation == "v"
|
|
310
|
+
value = @window.respond_to?(:clipboard) ? @window.clipboard : @clipboard
|
|
311
|
+
editor.insert_text(value.to_s, auto_indent: false)
|
|
312
|
+
else
|
|
313
|
+
value = editor.selections.map { |selection| editor.buffer.rope.byteslice(selection.range).to_s }.join("\n")
|
|
314
|
+
@clipboard = value
|
|
315
|
+
@window.clipboard = value if @window.respond_to?(:clipboard=)
|
|
316
|
+
editor.replace_selections("", kind: :cut) if operation == "x"
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
def palette_key(stroke)
|
|
320
|
+
palette = @workspace.palette
|
|
321
|
+
if palette[:search_options] && (option = {"ctrl-alt-r" => :regexp, "ctrl-alt-c" => :case_sensitive, "ctrl-alt-w" => :whole_word, "ctrl-alt-s" => :selection_only}[stroke])
|
|
322
|
+
return if option == :selection_only && palette[:kind] == :project_search
|
|
323
|
+
palette[:search_options][option] = !palette[:search_options][option]
|
|
324
|
+
return
|
|
325
|
+
end
|
|
326
|
+
case stroke
|
|
327
|
+
when "tab", "shift-tab"
|
|
328
|
+
if palette[:kind] == :snippet_choices
|
|
329
|
+
@workspace.palette_accept
|
|
330
|
+
stroke == "tab" ? @workspace.editor.next_snippet : @workspace.editor.previous_snippet
|
|
331
|
+
@workspace.show_snippet_choices
|
|
332
|
+
end
|
|
333
|
+
when "esc"
|
|
334
|
+
@workspace.palette = nil
|
|
335
|
+
when "up" then palette[:index] = [palette[:index] - 1, 0].max
|
|
336
|
+
when "down" then palette[:index] = [palette[:index] + 1, [palette[:matches].length - 1, 0].max].min
|
|
337
|
+
when "pageup", "pagedown"
|
|
338
|
+
if palette[:kind] == :workspace_edit
|
|
339
|
+
step = palette.fetch(:detail_rows, 8)
|
|
340
|
+
maximum = [palette.fetch(:detail_lines, []).length - step, 0].max
|
|
341
|
+
palette[:details_scroll] = (palette.fetch(:details_scroll, 0) + (stroke == "pageup" ? -step : step)).clamp(0, maximum)
|
|
342
|
+
end
|
|
343
|
+
when "backspace"
|
|
344
|
+
return if [:workspace_edit, :confirm_close, :trash_file].include?(palette[:kind])
|
|
345
|
+
palette[:query] = palette[:query].grapheme_clusters[0...-1].join
|
|
346
|
+
@workspace.update_palette
|
|
347
|
+
when "enter"
|
|
348
|
+
if palette[:kind] == :workspace_edit
|
|
349
|
+
result = if palette[:index].zero?
|
|
350
|
+
begin
|
|
351
|
+
@workspace.apply_workspace_edit(palette[:edit], confirmed: true)
|
|
352
|
+
rescue StandardError => error
|
|
353
|
+
{"applied" => false, "failureReason" => error.message}
|
|
354
|
+
end
|
|
355
|
+
else
|
|
356
|
+
{"applied" => false, "failureReason" => "User cancelled"}
|
|
357
|
+
end
|
|
358
|
+
palette[:response]&.fulfill(result.slice("applied", "failureReason", "failedChange"))
|
|
359
|
+
@workspace.palette = nil if @workspace.palette.equal?(palette)
|
|
360
|
+
if result["applied"]
|
|
361
|
+
palette[:on_applied]&.call
|
|
362
|
+
elsif result["failureReason"] != "User cancelled"
|
|
363
|
+
@workspace.message = result["failureReason"].to_s
|
|
364
|
+
end
|
|
365
|
+
elsif palette[:kind] == :confirm_close
|
|
366
|
+
case palette[:index]
|
|
367
|
+
when 0
|
|
368
|
+
dirty = @workspace.buffers.values.flat_map { |buffer| buffer.is_a?(MultiBuffer) ? buffer.excerpts.map(&:buffer) : [buffer] }.uniq.select(&:dirty?)
|
|
369
|
+
raise Error, "Save untitled documents first, then close again" if dirty.any? { |buffer| !buffer.path }
|
|
370
|
+
dirty.each { |buffer| @workspace.save_buffer(buffer) }
|
|
371
|
+
@workspace.palette = nil
|
|
372
|
+
@window.close
|
|
373
|
+
when 1 then @discard_close = true; @window.close
|
|
374
|
+
else @workspace.palette = nil
|
|
375
|
+
end
|
|
376
|
+
else
|
|
377
|
+
@workspace.palette_accept
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
def mouse_down(event)
|
|
382
|
+
action = @view.hit(event.position)
|
|
383
|
+
return unless action
|
|
384
|
+
kind, *args = action
|
|
385
|
+
return if @workspace.palette && (kind != :palette || event.button != :left)
|
|
386
|
+
if [:file, :directory].include?(kind)
|
|
387
|
+
@workspace.selected_project_path = args.first
|
|
388
|
+
@drag_file = [args.first, event.position] if event.button == :left
|
|
389
|
+
if event.button == :right
|
|
390
|
+
@workspace.palette = {kind: :commands, query: +"", index: 0, matches: %w[project.new_file project.new_folder project.rename project.trash]}
|
|
391
|
+
return
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
@terminal_focus = kind == :terminal
|
|
395
|
+
case kind
|
|
396
|
+
when :git_hunk then @workspace.toggle_git_hunk(args[0], row: args[1]) if event.button == :left
|
|
397
|
+
when :hover_link
|
|
398
|
+
return unless event.button == :left
|
|
399
|
+
uri = URI.parse(args.first)
|
|
400
|
+
@window.open_url(uri.to_s) if %w[http https].include?(uri.scheme) && uri.host && @window.respond_to?(:open_url)
|
|
401
|
+
when :dismiss_notification then @workspace.dismiss_notification(args.first)
|
|
402
|
+
when :pane then @workspace.focus(args.first)
|
|
403
|
+
when :split_resize, :dock_resize then @resize_drag = action
|
|
404
|
+
when :scrollbar
|
|
405
|
+
editor, direction, track, thumb, maximum = args
|
|
406
|
+
coordinate = direction == :vertical ? event.position.y : event.position.x
|
|
407
|
+
start = direction == :vertical ? thumb.y : thumb.x
|
|
408
|
+
length = direction == :vertical ? thumb.height : thumb.width
|
|
409
|
+
grab = coordinate.between?(start, start + length) ? coordinate - start : length / 2.0
|
|
410
|
+
@scroll_drag = [editor, direction, track, thumb, maximum, grab]
|
|
411
|
+
scroll_drag(event.position)
|
|
412
|
+
when :terminal
|
|
413
|
+
return if event.button == :left && @view.terminal_open_link(event.position, modifiers: event.modifiers)
|
|
414
|
+
tracking = [1000, 1002, 1003].any? { |mode| @workspace.terminal.vt.modes[mode] }
|
|
415
|
+
if tracking && !event.modifiers.map(&:to_s).include?("shift")
|
|
416
|
+
@terminal_drag = event.button
|
|
417
|
+
terminal_mouse(event, :press)
|
|
418
|
+
else
|
|
419
|
+
@terminal_drag = :selection
|
|
420
|
+
@view.terminal_select(event.position)
|
|
421
|
+
end
|
|
422
|
+
when :file then @workspace.open(args.first)
|
|
423
|
+
when :directory then @workspace.project_tree.toggle(args.first)
|
|
424
|
+
when :tab
|
|
425
|
+
pane, editor = args
|
|
426
|
+
@workspace.activate_tab(pane, editor)
|
|
427
|
+
@drag_tab = [pane, editor, event.position]
|
|
428
|
+
when :editor
|
|
429
|
+
pane, editor = args
|
|
430
|
+
@workspace.focus(pane)
|
|
431
|
+
position = @view.offset_at(editor, event.position)
|
|
432
|
+
modifiers = event.modifiers.map(&:to_s)
|
|
433
|
+
anchor = modifiers.include?("shift") ? editor.primary.anchor : position
|
|
434
|
+
editor.select(anchor, position, add: modifiers.include?("alt"))
|
|
435
|
+
if event.click_count == 2
|
|
436
|
+
editor.move(:word_left)
|
|
437
|
+
editor.move(:word_right, extend: true)
|
|
438
|
+
elsif event.click_count >= 3
|
|
439
|
+
editor.move(:line_start)
|
|
440
|
+
editor.move(:down, extend: true)
|
|
441
|
+
end
|
|
442
|
+
@drag = [editor, editor.primary.anchor, event.position, modifiers.include?("alt") && modifiers.include?("shift")]
|
|
443
|
+
when :palette
|
|
444
|
+
@workspace.palette[:index] = args.first
|
|
445
|
+
palette_key("enter")
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
def mouse_move(event)
|
|
449
|
+
editor, anchor, _origin, rectangle = @drag
|
|
450
|
+
offset = @view.offset_at(editor, event.position)
|
|
451
|
+
rectangle ? editor.rectangle(editor.display_map.to_display(anchor), editor.display_map.to_display(offset)) : editor.select(anchor, offset)
|
|
452
|
+
end
|
|
453
|
+
def scroll(event)
|
|
454
|
+
cancel_scroll
|
|
455
|
+
raise ArgumentError, "scroll deltas must be finite" unless [event.delta.x, event.delta.y].all? { |value| value.is_a?(Numeric) && value.finite? }
|
|
456
|
+
action = @view.hit(event.position)
|
|
457
|
+
if [:editor, :scrollbar].include?(action&.first)
|
|
458
|
+
editor = action.first == :editor ? action.last : action[1]
|
|
459
|
+
editor.scroll(dx: event.delta.x, dy: event.delta.y / 20.0)
|
|
460
|
+
# Cocoa already supplies native momentum deltas (numeric NSEvent phase).
|
|
461
|
+
# Other backends get a bounded decaying tail; zero friction disables it.
|
|
462
|
+
if !event.phase.is_a?(Numeric) && @workspace.settings["scroll_friction"].positive?
|
|
463
|
+
@scroll_motion = [editor, event.delta.x * 10, event.delta.y / 2.0, Process.clock_gettime(Process::CLOCK_MONOTONIC)]
|
|
464
|
+
end
|
|
465
|
+
elsif [:file, :directory].include?(action&.first)
|
|
466
|
+
@view.project_scroll(event.delta.y / 24.0)
|
|
467
|
+
elsif action&.first == :terminal
|
|
468
|
+
if [1000, 1002, 1003].any? { |mode| @workspace.terminal.vt.modes[mode] }
|
|
469
|
+
column, row = @view.terminal_point(event.position)
|
|
470
|
+
@workspace.terminal.mouse(button: event.delta.y.positive? ? :wheel_down : :wheel_up, column: column, row: row)
|
|
471
|
+
else
|
|
472
|
+
@view.terminal_scroll(event.delta.y / 20.0)
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
def scroll_drag(point)
|
|
477
|
+
editor, direction, track, thumb, maximum, grab = @scroll_drag
|
|
478
|
+
if direction == :vertical
|
|
479
|
+
fraction = (point.y - track.y - grab) / [track.height - thumb.height, 1].max
|
|
480
|
+
editor.scroll(dy: fraction.clamp(0, 1) * maximum - editor.scroll_y)
|
|
481
|
+
else
|
|
482
|
+
fraction = (point.x - track.x - grab) / [track.width - thumb.width, 1].max
|
|
483
|
+
editor.scroll(dx: fraction.clamp(0, 1) * maximum - editor.scroll_x)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
def terminal_mouse(event, action)
|
|
487
|
+
column, row = @view.terminal_point(event.position)
|
|
488
|
+
modifiers = event.modifiers.map(&:to_s)
|
|
489
|
+
@workspace.terminal.mouse(button: @terminal_drag, column: column, row: row, action: action,
|
|
490
|
+
shift: modifiers.include?("shift"), alt: modifiers.include?("alt"), control: modifiers.include?("ctrl"))
|
|
491
|
+
end
|
|
492
|
+
def resize_drag(point)
|
|
493
|
+
kind, target, bounds = @resize_drag
|
|
494
|
+
if kind == :split_resize
|
|
495
|
+
value = target[:direction] == :horizontal ? (point.x - bounds.x).to_f / bounds.width : (point.y - bounds.y).to_f / bounds.height
|
|
496
|
+
target[:ratio] = value.clamp(0.1, 0.9)
|
|
497
|
+
else
|
|
498
|
+
size = case target
|
|
499
|
+
when :left then point.x - bounds.x
|
|
500
|
+
when :right then bounds.right - point.x
|
|
501
|
+
when :bottom then bounds.bottom - 26 - point.y
|
|
502
|
+
end
|
|
503
|
+
@workspace.docks[target][:size] = size.clamp(80, target == :bottom ? bounds.height * 0.7 : bounds.width * 0.4)
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ruby 3.2 added Data; Struct provides the same record operations used here.
|
|
4
|
+
unless defined?(Data)
|
|
5
|
+
Data = Struct
|
|
6
|
+
def Data.define(*members, &block)
|
|
7
|
+
Struct.new(*members) do
|
|
8
|
+
members.each { |member| undef_method :"#{member}=" }
|
|
9
|
+
define_method(:initialize) do |*values, **keywords|
|
|
10
|
+
if keywords.any?
|
|
11
|
+
raise ArgumentError, "expected either positional or keyword members" unless values.empty? && keywords.keys.sort == members.sort
|
|
12
|
+
values = members.map { |member| keywords.fetch(member) }
|
|
13
|
+
end
|
|
14
|
+
raise ArgumentError, "wrong number of members" unless values.length == members.length
|
|
15
|
+
super(*values)
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
define_method(:with) do |**changes|
|
|
19
|
+
changes.empty? ? self : self.class.new(**to_h.merge(changes))
|
|
20
|
+
end
|
|
21
|
+
class_eval(&block) if block
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A builder owns immutable settings and a persistent Rope snapshot. Neither
|
|
4
|
+
# the background thread nor provisional reads mutate the visible Tree.
|
|
5
|
+
class Canopus::DisplayMap::LineBuilder
|
|
6
|
+
EMPTY_LINES = Canopus::DisplayMap.const_get(:EMPTY_LINES, false)
|
|
7
|
+
Row = Canopus::DisplayMap.const_get(:Row, false)
|
|
8
|
+
LineSet = Canopus::DisplayMap.const_get(:LineSet, false)
|
|
9
|
+
PendingLineSet = Canopus::DisplayMap.const_get(:PendingLineSet, false)
|
|
10
|
+
UNWRAPPED_LINE = Canopus::DisplayMap.const_get(:UNWRAPPED_LINE, false)
|
|
11
|
+
private_constant :EMPTY_LINES, :Row, :LineSet, :PendingLineSet, :UNWRAPPED_LINE
|
|
12
|
+
|
|
13
|
+
attr_reader :rope, :fold, :blocks
|
|
14
|
+
def initialize(rope, fold, tab, wrap, blocks)
|
|
15
|
+
@rope, @tab, @wrap = rope, tab, wrap
|
|
16
|
+
@fold = Canopus::FoldMap.new
|
|
17
|
+
@fold.instance_variable_set(:@ranges, fold.ranges.dup.freeze)
|
|
18
|
+
@fold.freeze
|
|
19
|
+
@blocks = blocks.blocks.values.group_by(&:row).transform_values do |values|
|
|
20
|
+
values.map { |block| Canopus::BlockMap::Block.new(block.id, block.row, block.text.dup.freeze, block.kind) }.freeze
|
|
21
|
+
end.freeze
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def line(row, provisional: false, from: 0, checkpoint: nil)
|
|
26
|
+
input = @fold.transform(@rope, row, max_bytes: provisional ? 16_384 : nil, from: from, checkpoint: checkpoint)
|
|
27
|
+
return EMPTY_LINES unless input
|
|
28
|
+
text, offsets = @tab.transform(*input, checkpoint: checkpoint)
|
|
29
|
+
wrapped = provisional ? [[text.freeze, offsets.freeze]] : @wrap.transform(text, offsets, checkpoint: checkpoint)
|
|
30
|
+
rows = wrapped.map { |value, positions| Row.new(value, positions, :text, nil) }
|
|
31
|
+
(@blocks[row] || []).each do |block|
|
|
32
|
+
block.text.split("\n", -1).each do |value|
|
|
33
|
+
checkpoint&.call
|
|
34
|
+
rows << Row.new(value.freeze, Array.new(value.length + 1, offsets.last).freeze, block.kind, block.id)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
LineSet.new(rows.freeze)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def pending_lines(first, count)
|
|
41
|
+
hidden = @fold.ranges.map { |range| (@rope.point_at(range.begin).row + 1)..@rope.point_at(range.end).row }
|
|
42
|
+
pointer = 0
|
|
43
|
+
Array.new(count) do |index|
|
|
44
|
+
row = first + index
|
|
45
|
+
pointer += 1 while hidden[pointer] && hidden[pointer].end < row
|
|
46
|
+
if hidden[pointer]&.cover?(row)
|
|
47
|
+
EMPTY_LINES
|
|
48
|
+
else
|
|
49
|
+
count = 1 + (@blocks[row] || []).sum { |block| block.text.count("\n") + 1 }
|
|
50
|
+
count == 1 ? UNWRAPPED_LINE : PendingLineSet.new(count)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Canopus::DisplayMap::Summary < Struct.new(:source_rows, :display_rows, :pending_rows)
|
|
4
|
+
def initialize(source_rows, display_rows, pending_rows)
|
|
5
|
+
super
|
|
6
|
+
freeze
|
|
7
|
+
end
|
|
8
|
+
ZERO = new(0, 0, 0)
|
|
9
|
+
def self.zero = ZERO
|
|
10
|
+
def +(other)
|
|
11
|
+
return other if source_rows.zero?
|
|
12
|
+
return self if other.source_rows.zero?
|
|
13
|
+
Canopus::DisplayMap::Summary.new(source_rows + other.source_rows, display_rows + other.display_rows, pending_rows + other.pending_rows)
|
|
14
|
+
end
|
|
15
|
+
def project_combined(dimension, other)
|
|
16
|
+
case dimension
|
|
17
|
+
when :source_rows then source_rows + other.source_rows
|
|
18
|
+
when :display_rows then display_rows + other.display_rows
|
|
19
|
+
when :pending_rows then pending_rows + other.pending_rows
|
|
20
|
+
else raise ArgumentError, "unknown display summary dimension: #{dimension}"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|