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,525 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "settings"
|
|
6
|
+
require_relative "theme"
|
|
7
|
+
require_relative "vim"
|
|
8
|
+
require_relative "lsp"
|
|
9
|
+
require_relative "pane"
|
|
10
|
+
require_relative "project/tree"
|
|
11
|
+
|
|
12
|
+
module Canopus
|
|
13
|
+
class Workspace
|
|
14
|
+
attr_reader :panes, :active_pane, :buffers, :actions, :settings, :theme, :project, :clients, :root, :docks, :panels
|
|
15
|
+
attr_accessor :window, :show_project, :terminal, :terminal_visible, :selected_project_path, :performance
|
|
16
|
+
attr_reader :message, :palette
|
|
17
|
+
|
|
18
|
+
def initialize(root: Dir.pwd, settings: nil)
|
|
19
|
+
@root = File.realpath(root)
|
|
20
|
+
@settings = settings || Settings.new(Settings.user_path, File.join(@root, ".canopus", "settings.jsonc"))
|
|
21
|
+
@panes, @buffers = [Pane.new], {}
|
|
22
|
+
@active_pane = @panes.first
|
|
23
|
+
@layout = {pane: @active_pane}
|
|
24
|
+
@theme = Theme.new(name: @settings["theme"])
|
|
25
|
+
@actions, @clients, @vim_states = Zaniah::Input::ActionRegistry.new, {}, {}
|
|
26
|
+
@show_project, @message = true, ""
|
|
27
|
+
@project = Project.new(@root) if defined?(Project)
|
|
28
|
+
@docks = {left: {visible: true, size: 220}, right: {visible: false, size: 260}, bottom: {visible: false, size: 220}}
|
|
29
|
+
@panels, @languages = {}, {}
|
|
30
|
+
register_actions
|
|
31
|
+
end
|
|
32
|
+
def editor = @active_pane.active
|
|
33
|
+
def palette=(value)
|
|
34
|
+
previous, @palette = @palette, @closed ? nil : value
|
|
35
|
+
response = previous&.dig(:response)
|
|
36
|
+
unless response.nil? || response.equal?(@palette&.dig(:response))
|
|
37
|
+
response.fulfill({"applied" => false, "failureReason" => value ? "Replaced by another dialog" : "User cancelled"})
|
|
38
|
+
end
|
|
39
|
+
value&.dig(:response)&.fulfill({"applied" => false, "failureReason" => "Workspace closed"}) if @closed
|
|
40
|
+
@palette
|
|
41
|
+
end
|
|
42
|
+
def plugins = @plugins ||= Plugins::Registry.new(self)
|
|
43
|
+
def files
|
|
44
|
+
return @files if @files
|
|
45
|
+
@project_entries = @project ? @project.files(include_directories: true).to_a : []
|
|
46
|
+
@files = @project_entries.reject { |path| path.end_with?("/") }
|
|
47
|
+
end
|
|
48
|
+
def message=(value)
|
|
49
|
+
@message = value.to_s
|
|
50
|
+
notify(@message) unless @message.empty?
|
|
51
|
+
@message
|
|
52
|
+
end
|
|
53
|
+
def notify(text, now: Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
54
|
+
@notifications ||= []
|
|
55
|
+
@notification_id = (@notification_id || 0) + 1
|
|
56
|
+
@notifications << {id: @notification_id, text: text.to_s.slice(0, 2_000), expires: now + 6}
|
|
57
|
+
@notifications.shift while @notifications.length > 3
|
|
58
|
+
@window&.request_frame
|
|
59
|
+
@notification_id
|
|
60
|
+
end
|
|
61
|
+
def notifications = @notifications || []
|
|
62
|
+
def dismiss_notification(id)
|
|
63
|
+
@notifications&.reject! { |item| item[:id] == id }
|
|
64
|
+
@window&.request_frame
|
|
65
|
+
end
|
|
66
|
+
def expire_notifications(now = Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
67
|
+
!!@notifications&.reject! { |item| item[:expires] <= now }
|
|
68
|
+
end
|
|
69
|
+
def refresh_files
|
|
70
|
+
@files = @project_tree = @project_entries = nil
|
|
71
|
+
@finder_index = nil
|
|
72
|
+
if @palette && @palette[:kind] == :files
|
|
73
|
+
@palette.delete(:search)
|
|
74
|
+
update_palette
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
def layout = @layout
|
|
78
|
+
def vim = @vim_states[editor] ||= Vim.new(editor).tap { |state| state.on_command = ->(command) { ex_command(command) } }
|
|
79
|
+
def open(path)
|
|
80
|
+
absolute = canonical_path(path)
|
|
81
|
+
buffer = @buffers[absolute] ||= File.file?(absolute) ? Buffer.open(absolute) : Buffer.new("", path: absolute)
|
|
82
|
+
@vim_states[editor]&.deactivate unless editor&.buffer.equal?(buffer)
|
|
83
|
+
opened = @active_pane.open(buffer)
|
|
84
|
+
@recent_files ||= []
|
|
85
|
+
@recent_files.delete(absolute)
|
|
86
|
+
@recent_files.unshift(absolute)
|
|
87
|
+
@recent_files.pop if @recent_files.length > 100
|
|
88
|
+
@finder_index = nil
|
|
89
|
+
opened.language = definition_for(absolute)
|
|
90
|
+
apply_editor_settings(opened)
|
|
91
|
+
@project_tree&.reveal(absolute.delete_prefix(@root + File::SEPARATOR))
|
|
92
|
+
@window&.request_frame
|
|
93
|
+
opened
|
|
94
|
+
end
|
|
95
|
+
def canonical_path(path)
|
|
96
|
+
absolute = File.expand_path(path, @root)
|
|
97
|
+
return File.realpath(absolute) if File.file?(absolute)
|
|
98
|
+
parent, pieces = File.dirname(absolute), [File.basename(absolute)]
|
|
99
|
+
until File.directory?(parent)
|
|
100
|
+
pieces.unshift(File.basename(parent))
|
|
101
|
+
parent = File.dirname(parent)
|
|
102
|
+
end
|
|
103
|
+
File.join(File.realpath(parent), *pieces)
|
|
104
|
+
end
|
|
105
|
+
def save_buffer(buffer = editor.buffer, path: buffer.path)
|
|
106
|
+
if buffer.is_a?(MultiBuffer)
|
|
107
|
+
buffer.excerpts.map(&:buffer).uniq.each { |source| save_buffer(source) if source.dirty? }
|
|
108
|
+
return buffer
|
|
109
|
+
end
|
|
110
|
+
target = path && canonical_path(path)
|
|
111
|
+
raise Error, "Choose a path before saving" unless target
|
|
112
|
+
existing = @buffers[target]
|
|
113
|
+
raise Error, "Destination is already open in another buffer" if existing && !existing.equal?(buffer)
|
|
114
|
+
previous = buffer.path
|
|
115
|
+
result = buffer.save(target)
|
|
116
|
+
if previous != buffer.path
|
|
117
|
+
invalidate_git
|
|
118
|
+
# Close the old URI before registering this buffer under its new path.
|
|
119
|
+
@opened_lsp_documents&.keys&.each do |client, document|
|
|
120
|
+
next unless document.equal?(buffer)
|
|
121
|
+
@opened_lsp_documents.delete([client, document])
|
|
122
|
+
begin
|
|
123
|
+
client.close_document(LSP::Protocol.uri(previous)) if previous
|
|
124
|
+
rescue StandardError => error
|
|
125
|
+
@message = "File saved; language server close failed: #{error.message}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
@buffers.delete_if { |_, value| value.equal?(buffer) }
|
|
129
|
+
@buffers[buffer.path] = buffer
|
|
130
|
+
@panes.each do |pane|
|
|
131
|
+
pane.editors.each do |current|
|
|
132
|
+
next unless current.buffer.equal?(buffer)
|
|
133
|
+
current.language = definition_for(buffer.path)
|
|
134
|
+
apply_editor_settings(current)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
refresh_files
|
|
138
|
+
end
|
|
139
|
+
@opened_lsp_documents&.each_key do |client, document|
|
|
140
|
+
next unless document.equal?(buffer)
|
|
141
|
+
begin
|
|
142
|
+
client.save_document(LSP::Protocol.uri(buffer.path))
|
|
143
|
+
rescue StandardError => error
|
|
144
|
+
self.message = "File saved; language server notification failed: #{error.message}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
result
|
|
148
|
+
end
|
|
149
|
+
def new_buffer
|
|
150
|
+
@vim_states[editor]&.deactivate
|
|
151
|
+
buffer = Buffer.new
|
|
152
|
+
@buffers[buffer.object_id] = buffer
|
|
153
|
+
opened = @active_pane.open(buffer)
|
|
154
|
+
apply_editor_settings(opened)
|
|
155
|
+
opened
|
|
156
|
+
end
|
|
157
|
+
def focus(pane)
|
|
158
|
+
raise Error, "pane is not in workspace" unless @panes.include?(pane)
|
|
159
|
+
@vim_states[editor]&.deactivate unless pane.equal?(@active_pane)
|
|
160
|
+
@active_pane = pane
|
|
161
|
+
new_buffer unless editor
|
|
162
|
+
@window&.request_frame
|
|
163
|
+
end
|
|
164
|
+
def activate_tab(pane, tab)
|
|
165
|
+
raise Error, "tab is not in pane" unless pane.editors.include?(tab)
|
|
166
|
+
@vim_states[editor]&.deactivate unless tab.equal?(editor)
|
|
167
|
+
focus(pane)
|
|
168
|
+
pane.activate(pane.editors.index(tab))
|
|
169
|
+
end
|
|
170
|
+
def split(direction = :horizontal)
|
|
171
|
+
raise ArgumentError, "invalid split" unless [:horizontal, :vertical].include?(direction)
|
|
172
|
+
pane = Pane.new
|
|
173
|
+
if editor
|
|
174
|
+
opened = pane.open(editor.buffer)
|
|
175
|
+
opened.language = editor.language_document.definition
|
|
176
|
+
apply_editor_settings(opened)
|
|
177
|
+
end
|
|
178
|
+
replace = lambda do |node|
|
|
179
|
+
if node[:pane] == @active_pane
|
|
180
|
+
{direction: direction, children: [node, {pane: pane}]}
|
|
181
|
+
elsif node[:children]
|
|
182
|
+
node.merge(children: node[:children].map { |child| replace.call(child) })
|
|
183
|
+
else node
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
@layout = replace.call(@layout)
|
|
187
|
+
@panes << pane
|
|
188
|
+
focus(pane)
|
|
189
|
+
pane
|
|
190
|
+
end
|
|
191
|
+
def close_editor(current = editor, discard: false)
|
|
192
|
+
raise Error, "buffer has unsaved changes" if current.buffer.dirty? && !discard
|
|
193
|
+
pane = @panes.find { |item| item.editors.include?(current) }
|
|
194
|
+
raise Error, "editor is not in workspace" unless pane
|
|
195
|
+
@vim_states.delete(current)&.dispose
|
|
196
|
+
pane.close(current, discard: discard)
|
|
197
|
+
remaining = @panes.flat_map(&:editors).any? { |item| item.buffer.equal?(current.buffer) }
|
|
198
|
+
remaining ||= @buffers.values.grep(MultiBuffer).any? { |multi| multi.excerpts.any? { |excerpt| excerpt.buffer.equal?(current.buffer) } }
|
|
199
|
+
unless remaining
|
|
200
|
+
close_language_documents(current.buffer)
|
|
201
|
+
@buffers.delete_if { |_, buffer| buffer.equal?(current.buffer) }
|
|
202
|
+
current.buffer.close
|
|
203
|
+
end
|
|
204
|
+
new_buffer unless editor
|
|
205
|
+
end
|
|
206
|
+
def clear_vim_states
|
|
207
|
+
@vim_states.each_value(&:dispose)
|
|
208
|
+
@vim_states.clear
|
|
209
|
+
end
|
|
210
|
+
def move_tab(from:, to:, editor: from.active, index: nil)
|
|
211
|
+
raise Error, "unknown pane" unless @panes.include?(from) && @panes.include?(to)
|
|
212
|
+
raise Error, "tab is not in pane" unless from.editors.include?(editor)
|
|
213
|
+
index ||= to.editors.length
|
|
214
|
+
raise Error, "invalid tab position" unless index.is_a?(Integer) && index.between?(0, to.editors.length)
|
|
215
|
+
index -= 1 if from.equal?(to) && from.editors.index(editor) < index
|
|
216
|
+
pinned = from.pinned.include?(editor)
|
|
217
|
+
@vim_states[self.editor]&.deactivate unless editor.equal?(self.editor)
|
|
218
|
+
from.detach(editor)
|
|
219
|
+
to.editors.insert(index, editor)
|
|
220
|
+
to.active_index = index
|
|
221
|
+
to.pin(editor) if pinned
|
|
222
|
+
focus(to)
|
|
223
|
+
end
|
|
224
|
+
def toggle_dock(side)
|
|
225
|
+
dock = @docks.fetch(side)
|
|
226
|
+
dock[:visible] = !dock[:visible]
|
|
227
|
+
end
|
|
228
|
+
def register_panel(name, side: :left, &render)
|
|
229
|
+
raise ArgumentError, "invalid dock side" unless @docks.key?(side)
|
|
230
|
+
@panels[name] = [side, render]
|
|
231
|
+
register_action("panel.#{name}") { @docks[side][:visible] = true }
|
|
232
|
+
end
|
|
233
|
+
def register_action(name, description: name, &block) = @actions.register(name, description: description, &block)
|
|
234
|
+
def definition_for(path)
|
|
235
|
+
@languages.values.find { |definition| definition.extensions.include?(File.extname(path.to_s)) || definition.extensions.include?(File.basename(path.to_s)) } || Language.for_path(path)
|
|
236
|
+
end
|
|
237
|
+
def register_language(name, extensions:, lexer: "plaintext", comment: "#", indent_open: "[\\[{(]\\s*$", indent_close: "^\\s*[\\]})]", servers: [])
|
|
238
|
+
raise ArgumentError, "language extensions must be strings" unless extensions.is_a?(Array) && extensions.all? { |extension| extension.is_a?(String) }
|
|
239
|
+
@languages[name] = Language::Definition.new(name, lexer, extensions.freeze, comment, Regexp.new(indent_open), Regexp.new(indent_close), servers)
|
|
240
|
+
@panes.each do |pane|
|
|
241
|
+
pane.editors.each do |current|
|
|
242
|
+
current.language = definition_for(current.buffer.path)
|
|
243
|
+
apply_editor_settings(current)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
def call(name, *args)
|
|
248
|
+
@actions.call(name, *args)
|
|
249
|
+
@window&.request_frame
|
|
250
|
+
rescue StandardError => error
|
|
251
|
+
@message = error.message
|
|
252
|
+
@window&.request_frame
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def theme=(theme)
|
|
256
|
+
@theme = theme.is_a?(Theme) ? theme : Theme.new(name: theme)
|
|
257
|
+
@window&.request_frame
|
|
258
|
+
end
|
|
259
|
+
def palette_open(kind)
|
|
260
|
+
self.palette = {kind: kind, query: +"", index: 0, matches: []}
|
|
261
|
+
if [:search, :replace_query, :project_search].include?(kind)
|
|
262
|
+
@palette[:search_options] = {regexp: false, case_sensitive: true, whole_word: false, selection_only: false}
|
|
263
|
+
@palette[:selection] = editor.primary.range unless editor.primary.empty?
|
|
264
|
+
@palette[:editor], @palette[:version] = editor, editor.buffer.version
|
|
265
|
+
end
|
|
266
|
+
update_palette
|
|
267
|
+
end
|
|
268
|
+
def show_snippet_choices(current = editor)
|
|
269
|
+
choices = current.snippet_choices
|
|
270
|
+
return unless choices
|
|
271
|
+
self.palette = {kind: :snippet_choices, editor: current, query: +"", index: 0, matches: choices.dup}
|
|
272
|
+
end
|
|
273
|
+
def update_palette
|
|
274
|
+
return unless @palette
|
|
275
|
+
if [:completion, :locations, :symbols, :code_actions, :outline, :branches, :settings_keys, :snippet_choices].include?(@palette[:kind])
|
|
276
|
+
labels = @palette[:all_matches] ||= @palette[:matches].dup
|
|
277
|
+
session = @palette[:search] ||= Spica::Index.new(labels).session
|
|
278
|
+
session.query = @palette[:query]
|
|
279
|
+
matches = session.matches(12)
|
|
280
|
+
@palette[:indices] = matches.map(&:index)
|
|
281
|
+
@palette[:matches] = matches.map(&:candidate)
|
|
282
|
+
@palette[:index] = 0
|
|
283
|
+
return
|
|
284
|
+
end
|
|
285
|
+
@palette[:search] ||= case @palette[:kind]
|
|
286
|
+
when :commands then Spica::Index.new(@actions.entries.keys).session
|
|
287
|
+
when :files
|
|
288
|
+
@finder_index ||= Spica::Index.new(((@recent_files || []).select { |path| File.file?(path) }.map { |path| path.delete_prefix(@root + File::SEPARATOR) } + files).uniq, tie_break: :index)
|
|
289
|
+
@finder_index.session
|
|
290
|
+
end
|
|
291
|
+
@palette[:matches] = if @palette[:search]
|
|
292
|
+
@palette[:search].query = @palette[:query]
|
|
293
|
+
@palette[:search].matches(12).map(&:candidate)
|
|
294
|
+
else []
|
|
295
|
+
end
|
|
296
|
+
@palette[:index] = @palette[:index].clamp(0, [@palette[:matches].length - 1, 0].max)
|
|
297
|
+
end
|
|
298
|
+
def palette_accept
|
|
299
|
+
search_state = @palette.slice(:search_options, :selection, :editor, :version)
|
|
300
|
+
if @palette[:kind] == :snippet_choices
|
|
301
|
+
current = @palette
|
|
302
|
+
self.palette = nil
|
|
303
|
+
return @panes.any? { |pane| pane.editors.include?(current[:editor]) } && current[:editor].choose_snippet(current[:matches][current[:index]])
|
|
304
|
+
elsif [:completion, :locations, :symbols, :code_actions].include?(@palette[:kind])
|
|
305
|
+
current = @palette
|
|
306
|
+
self.palette = nil
|
|
307
|
+
index = current[:indices] ? current[:indices][current[:index]] : current[:index]
|
|
308
|
+
return accept_language_result(current, index) if index
|
|
309
|
+
elsif @palette[:kind] == :outline
|
|
310
|
+
current = @palette
|
|
311
|
+
index = @palette[:indices] ? @palette[:indices][@palette[:index]] : @palette[:index]
|
|
312
|
+
symbol = index && @palette[:items][index]
|
|
313
|
+
self.palette = nil
|
|
314
|
+
if symbol && current[:editor].equal?(editor) && current[:version] == editor.buffer.version && current[:document].equal?(editor.language_document)
|
|
315
|
+
editor.select(symbol.selection.begin)
|
|
316
|
+
editor.reveal_cursor
|
|
317
|
+
end
|
|
318
|
+
return
|
|
319
|
+
end
|
|
320
|
+
selected = @palette[:matches][@palette[:index]]
|
|
321
|
+
kind, query, pattern = @palette.values_at(:kind, :query, :pattern)
|
|
322
|
+
self.palette = nil
|
|
323
|
+
if kind == :files && selected
|
|
324
|
+
open(selected)
|
|
325
|
+
elsif kind == :commands && selected
|
|
326
|
+
call(selected)
|
|
327
|
+
elsif kind == :search
|
|
328
|
+
pattern, options = search_query(query, search_state)
|
|
329
|
+
matches = editor.search(pattern, **options)
|
|
330
|
+
editor.select(matches.first.begin, matches.first.end) unless matches.empty?
|
|
331
|
+
@message = "#{matches.length} matches"
|
|
332
|
+
elsif kind == :project_search
|
|
333
|
+
search_project(query, **search_state.fetch(:search_options, {}).reject { |key, _| key == :selection_only })
|
|
334
|
+
elsif kind == :replace_query
|
|
335
|
+
search_query(query, search_state) # Validate before accepting replacement text.
|
|
336
|
+
self.palette = {kind: :replace_value, query: +"", pattern: query, index: 0, matches: [], **search_state}
|
|
337
|
+
elsif kind == :replace_value
|
|
338
|
+
expression, options = search_query(pattern, search_state)
|
|
339
|
+
@message = "#{replace_in_buffer(expression, query, **options)} replacements (unsaved)"
|
|
340
|
+
elsif kind == :save_as
|
|
341
|
+
save_buffer(path: query)
|
|
342
|
+
elsif kind == :rename
|
|
343
|
+
language_request(:rename, name: query)
|
|
344
|
+
elsif kind == :workspace_symbols
|
|
345
|
+
language_request(:workspace_symbols, query: query)
|
|
346
|
+
elsif kind == :create_file || kind == :create_folder
|
|
347
|
+
create_project_entry(query, directory: kind == :create_folder)
|
|
348
|
+
elsif kind == :rename_file
|
|
349
|
+
rename_project_entry(@selected_project_path, query)
|
|
350
|
+
elsif kind == :trash_file && selected == "Move to trash"
|
|
351
|
+
trash_project_entry(@selected_project_path)
|
|
352
|
+
elsif kind == :branches && selected
|
|
353
|
+
checkout_branch(selected)
|
|
354
|
+
elsif kind == :settings_keys && selected
|
|
355
|
+
editor.insert_text("#{JSON.generate(selected)}: #{JSON.generate(Settings::DEFAULTS.fetch(selected))}", auto_indent: false)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
def search_query(query, state)
|
|
359
|
+
if state[:editor] && (!state[:editor].equal?(editor) || state[:version] != editor.buffer.version)
|
|
360
|
+
raise Error, "Document changed; open search again"
|
|
361
|
+
end
|
|
362
|
+
options = state.fetch(:search_options, {})
|
|
363
|
+
pattern = options[:regexp] ? Regexp.new(query, options[:case_sensitive] == false ? Regexp::IGNORECASE : 0, timeout: 0.25) : query
|
|
364
|
+
search_options = options.slice(:case_sensitive, :whole_word)
|
|
365
|
+
if options[:selection_only]
|
|
366
|
+
raise Error, "Select text before opening search" unless state[:selection]
|
|
367
|
+
search_options[:range] = state[:selection]
|
|
368
|
+
end
|
|
369
|
+
[pattern, search_options]
|
|
370
|
+
end
|
|
371
|
+
def connect_server(language, command)
|
|
372
|
+
options = normalize_server_options({"command" => command})
|
|
373
|
+
(@client_lock ||= Mutex.new).synchronize { ensure_language_server(language, options) }
|
|
374
|
+
end
|
|
375
|
+
def drain
|
|
376
|
+
if @main_queue
|
|
377
|
+
loop do
|
|
378
|
+
callback = @main_queue.pop(true)
|
|
379
|
+
begin
|
|
380
|
+
callback.call unless @closed
|
|
381
|
+
rescue StandardError => error
|
|
382
|
+
@message = error.message
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
rescue ThreadError
|
|
387
|
+
nil
|
|
388
|
+
end
|
|
389
|
+
def close
|
|
390
|
+
@closed = true
|
|
391
|
+
cancel_project_search
|
|
392
|
+
@palette&.dig(:response)&.fulfill({"applied" => false, "failureReason" => "Workspace closed"})
|
|
393
|
+
self.palette = nil
|
|
394
|
+
@plugins&.close
|
|
395
|
+
@watcher&.close
|
|
396
|
+
stop_language_servers
|
|
397
|
+
@terminal&.close
|
|
398
|
+
clear_vim_states
|
|
399
|
+
@panes.each { |pane| pane.editors.each(&:dispose) }
|
|
400
|
+
@buffers.each_value(&:close)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
private
|
|
404
|
+
def register_actions
|
|
405
|
+
register_action("file.new") { new_buffer }
|
|
406
|
+
register_action("file.save") { editor.buffer.path || editor.buffer.is_a?(MultiBuffer) ? save_buffer : palette_open(:save_as) }
|
|
407
|
+
register_action("file.close") { close_editor }
|
|
408
|
+
register_action("file.find") { palette_open(:files) }
|
|
409
|
+
register_action("project.new_file") { project_prompt(:create_file) }
|
|
410
|
+
register_action("project.new_folder") { project_prompt(:create_folder) }
|
|
411
|
+
register_action("project.rename") { project_prompt(:rename_file) }
|
|
412
|
+
register_action("project.trash") { project_prompt(:trash_file) }
|
|
413
|
+
register_action("git.diff") { show_git_diff }
|
|
414
|
+
register_action("git.toggle_hunk") { toggle_git_hunk }
|
|
415
|
+
register_action("git.blame") { show_git_blame }
|
|
416
|
+
register_action("git.revert_hunk") { revert_current_hunk }
|
|
417
|
+
register_action("git.branches") { self.palette = {kind: :branches, query: +"", index: 0, matches: git ? git.branches : []} }
|
|
418
|
+
register_action("command.palette") { palette_open(:commands) }
|
|
419
|
+
register_action("pane.split_right") { split(:horizontal) }
|
|
420
|
+
register_action("pane.split_down") { split(:vertical) }
|
|
421
|
+
register_action("pane.next") { focus(@panes[(@panes.index(@active_pane) + 1) % @panes.length]) }
|
|
422
|
+
register_action("tab.pin") { @active_pane.pin }
|
|
423
|
+
register_action("tab.back") { @vim_states[editor]&.deactivate; @active_pane.back }
|
|
424
|
+
register_action("tab.forward") { @vim_states[editor]&.deactivate; @active_pane.forward }
|
|
425
|
+
register_action("edit.undo") { editor.undo }
|
|
426
|
+
register_action("edit.redo") { editor.redo }
|
|
427
|
+
register_action("edit.select_all") { editor.select_all }
|
|
428
|
+
register_action("edit.select_next") { editor.select_next_occurrence }
|
|
429
|
+
register_action("edit.select_all_occurrences") { editor.select_next_occurrence(all: true) }
|
|
430
|
+
register_action("edit.duplicate_line") { editor.duplicate_lines }
|
|
431
|
+
%i[up down].each do |direction|
|
|
432
|
+
register_action("edit.move_line_#{direction}") { editor.move_lines(direction); editor.reveal_cursor }
|
|
433
|
+
register_action("editor.paragraph_#{direction}") { editor.move(:"paragraph_#{direction}") }
|
|
434
|
+
end
|
|
435
|
+
register_action("edit.toggle_comment") { editor.toggle_comment(prefix: editor.language_document.definition.comment) }
|
|
436
|
+
register_action("language.outline") { show_outline }
|
|
437
|
+
register_action("language.workspace_symbols") { palette_open(:workspace_symbols) }
|
|
438
|
+
%i[completion hover definition typeDefinition implementation references formatting codeAction signatureHelp documentSymbol inlayHint codeLens diagnostic semantic_tokens].each do |kind|
|
|
439
|
+
register_action("language.#{kind}") { language_request(kind) }
|
|
440
|
+
end
|
|
441
|
+
register_action("language.rename") { self.palette = {kind: :rename, query: +"", index: 0, matches: []} }
|
|
442
|
+
register_action("editor.fold") { fold_current }
|
|
443
|
+
register_action("editor.unfold") { editor.display_map.unfold(editor.primary.head) }
|
|
444
|
+
register_action("edit.indent") { editor.indent }
|
|
445
|
+
register_action("edit.outdent") { editor.indent(outdent: true) }
|
|
446
|
+
register_action("search.buffer") { palette_open(:search) }
|
|
447
|
+
register_action("search.project") { palette_open(:project_search) }
|
|
448
|
+
register_action("search.replace") { palette_open(:replace_query) }
|
|
449
|
+
register_action("settings.open") { open_settings }
|
|
450
|
+
register_action("settings.complete") { settings_completions }
|
|
451
|
+
register_action("language.diagnostics") { show_diagnostics }
|
|
452
|
+
register_action("view.project") { @show_project = !@show_project }
|
|
453
|
+
[:left, :right, :bottom].each { |side| register_action("view.dock_#{side}") { toggle_dock(side) } }
|
|
454
|
+
register_action("view.wrap") { editor.display_map.wrap_width = editor.display_map.wrap_map.width ? nil : 100 }
|
|
455
|
+
register_action("view.theme") { self.theme = @theme.name.include?("Dark") ? "Canopus Light" : "Canopus Dark" }
|
|
456
|
+
register_action("view.vim") do
|
|
457
|
+
@settings.merge!("vim_mode" => !@settings["vim_mode"])
|
|
458
|
+
clear_vim_states unless @settings["vim_mode"]
|
|
459
|
+
end
|
|
460
|
+
register_action("view.terminal") do
|
|
461
|
+
@terminal ||= Terminal::PTY.new(cwd: @root, columns: 100, rows: 12)
|
|
462
|
+
@terminal_visible = !@terminal_visible
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
def encode_layout(node)
|
|
466
|
+
node[:pane] ? {pane: @panes.index(node[:pane])} : {direction: node[:direction], ratio: node.fetch(:ratio, 0.5), children: node[:children].map { |child| encode_layout(child) }}
|
|
467
|
+
end
|
|
468
|
+
def project_prompt(kind)
|
|
469
|
+
path = @selected_project_path || editor.buffer.path&.delete_prefix(@root + File::SEPARATOR) || ""
|
|
470
|
+
@selected_project_path = path
|
|
471
|
+
query = if kind == :rename_file || kind == :trash_file
|
|
472
|
+
path
|
|
473
|
+
else
|
|
474
|
+
directory = File.directory?(File.join(@root, path)) ? path : File.dirname(path)
|
|
475
|
+
directory == "." || directory.empty? ? "" : directory + "/"
|
|
476
|
+
end
|
|
477
|
+
self.palette = {kind: kind, query: +query, index: 0, matches: kind == :trash_file ? ["Move to trash", "Cancel"] : []}
|
|
478
|
+
end
|
|
479
|
+
def decode_layout(node, depth = 0, panes: @panes)
|
|
480
|
+
raise Error, "session layout too deep" if depth > 32
|
|
481
|
+
if node.key?("pane")
|
|
482
|
+
{pane: panes.fetch(node["pane"])}
|
|
483
|
+
else
|
|
484
|
+
direction = node.fetch("direction").to_sym
|
|
485
|
+
raise Error, "invalid split direction" unless [:horizontal, :vertical].include?(direction)
|
|
486
|
+
children = node.fetch("children")
|
|
487
|
+
raise Error, "split must contain two panes" unless children.is_a?(Array) && children.length == 2
|
|
488
|
+
ratio = node.fetch("ratio", 0.5)
|
|
489
|
+
raise Error, "invalid split ratio" unless ratio.is_a?(Numeric) && ratio.between?(0.1, 0.9)
|
|
490
|
+
{direction: direction, ratio: ratio, children: children.map { |child| decode_layout(child, depth + 1, panes: panes) }}
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
def ex_command(command)
|
|
494
|
+
case command
|
|
495
|
+
when "w" then call("file.save")
|
|
496
|
+
when /\Aw / then save_buffer(path: command.delete_prefix("w "))
|
|
497
|
+
when "q" then call("file.close")
|
|
498
|
+
when "q!" then close_editor(discard: true)
|
|
499
|
+
when "wq" then call("file.save"); call("file.close")
|
|
500
|
+
when "split", "sp" then split(:vertical)
|
|
501
|
+
when "vsplit", "vs" then split(:horizontal)
|
|
502
|
+
else open(command.delete_prefix("e ")) if command.start_with?("e ")
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
require_relative "workspace/session_persistable"
|
|
509
|
+
require_relative "workspace/project_tree_editable"
|
|
510
|
+
require_relative "workspace/file_change_aware"
|
|
511
|
+
require_relative "workspace/language_server_configurable"
|
|
512
|
+
require_relative "workspace/language_aware"
|
|
513
|
+
require_relative "workspace/git_aware"
|
|
514
|
+
require_relative "workspace/project_searchable"
|
|
515
|
+
require_relative "workspace/settings_aware"
|
|
516
|
+
require_relative "workspace/file_previewable"
|
|
517
|
+
Canopus::Workspace.include Canopus::Workspace::SessionPersistable
|
|
518
|
+
Canopus::Workspace.include Canopus::Workspace::ProjectTreeEditable
|
|
519
|
+
Canopus::Workspace.include Canopus::Workspace::FileChangeAware
|
|
520
|
+
Canopus::Workspace.include Canopus::Workspace::LanguageServerConfigurable
|
|
521
|
+
Canopus::Workspace.include Canopus::Workspace::LanguageAware
|
|
522
|
+
Canopus::Workspace.include Canopus::Workspace::GitAware
|
|
523
|
+
Canopus::Workspace.include Canopus::Workspace::ProjectSearchable
|
|
524
|
+
Canopus::Workspace.include Canopus::Workspace::SettingsAware
|
|
525
|
+
Canopus::Workspace.include Canopus::Workspace::FilePreviewable
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canopus
|
|
4
|
+
class WrapMap
|
|
5
|
+
attr_reader :width, :font, :font_size, :font_paths, :typesetter
|
|
6
|
+
def initialize(width: nil, font: nil, font_size: 14, font_paths: nil, typesetter: nil)
|
|
7
|
+
raise ArgumentError, "use typesetter or font/font_paths, not both" if typesetter && (font || font_paths)
|
|
8
|
+
font ||= typesetter&.font
|
|
9
|
+
valid = font ? width.is_a?(Numeric) && width.finite? && width.positive? : width.is_a?(Integer) && width.positive?
|
|
10
|
+
raise ArgumentError, "wrap width must be positive" if width && !valid
|
|
11
|
+
raise ArgumentError, "font size must be finite and positive" unless font_size.is_a?(Numeric) && font_size.finite? && font_size.positive?
|
|
12
|
+
@width, @font, @font_size = width, font, font_size
|
|
13
|
+
@font_paths = font_paths&.frozen? ? font_paths : font_paths&.dup&.freeze
|
|
14
|
+
@typesetter, @typesetters = typesetter, {}
|
|
15
|
+
if @font
|
|
16
|
+
@layout_template = if typesetter
|
|
17
|
+
typesetter.fork(capacity: 32)
|
|
18
|
+
else
|
|
19
|
+
Zaniah::TextSystem::Typesetter.new(
|
|
20
|
+
font: Alhena::Font.new(@font.data, index: @font.index, axes: @font.axis_values),
|
|
21
|
+
font_db: Zaniah::TextSystem::FontDB.new(paths: @font_paths), capacity: 32,
|
|
22
|
+
shaper: :native, segmenter: :native)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
def transform(text, offsets, checkpoint: nil)
|
|
27
|
+
return pixel_rows(text, offsets, checkpoint) if @width && @font
|
|
28
|
+
return [[text.freeze, offsets.freeze]] if !@width || (text.ascii_only? && text.length <= @width)
|
|
29
|
+
rows, offset, chunk, positions, cells = [], 0, +"", [offsets.first], 0
|
|
30
|
+
text.each_grapheme_cluster do |grapheme|
|
|
31
|
+
checkpoint&.call if (offset & 1023).zero?
|
|
32
|
+
advance = grapheme.ascii_only? ? grapheme.length : Zaniah::Unicode.width(grapheme)
|
|
33
|
+
if !chunk.empty? && cells + advance > @width
|
|
34
|
+
rows << [chunk.freeze, positions.freeze]
|
|
35
|
+
chunk, positions, cells = +"", [offsets[offset]], 0
|
|
36
|
+
end
|
|
37
|
+
chunk << grapheme
|
|
38
|
+
cells += advance
|
|
39
|
+
grapheme.length.times do
|
|
40
|
+
checkpoint&.call if (offset & 1023).zero?
|
|
41
|
+
offset += 1
|
|
42
|
+
positions << offsets[offset]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
rows << [chunk.freeze, positions.freeze]
|
|
46
|
+
rows
|
|
47
|
+
end
|
|
48
|
+
def close
|
|
49
|
+
@typesetters.each_value(&:close)
|
|
50
|
+
@typesetters.clear
|
|
51
|
+
@layout_template&.close
|
|
52
|
+
@layout_template = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
def pixel_rows(text, offsets, checkpoint)
|
|
57
|
+
# The template is never used for shaping. Each owner copies its complete
|
|
58
|
+
# typography without reading the rendering thread's mutable caches.
|
|
59
|
+
typesetter = @typesetters[Thread.current] ||= @layout_template.fork(capacity: 32)
|
|
60
|
+
rows, pending, positions, column = [], +"", [offsets.first], 0
|
|
61
|
+
# Bound each shaping input; keep grapheme clusters intact, including one
|
|
62
|
+
# unusually large cluster. The last row carries into the next batch.
|
|
63
|
+
pixel_graphemes(typesetter, text).each do |grapheme|
|
|
64
|
+
checkpoint&.call
|
|
65
|
+
pending << grapheme
|
|
66
|
+
grapheme.length.times { column += 1; positions << offsets[column] }
|
|
67
|
+
next if pending.bytesize < 4096
|
|
68
|
+
parts = split_pixels(typesetter, pending, positions, checkpoint)
|
|
69
|
+
pending, positions = parts.pop
|
|
70
|
+
rows.concat(parts)
|
|
71
|
+
pending, positions = pending.dup, positions.dup
|
|
72
|
+
end
|
|
73
|
+
rows.concat(split_pixels(typesetter, pending, positions, checkpoint))
|
|
74
|
+
rows
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def split_pixels(typesetter, text, offsets, checkpoint)
|
|
78
|
+
layout = typesetter.layout_line(text, size: @font_size)
|
|
79
|
+
return [[text.freeze, offsets.freeze]] if layout.width <= @width
|
|
80
|
+
clusters, bytes, columns = pixel_graphemes(typesetter, text).to_a, [0], [0]
|
|
81
|
+
clusters.each { |value| bytes << bytes.last + value.bytesize; columns << columns.last + value.length }
|
|
82
|
+
rows, first = [], 0
|
|
83
|
+
while first < clusters.length
|
|
84
|
+
checkpoint&.call
|
|
85
|
+
origin = layout.x_for_index(bytes[first])
|
|
86
|
+
last = first + 1
|
|
87
|
+
last += 1 while last < clusters.length && layout.x_for_index(bytes[last + 1]) - origin <= @width
|
|
88
|
+
value = loop do
|
|
89
|
+
value = text.byteslice(bytes[first]...bytes[last])
|
|
90
|
+
break value if last == first + 1 || typesetter.layout_line(value, size: @font_size).width <= @width
|
|
91
|
+
last -= 1 # Re-shaping a row can change boundary kerning/ligatures.
|
|
92
|
+
end
|
|
93
|
+
rows << [value.freeze, offsets[columns[first]..columns[last]].freeze]
|
|
94
|
+
first = last
|
|
95
|
+
end
|
|
96
|
+
rows
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def pixel_graphemes(typesetter, text)
|
|
100
|
+
return text.each_grapheme_cluster if typesetter.segmenter.equal?(Zaniah::Unicode)
|
|
101
|
+
parts = typesetter.segmenter.grapheme_clusters(text)
|
|
102
|
+
unless parts.is_a?(Array) && parts.all? { |part| part.is_a?(String) && !part.empty? && part.valid_encoding? } && parts.join == text
|
|
103
|
+
raise Zaniah::Error, "segmenter must partition the UTF-8 text into nonempty strings"
|
|
104
|
+
end
|
|
105
|
+
parts
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/canopus.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "canopus/version"
|
|
4
|
+
require_relative "canopus/data_compat"
|
|
5
|
+
require_relative "canopus/regexp_compat"
|
|
6
|
+
require_relative "canopus/match_data_compat"
|
|
7
|
+
require_relative "canopus/error"
|
|
8
|
+
require_relative "canopus/save_conflict"
|
|
9
|
+
|
|
10
|
+
require "denebola"
|
|
11
|
+
require "zaniah"
|
|
12
|
+
require_relative "canopus/patch"
|
|
13
|
+
require_relative "canopus/buffer"
|
|
14
|
+
require_relative "canopus/multi_buffer"
|
|
15
|
+
require_relative "canopus/display_map"
|
|
16
|
+
require_relative "canopus/editor"
|
|
17
|
+
require_relative "canopus/language"
|
|
18
|
+
require "spica"
|
|
19
|
+
require_relative "canopus/project"
|
|
20
|
+
require_relative "canopus/git"
|
|
21
|
+
require_relative "canopus/terminal"
|
|
22
|
+
require_relative "canopus/workspace"
|
|
23
|
+
require_relative "canopus/plugins"
|
|
24
|
+
require_relative "canopus/controller"
|