canopus 0.4.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +191 -19
- data/assets/keymaps/emacs.jsonc +5 -0
- data/assets/keymaps/jetbrains.jsonc +5 -0
- data/assets/keymaps/sublime.jsonc +5 -0
- data/assets/keymaps/vscode.jsonc +5 -0
- data/docs/debugging.md +143 -0
- data/docs/distribution.md +40 -9
- data/docs/lsp.md +131 -0
- data/docs/performance.md +18 -0
- data/docs/providers.md +36 -0
- data/docs/release-log.md +12 -0
- data/docs/tasks.md +64 -0
- data/docs/testing.md +49 -0
- data/docs/vim.md +3 -0
- data/exe/canopus +4 -1
- data/lib/canopus/buffer.rb +291 -50
- data/lib/canopus/cli.rb +5 -2
- data/lib/canopus/command.rb +13 -4
- data/lib/canopus/controller.rb +183 -40
- data/lib/canopus/debug/breakpoints.rb +550 -0
- data/lib/canopus/debug/configuration.rb +291 -0
- data/lib/canopus/debug/console.rb +243 -0
- data/lib/canopus/debug/panel.rb +487 -0
- data/lib/canopus/debug/session.rb +354 -0
- data/lib/canopus/decoration.rb +4 -2
- data/lib/canopus/denebola_compat.rb +27 -0
- data/lib/canopus/diagnostics.rb +173 -0
- data/lib/canopus/display_map/overlay_map.rb +6 -2
- data/lib/canopus/editor/snippet_expandable.rb +3 -2
- data/lib/canopus/editor.rb +122 -31
- data/lib/canopus/git/state.rb +100 -0
- data/lib/canopus/language/background_analysis.rb +93 -14
- data/lib/canopus/language/document.rb +70 -34
- data/lib/canopus/language/symbol.rb +1 -0
- data/lib/canopus/language/syntax_worker.rb +61 -9
- data/lib/canopus/language.rb +3 -0
- data/lib/canopus/minimap.rb +201 -0
- data/lib/canopus/patch.rb +3 -1
- data/lib/canopus/plugins/api.rb +4 -2
- data/lib/canopus/plugins/isolated_runtime.rb +64 -7
- data/lib/canopus/plugins/registry.rb +114 -3
- data/lib/canopus/plugins.rb +8 -7
- data/lib/canopus/project.rb +15 -6
- data/lib/canopus/provider.rb +192 -0
- data/lib/canopus/regexp_compat.rb +36 -4
- data/lib/canopus/selection.rb +12 -0
- data/lib/canopus/settings/schema.rb +201 -0
- data/lib/canopus/settings.rb +328 -49
- data/lib/canopus/task/configuration.rb +356 -0
- data/lib/canopus/task/problem_matcher.rb +264 -0
- data/lib/canopus/task/runner.rb +314 -0
- data/lib/canopus/test_runner/bounded_ignore.rb +144 -0
- data/lib/canopus/test_runner/discovery.rb +97 -0
- data/lib/canopus/test_runner/execution.rb +194 -0
- data/lib/canopus/test_runner/minitest.rb +116 -0
- data/lib/canopus/test_runner/rspec.rb +103 -0
- data/lib/canopus/test_runner/safe_walker.rb +37 -0
- data/lib/canopus/test_runner.rb +54 -0
- data/lib/canopus/theme/import.rb +131 -0
- data/lib/canopus/theme.rb +28 -4
- data/lib/canopus/version.rb +1 -1
- data/lib/canopus/vim/motionable.rb +1 -1
- data/lib/canopus/vim.rb +10 -16
- data/lib/canopus/workspace/auto_savable.rb +83 -0
- data/lib/canopus/workspace/debug_aware.rb +600 -0
- data/lib/canopus/workspace/edit/plan.rb +2 -2
- data/lib/canopus/workspace/editor_configurable.rb +113 -0
- data/lib/canopus/workspace/file_change_aware.rb +11 -3
- data/lib/canopus/workspace/git_aware.rb +80 -34
- data/lib/canopus/workspace/git_blame.rb +175 -0
- data/lib/canopus/workspace/git_conflict_resolution.rb +410 -0
- data/lib/canopus/workspace/git_diff_view.rb +641 -0
- data/lib/canopus/workspace/git_history.rb +171 -0
- data/lib/canopus/workspace/git_remote.rb +267 -0
- data/lib/canopus/workspace/git_staging.rb +583 -0
- data/lib/canopus/workspace/hierarchy_aware.rb +477 -0
- data/lib/canopus/workspace/keymap_aware.rb +66 -0
- data/lib/canopus/workspace/language_aware.rb +3721 -135
- data/lib/canopus/workspace/language_server_configurable.rb +287 -71
- data/lib/canopus/workspace/persistent_undo.rb +169 -0
- data/lib/canopus/workspace/problems_aware.rb +131 -0
- data/lib/canopus/workspace/project_searchable.rb +88 -11
- data/lib/canopus/workspace/project_tree_editable.rb +3 -1
- data/lib/canopus/workspace/recoverable.rb +204 -0
- data/lib/canopus/workspace/session_persistable.rb +85 -13
- data/lib/canopus/workspace/settings_aware.rb +76 -2
- data/lib/canopus/workspace/task_aware.rb +194 -0
- data/lib/canopus/workspace/test_aware.rb +394 -0
- data/lib/canopus/workspace/trust.rb +73 -0
- data/lib/canopus/workspace/view/terminal_presentable.rb +298 -47
- data/lib/canopus/workspace/view.rb +399 -74
- data/lib/canopus/workspace.rb +688 -44
- data/lib/canopus.rb +20 -2
- data/sig/canopus.rbs +138 -31
- data/sig/controller.rbs +10 -3
- data/sig/debug.rbs +121 -0
- data/sig/decoration.rbs +12 -2
- data/sig/diagnostics.rbs +26 -0
- data/sig/minimap.rbs +20 -0
- data/sig/provider.rbs +29 -0
- data/sig/services.rbs +1 -16
- data/sig/task.rbs +92 -0
- data/sig/test_runner.rbs +109 -0
- data/sig/trust.rbs +17 -0
- data/sig/workspace_edits.rbs +2 -2
- data/sig/workspace_services.rbs +234 -2
- data/tools/check_dependencies.rb +21 -2
- data/tools/package.rb +172 -14
- data/tools/package_signature_test.rb +33 -0
- data/tools/package_test.rb +29 -0
- data/tools/sign_package.rb +123 -0
- data/tools/update.rb +189 -0
- data/tools/update_test.rb +71 -0
- metadata +206 -31
- data/lib/canopus/lsp/client.rb +0 -307
- data/lib/canopus/lsp/error.rb +0 -7
- data/lib/canopus/lsp/future/subscription.rb +0 -9
- data/lib/canopus/lsp/future.rb +0 -87
- data/lib/canopus/lsp/protocol.rb +0 -83
- data/lib/canopus/lsp/server_error.rb +0 -13
- data/lib/canopus/lsp/timeout.rb +0 -7
- data/lib/canopus/lsp/transport.rb +0 -123
- data/lib/canopus/lsp.rb +0 -19
- data/lib/canopus/project/search.rb +0 -164
- data/lib/canopus/project/search_worker/cancelled.rb +0 -3
- data/lib/canopus/project/search_worker/runner.rb +0 -9
- data/lib/canopus/project/search_worker.rb +0 -108
- data/lib/canopus/terminal/cell.rb +0 -7
- data/lib/canopus/terminal/grid.rb +0 -321
- data/lib/canopus/terminal/pty.rb +0 -178
- data/lib/canopus/terminal/scrollback.rb +0 -38
- data/lib/canopus/terminal/vt.rb +0 -398
- data/lib/canopus/terminal.rb +0 -13
- data/sig/lsp.rbs +0 -99
- data/sig/terminal.rbs +0 -130
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "editor_config"
|
|
4
|
+
|
|
5
|
+
module Canopus
|
|
6
|
+
module Workspace::EditorConfigurable
|
|
7
|
+
EDITORCONFIG_MAX_BYTES = 64 * 1024
|
|
8
|
+
EDITORCONFIG_CACHE_LIMIT = 1_024
|
|
9
|
+
|
|
10
|
+
# Return settings with the file's EditorConfig properties inserted before
|
|
11
|
+
# the project settings layer.
|
|
12
|
+
def settings_for_path(path, language: nil)
|
|
13
|
+
settings = editorconfig_settings(path)
|
|
14
|
+
language ? settings.for_language(language) : settings
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def settings_for_editor(editor)
|
|
18
|
+
settings_for_path(editor.buffer.path, language: editor.language_document.definition.name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def invalidate_editorconfig
|
|
22
|
+
@editorconfig_cache&.clear
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def prepare_editorconfig_save(buffer, path)
|
|
27
|
+
return if buffer.read_only
|
|
28
|
+
values = settings_for_path(path)
|
|
29
|
+
buffer.trim_trailing_whitespace if values["trim_trailing_whitespace"]
|
|
30
|
+
buffer.ensure_final_newline if values["insert_final_newline"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def editorconfig_settings(path)
|
|
36
|
+
return @settings unless @settings["editorconfig"] && path
|
|
37
|
+
properties = editorconfig_properties(path)
|
|
38
|
+
return @settings if properties.empty?
|
|
39
|
+
|
|
40
|
+
layers = @settings.layers.dup
|
|
41
|
+
project_path = File.expand_path(File.join(@root, ".canopus", "settings.jsonc"))
|
|
42
|
+
index = layers.index do |layer|
|
|
43
|
+
next false unless layer.is_a?(String)
|
|
44
|
+
candidate = File.expand_path(layer)
|
|
45
|
+
candidate = File.realpath(candidate) if File.exist?(candidate)
|
|
46
|
+
candidate == project_path
|
|
47
|
+
end
|
|
48
|
+
layers.insert(index || layers.length, properties)
|
|
49
|
+
Settings.new(*layers)
|
|
50
|
+
rescue StandardError => error
|
|
51
|
+
@message = "EditorConfig ignored: #{error.message}"
|
|
52
|
+
@settings
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def editorconfig_properties(path)
|
|
56
|
+
absolute = File.expand_path(path, @root)
|
|
57
|
+
absolute = File.realpath(absolute) if File.exist?(absolute)
|
|
58
|
+
return {} unless absolute == @root || absolute.start_with?(@root + File::SEPARATOR)
|
|
59
|
+
relative = absolute.delete_prefix(@root + File::SEPARATOR)
|
|
60
|
+
return {} if relative.empty?
|
|
61
|
+
|
|
62
|
+
@editorconfig_cache ||= {}
|
|
63
|
+
return @editorconfig_cache[absolute] if @editorconfig_cache.key?(absolute)
|
|
64
|
+
|
|
65
|
+
raw = EditorConfig.load(relative) do |candidate|
|
|
66
|
+
editorconfig_file(candidate)
|
|
67
|
+
end
|
|
68
|
+
properties = map_editorconfig(EditorConfig.preprocess(raw))
|
|
69
|
+
@editorconfig_cache[absolute] = properties.freeze
|
|
70
|
+
@editorconfig_cache.shift if @editorconfig_cache.length > EDITORCONFIG_CACHE_LIMIT
|
|
71
|
+
properties
|
|
72
|
+
rescue ArgumentError, EncodingError, IOError, SystemCallError
|
|
73
|
+
@editorconfig_cache[absolute] = {}.freeze if defined?(@editorconfig_cache) && absolute
|
|
74
|
+
{}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def editorconfig_file(candidate)
|
|
78
|
+
absolute = File.expand_path(candidate, @root)
|
|
79
|
+
real = File.realpath(absolute)
|
|
80
|
+
return unless real == @root || real.start_with?(@root + File::SEPARATOR)
|
|
81
|
+
return unless absolute == @root || absolute.start_with?(@root + File::SEPARATOR)
|
|
82
|
+
stat = File.lstat(absolute)
|
|
83
|
+
return unless stat.file? && !stat.symlink? && stat.size <= EDITORCONFIG_MAX_BYTES
|
|
84
|
+
|
|
85
|
+
source = File.binread(absolute)
|
|
86
|
+
return unless source.force_encoding(Encoding::UTF_8).valid_encoding?
|
|
87
|
+
|
|
88
|
+
source
|
|
89
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP, IOError, SystemCallError
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def map_editorconfig(properties)
|
|
94
|
+
values = {}
|
|
95
|
+
style = properties[EditorConfig::INDENT_STYLE]
|
|
96
|
+
values["use_tabs"] = style == EditorConfig::TAB if [EditorConfig::SPACE, EditorConfig::TAB].include?(style)
|
|
97
|
+
|
|
98
|
+
size = properties[EditorConfig::INDENT_SIZE]
|
|
99
|
+
size = properties[EditorConfig::TAB_WIDTH] if size.nil? || size == EditorConfig::TAB
|
|
100
|
+
values["tab_size"] = size.to_i if size&.match?(/\A[1-9]\d*\z/) && size.to_i <= 16
|
|
101
|
+
|
|
102
|
+
{EditorConfig::TRIM_TRAILING_WHITESPACE => "trim_trailing_whitespace",
|
|
103
|
+
EditorConfig::INSERT_FINAL_NEWLINE => "insert_final_newline"}.each do |source, target|
|
|
104
|
+
value = properties[source]
|
|
105
|
+
values[target] = value == EditorConfig::TRUE if [EditorConfig::TRUE, EditorConfig::FALSE].include?(value)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
length = properties[EditorConfig::MAX_LINE_LENGTH]
|
|
109
|
+
values["max_line_length"] = length.to_i if length&.match?(/\A[1-9]\d*\z/) && length.to_i <= 1_000_000
|
|
110
|
+
values
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -4,10 +4,14 @@ module Canopus
|
|
|
4
4
|
module Workspace::FileChangeAware
|
|
5
5
|
def start_watching
|
|
6
6
|
return if @watcher || !@project
|
|
7
|
-
native =
|
|
8
|
-
Zaniah::Platform.watch(@root)
|
|
9
|
-
rescue LoadError, Zaniah::Error, SystemCallError
|
|
7
|
+
native = if @window && @window.class == Zaniah::Platform::Headless::Window
|
|
10
8
|
nil
|
|
9
|
+
else
|
|
10
|
+
begin
|
|
11
|
+
Zaniah::Platform.watch(@root)
|
|
12
|
+
rescue LoadError, Zaniah::Error, SystemCallError
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
11
15
|
end
|
|
12
16
|
@watcher = @project.watcher(native: native)
|
|
13
17
|
@last_watch = 0
|
|
@@ -20,6 +24,10 @@ module Canopus
|
|
|
20
24
|
events = @watcher.poll
|
|
21
25
|
invalidate_git unless events.empty?
|
|
22
26
|
refresh_files unless events.empty?
|
|
27
|
+
if events.any? { |event| File.basename(event.path) == ".editorconfig" }
|
|
28
|
+
invalidate_editorconfig
|
|
29
|
+
apply_settings
|
|
30
|
+
end
|
|
23
31
|
events.each do |event|
|
|
24
32
|
path = File.expand_path(event.path, @root)
|
|
25
33
|
buffer = @buffers[path]
|
|
@@ -1,35 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../git/state"
|
|
4
|
+
|
|
3
5
|
module Canopus
|
|
4
6
|
module Workspace::GitAware
|
|
5
7
|
def git
|
|
6
8
|
return @git if defined?(@git)
|
|
7
9
|
@git = Thuban::Repository.new(@root)
|
|
8
10
|
@git = nil unless @git.root
|
|
11
|
+
@git
|
|
9
12
|
rescue ArgumentError
|
|
10
13
|
@git = nil
|
|
11
14
|
end
|
|
12
15
|
def git_status
|
|
13
|
-
|
|
14
|
-
return {} unless
|
|
16
|
+
state = git_state
|
|
17
|
+
return {} unless state
|
|
18
|
+
return state.snapshot.entries if state.snapshot
|
|
15
19
|
if @window
|
|
16
20
|
unless @git_status_job&.alive?
|
|
17
|
-
generation = @git_generation
|
|
21
|
+
generation = @git_generation ||= 0
|
|
18
22
|
@git_status_job = Thread.new do
|
|
19
|
-
|
|
20
|
-
post {
|
|
23
|
+
snapshot = state.capture
|
|
24
|
+
post { install_git_snapshot(state, snapshot) if generation == @git_generation } unless @closed
|
|
21
25
|
rescue StandardError => error
|
|
22
26
|
post { @message = "Git status: #{error.message}" } unless @closed
|
|
23
27
|
end
|
|
24
28
|
end
|
|
25
29
|
{}
|
|
26
30
|
else
|
|
27
|
-
|
|
31
|
+
install_git_snapshot(state, state.capture).entries
|
|
28
32
|
end
|
|
29
33
|
end
|
|
30
34
|
def invalidate_git
|
|
31
35
|
@git_generation = (@git_generation || 0) + 1
|
|
32
|
-
@
|
|
36
|
+
@git_state&.invalidate
|
|
33
37
|
@decorations.invalidate(:git)
|
|
34
38
|
@panes.each do |pane|
|
|
35
39
|
pane.editors.each do |current|
|
|
@@ -41,18 +45,18 @@ module Canopus
|
|
|
41
45
|
def poll_git_changes(now: Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
42
46
|
return if @last_git_poll && now - @last_git_poll < 1
|
|
43
47
|
@last_git_poll = now
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
state = git_state
|
|
49
|
+
return unless state
|
|
50
|
+
unless @git_poll_job&.alive?
|
|
51
|
+
generation = @git_generation ||= 0
|
|
52
|
+
@git_poll_job = Thread.new do
|
|
53
|
+
identity = state.identity
|
|
54
|
+
post { install_git_identity(identity) if generation == @git_generation } unless @closed
|
|
55
|
+
rescue StandardError => error
|
|
56
|
+
post { @message = "Git: #{error.message}" } unless @closed
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
nil
|
|
56
60
|
end
|
|
57
61
|
def git_relative_path(buffer = editor.buffer)
|
|
58
62
|
raise Error, "Current document is not in a Git repository" unless git && buffer.path && buffer.path.start_with?(git.root + File::SEPARATOR)
|
|
@@ -61,22 +65,25 @@ module Canopus
|
|
|
61
65
|
def git_diff(buffer = editor.buffer, async: true)
|
|
62
66
|
return unless git && buffer.path && buffer.path.start_with?(git.root + File::SEPARATOR) && buffer.rope.bytesize < 10 << 20
|
|
63
67
|
path = git_relative_path(buffer)
|
|
64
|
-
|
|
65
|
-
key = [buffer.object_id, path, buffer.version,
|
|
66
|
-
|
|
67
|
-
return
|
|
68
|
+
state = git_state
|
|
69
|
+
key = [buffer.object_id, path, buffer.version, state.snapshot&.head]
|
|
70
|
+
cached = state.cached_diff(key)
|
|
71
|
+
return cached if cached
|
|
68
72
|
if @window && async
|
|
69
73
|
@git_diff_jobs ||= {}
|
|
70
74
|
unless @git_diff_jobs[buffer]&.alive?
|
|
71
75
|
snapshot = buffer.rope
|
|
76
|
+
generation = @git_generation ||= 0
|
|
72
77
|
@git_diff_jobs[buffer] = Thread.new do
|
|
73
|
-
before = Buffer.decode_bytes(
|
|
78
|
+
before = Buffer.decode_bytes(state.synchronize { |repository| repository.blob(path, reference: key.last || "HEAD") }.to_s).first
|
|
74
79
|
diff = Porrima.diff(before, snapshot.to_s, context: 0)
|
|
75
80
|
diff.hunks
|
|
76
81
|
diff.marks
|
|
77
82
|
post do
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
if generation == @git_generation
|
|
84
|
+
state.store_diff(key, diff)
|
|
85
|
+
@decorations.invalidate(:git, buffer: buffer)
|
|
86
|
+
end
|
|
80
87
|
end unless @closed
|
|
81
88
|
rescue StandardError => error
|
|
82
89
|
post { @message = "Git diff: #{error.message}" } unless @closed
|
|
@@ -84,11 +91,11 @@ module Canopus
|
|
|
84
91
|
end
|
|
85
92
|
nil
|
|
86
93
|
else
|
|
87
|
-
before = Buffer.decode_bytes(
|
|
94
|
+
before = Buffer.decode_bytes(state.synchronize { |repository| repository.blob(path, reference: key.last || "HEAD") }.to_s).first
|
|
88
95
|
diff = Porrima.diff(before, buffer.text, context: 0)
|
|
89
96
|
diff.hunks
|
|
90
97
|
diff.marks
|
|
91
|
-
|
|
98
|
+
state.store_diff(key, diff)
|
|
92
99
|
end
|
|
93
100
|
end
|
|
94
101
|
def git_hunks(buffer = editor.buffer, async: true) = git_diff(buffer, async: async)&.hunks || []
|
|
@@ -100,23 +107,24 @@ module Canopus
|
|
|
100
107
|
next if first + count <= rows.begin || first >= rows.end
|
|
101
108
|
|
|
102
109
|
color = mark.kind == :removed ? :error : mark.kind == :added ? "#80b987" : :accent
|
|
103
|
-
style = {color: color, rows: count}.freeze
|
|
110
|
+
style = {color: color, rows: count, hit_width: 7}.freeze
|
|
104
111
|
click = ->(current, row) { toggle_git_hunk(current, row: row) }
|
|
105
112
|
Decoration::Item.new(:gutter, nil, first, "Toggle Git hunk", style, 0, :git, click)
|
|
106
113
|
end
|
|
107
114
|
end
|
|
108
115
|
def show_git_diff
|
|
109
116
|
path = git_relative_path
|
|
110
|
-
before = Buffer.decode_bytes(
|
|
117
|
+
before = Buffer.decode_bytes(git_state.synchronize { |repository| repository.blob(path) }.to_s).first
|
|
111
118
|
buffer = Buffer.new(Porrima.unified(before, editor.buffer.text, old_name: "a/#{path}", new_name: "b/#{path}"), read_only: true)
|
|
112
119
|
@buffers[buffer.object_id] = buffer
|
|
113
120
|
@active_pane.open(buffer).language = Language::Definition.new("diff", "diff", [], "", /\A\z/, /\A\z/, [])
|
|
121
|
+
invalidate_hidden_selection_ranges
|
|
114
122
|
end
|
|
115
123
|
def toggle_git_hunk(current = editor, row: nil)
|
|
116
124
|
row ||= current.buffer.rope.point_at(current.primary.head).row
|
|
117
125
|
hunk = git_diff(current.buffer, async: false)&.hunk_at(new_line: row + 1)
|
|
118
126
|
raise Error, "No change at the cursor" unless hunk
|
|
119
|
-
id = [:git_hunk, current.buffer.path, current.buffer.version,
|
|
127
|
+
id = [:git_hunk, current.buffer.path, current.buffer.version, git_state.snapshot&.head, hunk.old_start, hunk.new_start]
|
|
120
128
|
map = current.display_map
|
|
121
129
|
if map.block_map.blocks.key?(id)
|
|
122
130
|
map.remove_block(id)
|
|
@@ -138,7 +146,7 @@ module Canopus
|
|
|
138
146
|
end
|
|
139
147
|
def show_git_blame
|
|
140
148
|
path = git_relative_path
|
|
141
|
-
lines =
|
|
149
|
+
lines = git_state.synchronize { |repository| repository.blame(path) }
|
|
142
150
|
@hover_card = lines.map { |line| "#{line.commit.to_s[0, 8]} #{line.author} #{line.text}" }.join
|
|
143
151
|
end
|
|
144
152
|
def revert_current_hunk
|
|
@@ -150,12 +158,50 @@ module Canopus
|
|
|
150
158
|
end
|
|
151
159
|
def checkout_branch(name)
|
|
152
160
|
raise Error, "Save or discard all buffer changes before switching branches" if @buffers.values.any?(&:dirty?)
|
|
153
|
-
raise Error, "Not a Git repository" unless
|
|
154
|
-
|
|
161
|
+
raise Error, "Not a Git repository" unless git_state
|
|
162
|
+
git_state.synchronize { |repository| repository.checkout(name) }
|
|
155
163
|
invalidate_git
|
|
156
164
|
refresh_files
|
|
157
165
|
@buffers.values.each { |buffer| buffer.reload if buffer.path && File.file?(buffer.path) && !buffer.read_only }
|
|
158
166
|
@message = "Switched to #{name}"
|
|
159
167
|
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def git_state
|
|
172
|
+
return @git_state if defined?(@git_state)
|
|
173
|
+
@git_state = Git::State.new(git) if git
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def install_git_snapshot(state, snapshot)
|
|
177
|
+
state.install(snapshot)
|
|
178
|
+
@git_identity = snapshot.identity
|
|
179
|
+
refresh_scm if @scm_tree
|
|
180
|
+
snapshot
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def install_git_identity(identity)
|
|
184
|
+
initial = !defined?(@git_identity)
|
|
185
|
+
changed = !initial && @git_identity != identity
|
|
186
|
+
@git_identity = identity
|
|
187
|
+
return unless changed
|
|
188
|
+
|
|
189
|
+
invalidate_git
|
|
190
|
+
refresh_scm if @scm_tree
|
|
191
|
+
@window&.request_frame
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def git_branches
|
|
195
|
+
state = git_state
|
|
196
|
+
state ? state.synchronize { |repository| repository.branches } : []
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def close_git
|
|
200
|
+
[@git_status_job, @git_poll_job, @git_commit_job, *@git_diff_jobs&.values].compact.uniq.each do |thread|
|
|
201
|
+
thread.join unless thread.equal?(Thread.current)
|
|
202
|
+
end
|
|
203
|
+
@git_diff_jobs&.clear
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
160
206
|
end
|
|
161
207
|
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Canopus
|
|
4
|
+
module Workspace::GitBlame
|
|
5
|
+
BLAME_MAX_BYTES = 1 << 20
|
|
6
|
+
BLAME_MAX_LINES = 20_000
|
|
7
|
+
BLAME_CACHE_LIMIT = 16
|
|
8
|
+
BLAME_BUDGET = Porrima::Budget.new(max_bytes: 2 << 20, max_lines: BLAME_MAX_LINES)
|
|
9
|
+
|
|
10
|
+
GitBlameLine = Data.define(:commit, :author, :original_line, :path)
|
|
11
|
+
GitBlameSnapshot = Data.define(:key, :lines, :reason)
|
|
12
|
+
|
|
13
|
+
def git_blame_decorations(buffer, rows, current = nil)
|
|
14
|
+
mode = @settings["git"]["inline_blame"]
|
|
15
|
+
return [] if mode == "off" || !buffer.path || buffer.rope.bytesize > BLAME_MAX_BYTES ||
|
|
16
|
+
buffer.line_count > BLAME_MAX_LINES
|
|
17
|
+
text = buffer.text
|
|
18
|
+
return [] unless git_blame_text_supported?(text)
|
|
19
|
+
|
|
20
|
+
path = safe_git_diff_path(git_relative_path(buffer))
|
|
21
|
+
key = [buffer.object_id, buffer.version, @git_generation.to_i, path].freeze
|
|
22
|
+
snapshot = (@git_blame_cache ||= {})[key]
|
|
23
|
+
unless snapshot
|
|
24
|
+
requested = request_git_blame(buffer, path, key, text.dup.freeze)
|
|
25
|
+
snapshot = requested if requested.is_a?(GitBlameSnapshot)
|
|
26
|
+
return [] unless snapshot
|
|
27
|
+
end
|
|
28
|
+
return [] if snapshot.reason
|
|
29
|
+
|
|
30
|
+
visible = rows.begin...[rows.end, buffer.line_count].min
|
|
31
|
+
selected = if mode == "cursor"
|
|
32
|
+
return [] unless current.is_a?(Editor) && current.buffer.equal?(buffer)
|
|
33
|
+
row = buffer.rope.point_at(current.primary.head).row
|
|
34
|
+
visible.cover?(row) ? [row] : []
|
|
35
|
+
else
|
|
36
|
+
visible.to_a
|
|
37
|
+
end
|
|
38
|
+
selected.filter_map do |row|
|
|
39
|
+
next if row == buffer.line_count - 1 && buffer.line(row).empty?
|
|
40
|
+
git_blame_decoration(buffer, row, snapshot.lines[row])
|
|
41
|
+
end
|
|
42
|
+
rescue Error
|
|
43
|
+
[]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def invalidate_git_blame(buffer = nil)
|
|
47
|
+
@git_blame_generation = (@git_blame_generation || 0) + 1
|
|
48
|
+
if buffer
|
|
49
|
+
@git_blame_cache&.delete_if { |key, _| key[0] == buffer.object_id }
|
|
50
|
+
else
|
|
51
|
+
@git_blame_cache&.clear
|
|
52
|
+
end
|
|
53
|
+
@decorations.invalidate(:blame, buffer: buffer)
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def invalidate_git
|
|
58
|
+
invalidate_git_blame
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def close_git
|
|
63
|
+
@git_blame_generation = (@git_blame_generation || 0) + 1
|
|
64
|
+
jobs = [*@git_blame_jobs&.values].compact.uniq.reject { |thread| thread.equal?(Thread.current) }
|
|
65
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 0.1
|
|
66
|
+
jobs.each do |thread|
|
|
67
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
68
|
+
break unless remaining.positive?
|
|
69
|
+
thread.join(remaining)
|
|
70
|
+
end
|
|
71
|
+
jobs.each { |thread| thread.kill if thread.alive? }
|
|
72
|
+
jobs.each(&:join)
|
|
73
|
+
@git_blame_jobs&.clear
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def request_git_blame(buffer, path, key, text)
|
|
80
|
+
@git_blame_jobs ||= {}
|
|
81
|
+
return if @git_blame_jobs[buffer]&.alive?
|
|
82
|
+
|
|
83
|
+
generation = @git_blame_generation ||= 0
|
|
84
|
+
work = lambda do
|
|
85
|
+
capture = git_state.synchronize do |repository|
|
|
86
|
+
raw = repository.blob(path)
|
|
87
|
+
if raw && raw.bytesize <= BLAME_MAX_BYTES
|
|
88
|
+
committed = Buffer.decode_bytes(raw).first
|
|
89
|
+
git_blame_text_supported?(committed) ? [committed, repository.blame(path)].freeze : [nil, nil].freeze
|
|
90
|
+
else
|
|
91
|
+
[nil, nil].freeze
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
materialize_git_blame(key, text, *capture)
|
|
95
|
+
end
|
|
96
|
+
return install_git_blame(buffer, work.call) unless @window
|
|
97
|
+
|
|
98
|
+
@git_blame_jobs[buffer] = Thread.new do
|
|
99
|
+
current = Thread.current
|
|
100
|
+
snapshot = work.call
|
|
101
|
+
post { finish_git_blame(current, generation, buffer, snapshot, nil) } unless @closed
|
|
102
|
+
rescue StandardError => error
|
|
103
|
+
post { finish_git_blame(current, generation, buffer, nil, error) } unless @closed
|
|
104
|
+
end
|
|
105
|
+
rescue StandardError => error
|
|
106
|
+
install_git_blame(buffer, GitBlameSnapshot.new(key, [].freeze,
|
|
107
|
+
safe_git_history_text(error.message, 200).freeze).freeze)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def git_blame_text_supported?(text)
|
|
111
|
+
text.bytesize <= BLAME_MAX_BYTES && text.count("\n") + 1 <= BLAME_MAX_LINES &&
|
|
112
|
+
!text.match?(/\r(?!\n)|[\u2028\u2029]/)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def materialize_git_blame(key, text, committed, blamed)
|
|
116
|
+
return GitBlameSnapshot.new(key, [].freeze, "Blame unavailable for large or missing content".freeze).freeze unless committed
|
|
117
|
+
reason = scm_diff_budget_reason(committed, text)
|
|
118
|
+
return GitBlameSnapshot.new(key, [].freeze, "Blame unavailable: #{scm_diff_budget_description(reason)}".freeze).freeze if reason
|
|
119
|
+
|
|
120
|
+
lines = Array.new(text.lines.length)
|
|
121
|
+
comparison_committed = committed.gsub("\r\n", "\n")
|
|
122
|
+
comparison_text = text.gsub("\r\n", "\n")
|
|
123
|
+
if comparison_committed == comparison_text
|
|
124
|
+
blamed.each_with_index { |line, index| lines[index] = copy_git_blame_line(line) }
|
|
125
|
+
else
|
|
126
|
+
Porrima.diff(comparison_committed, comparison_text, budget: BLAME_BUDGET).edits.each do |edit|
|
|
127
|
+
next unless edit.kind == :equal
|
|
128
|
+
lines[edit.new_line - 1] = copy_git_blame_line(blamed[edit.old_line - 1])
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
GitBlameSnapshot.new(key, lines.freeze, nil).freeze
|
|
132
|
+
rescue Porrima::BudgetExceeded
|
|
133
|
+
GitBlameSnapshot.new(key, [].freeze, "Blame unavailable: diff budget exceeded".freeze).freeze
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def copy_git_blame_line(line)
|
|
137
|
+
return unless line
|
|
138
|
+
GitBlameLine.new(line.commit.to_s.dup.freeze, safe_git_history_text(line.author, 100).freeze,
|
|
139
|
+
line.original_line, line.path.to_s.dup.freeze).freeze
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def git_blame_decoration(buffer, row, line)
|
|
143
|
+
source = buffer.line(row)
|
|
144
|
+
body = source.delete_suffix("\n").delete_suffix("\r")
|
|
145
|
+
offset = buffer.rope.line_start(row) + body.bytesize
|
|
146
|
+
content = if line
|
|
147
|
+
" #{line.author} · #{line.commit[0, 8]}"
|
|
148
|
+
else
|
|
149
|
+
" Uncommitted changes"
|
|
150
|
+
end
|
|
151
|
+
Decoration::Item.new(:inline, offset...offset, row, content.freeze,
|
|
152
|
+
{color: :muted, padding_left: 8, cells: Zaniah::Unicode.width(content)}.freeze,
|
|
153
|
+
90, :blame, nil)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def install_git_blame(buffer, snapshot)
|
|
157
|
+
cache = @git_blame_cache ||= {}
|
|
158
|
+
cache.shift while cache.length >= BLAME_CACHE_LIMIT
|
|
159
|
+
cache[snapshot.key] = snapshot
|
|
160
|
+
@decorations.invalidate(:blame, buffer: buffer)
|
|
161
|
+
@window&.request_frame
|
|
162
|
+
snapshot
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def finish_git_blame(worker, generation, buffer, snapshot, error)
|
|
166
|
+
@git_blame_jobs&.delete(buffer) if @git_blame_jobs&.[](buffer).equal?(worker)
|
|
167
|
+
return unless generation == @git_blame_generation
|
|
168
|
+
if error
|
|
169
|
+
self.message = "Git blame: #{safe_git_history_text(error.message, 200)}"
|
|
170
|
+
else
|
|
171
|
+
install_git_blame(buffer, snapshot)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|